
Chismes del Lince 25: Heltec Bluetooth, el letrero del Lince
Este proyecto de Heltec Bluetooth es el letrero del Lince, el Chisme 25: escribes un mensaje en el móvil y aparece, al momento, en la pantalla OLED de una placa Heltec LoRa 32 con ESP32. La conexión Heltec Bluetooth es inalámbrica, sin cables y sin emparejar nada, y funciona desde el navegador del móvil.
Es el primer Chisme con la Heltec y nuestro primer proyecto de Heltec Bluetooth. Hasta ahora hemos trabajado con la micro:bit (la brújula, el nivel de burbuja, Simón dice, la estación del tiempo y la pizarra de LEDs). La Heltec es una placa con un chip ESP32, más potente que la micro:bit, con pantalla OLED incluida y con radio LoRa para llegar a kilómetros. Lo que cambia es cómo se programa: en lugar de bloques de MakeCode, usamos código escrito en el programa Arduino IDE. Con Heltec Bluetooth aprenderás a montar un entorno de programación real, y el resultado es una introducción muy sencilla a ESP32 Web Bluetooth.
Antes de empezar: al ser la primera vez con la Heltec, la preparación (pasos 1 a 4) es larga: hay que instalar el programa, el soporte de la placa y dos librerías. Se hace una sola vez. En los próximos Chismes de Heltec Bluetooth solo tendrás que pegar el código y pulsar un botón. Ve con calma, paso a paso, y no te saltes ninguno.
Heltec Bluetooth: qué vamos a construir
La página web tiene:
- Un botón para conectar la Heltec por Bluetooth.
- Una pantallita de vista previa que enseña lo que verás en la placa.
- Un cuadro de texto de hasta 60 letras, con mensajes de ejemplo (HOLA, Hola Lince, MonteSTEAM, Funciona!).
- Un botón Enviar a la Heltec y otro para borrar la pantalla.
Y la Heltec Bluetooth también le habla a la página: cuando recibe un mensaje contesta «lo he recibido», y si pulsas el botón PRG de la placa, la página se entera. Así compruebas que el Bluetooth funciona en los dos sentidos con una sola prueba.
Lo que necesitas para tu Heltec Bluetooth
- Una placa Heltec WiFi LoRa 32 o una TTGO LoRa32 v1.3 (son parecidas; abajo te explico la diferencia).
- Un cable USB que transmita datos. Ojo: algunos cables solo sirven para cargar y con ellos el ordenador no verá la placa.
- Un ordenador (Windows, Mac o Linux) con conexión a internet.
- Un móvil Android con Chrome, con el Bluetooth y la ubicación encendidos. En iPhone, Safari no sirve: hay que abrir la página en el navegador Bluefy.
Dos placas parecidas, pines distintos
La pantalla OLED de la placa está conectada a unos pines concretos, y no son los mismos en todas las revisiones. Mira lo que pone en tu placa:
| Placa | SDA | SCL | RST (reset de la pantalla) |
|---|---|---|---|
| TTGO LoRa32 v1.x (el código tal como está) | 21 | 22 | no tiene (en el código lo dejamos en 16; si la pantalla no sale, pon -1) |
| Heltec WiFi LoRa 32 V1 / V2 | 4 | 15 | 16 |
El botón PRG es el pin 0 en las dos. Si la pantalla se queda en negro, en el paso 5 te digo qué tres líneas cambiar.
Paso 1. Instala el Arduino IDE
El Arduino IDE es el programa donde escribiremos el código y desde el que lo pasaremos a la placa.
- Entra en la página de descargas del Arduino IDE.
- Descarga Arduino IDE 2.x para tu sistema (Windows, macOS o Linux).
- Instálalo aceptando las opciones que propone. Si Windows pregunta si permites instalar «controladores» o dispositivos, di que sí.
- Ábrelo. Verás una ventana con un programa de ejemplo con dos bloques:
void setup()yvoid loop().
Paso 2. Conecta la placa y comprueba que el ordenador la ve
- Conecta la Heltec al ordenador con el cable USB. Se encenderá la pantalla o un LED.
- En el Arduino IDE, abre el menú Herramientas → Puerto.
- Debe aparecer un puerto nuevo: en Windows algo como
COM3oCOM5; en Mac,/dev/cu.usbserial-…o/dev/cu.SLAB_USBtoUART.
¿No aparece ningún puerto? Prueba por orden:
- Cambia el cable por otro (casi siempre es el cable).
- Cambia de puerto USB del ordenador.
- Instala el controlador CP210x (el chip que traduce el USB de la placa; en Windows y Mac suele hacer falta). Búscalo como «CP210x USB to UART driver» en la web de Silicon Labs. Algunas TTGO usan en su lugar el chip CH340/CH9102 y su controlador se busca igual.
- Desenchufa y vuelve a enchufar la placa.
Paso 3. Añade el soporte para placas ESP32
El Arduino IDE no sabe programar el ESP32 «de fábrica»: hay que enseñárselo (la guía oficial está en la documentación de Arduino-ESP32 de Espressif). Son dos partes.
Parte A: dar la dirección de descarga
- Menú Archivo → Preferencias (en Mac: Arduino IDE → Ajustes).
- Busca el campo Gestor de URLs adicionales de tarjetas (Additional boards manager URLs).
- Pega esta dirección:
https://espressif.github.io/arduino-esp32/package_esp32_index.json
- Pulsa OK.
Parte B: instalar el paquete
- A la izquierda, pulsa el icono del Gestor de tarjetas (Boards Manager): es una placa pequeña. También vale Herramientas → Placa → Gestor de tarjetas.
- En el buscador escribe
esp32. - Busca esp32 by Espressif Systems y pulsa Instalar.
- Espera. Descarga varios cientos de megas y puede tardar varios minutos. Al terminar pondrá «INSTALLED».

Paso 4. Instala las librerías de la pantalla
Una librería es un código ya hecho por otras personas que nos ahorra trabajo. Para dibujar texto en la pantalla OLED usamos dos. La de Bluetooth (BLE) ya viene incluida con el paquete del paso 3: no hay que instalarla.
- Pulsa el icono de Librerías (Library Manager, el de los libros) o usa Herramientas → Administrar bibliotecas.
- Escribe
Adafruit SSD1306y pulsa Instalar en la que se llama Adafruit SSD1306 (autora: Adafruit). - Te saldrá una ventana preguntando si quieres instalar también las dependencias. Pulsa Install all (instalar todas). Así se instalan juntas Adafruit GFX Library y Adafruit BusIO.
- Comprueba que las tres aparecen como instaladas: busca
Adafruit GFXyAdafruit BusIOy mira que ponga «INSTALLED».

Paso 5. Pega el código de la placa
- Menú Archivo → Nuevo sketch. (Sketch es como se llaman los programas de Arduino.)
- Borra todo lo que haya en la ventana.
- Copia el código del firmware que tienes al final de esta página (sección «Código 1») y pégalo.
- Guarda con Archivo → Guardar y ponle de nombre
chisme25_heltec_texto.
Si tu placa es una Heltec V1/V2 (la de pines 4, 15 y 16), cambia estas líneas, que están al principio del código:
#define OLED_SDA 4
#define OLED_SCL 15
#define OLED_RST 16
Si es una TTGO y la pantalla no se enciende, prueba a poner #define OLED_RST -1.
Paso 6. Elige la placa y el puerto
⚠️ Muy importante: elige «ESP32 Dev Module», aunque tu placa sea una Heltec. En la lista de placas del Arduino IDE hay otras que suenan mucho mejor para tu placa (por ejemplo «Heltec WiFi LoRa 32», «WiFi LoRa 32(V2)» o «TTGO LoRa32»), pero no las uses en este Chisme: con ellas pueden salir errores raros, pines distintos o una pantalla que no responde. La que funciona es ESP32 Dev Module. Si algo va mal más adelante, lo primero que hay que revisar es esto.
- Menú Herramientas → Placa → esp32 → ESP32 Dev Module. Es una lista larguísima ordenada alfabéticamente: baja hasta la E. Si te cuesta, en la barra de arriba del programa, pulsa el nombre de la placa y escribe
ESP32 Deven el buscador. - Comprueba que arriba, junto a la flecha de subir, pone «ESP32 Dev Module» y el puerto de tu placa (por ejemplo
COM5). - Herramientas → Puerto: elige el puerto que apareció en el paso 2.
- Deja el resto de opciones como están.

Paso 7. Sube el programa a la placa
- Pulsa el botón Subir (la flecha → arriba a la izquierda).
- Abajo verás la consola con mensajes. Primero compila (tarda un rato la primera vez, porque el Bluetooth es grande) y luego sube.
- Si aparece
Connecting........____y se queda ahí, mantén pulsado el botón PRG de la placa hasta que empiece a salirWriting at 0x…; entonces lo sueltas. - Al terminar dirá
Hard resetting via RTS pin...y la placa se reiniciará. - Mira la pantalla de la Heltec. Debe poner:
Heltec-Lince
Esperando conexion
Bluetooth...
Si lo ves, la placa ya está lista. ¡Lo difícil ya está hecho!
Si el programa da un error:
Adafruit_SSD1306.h: No such file or directory: falta una librería. Repite el paso 4.Sketch too bigotext section exceeds available space: Herramientas → Partition Scheme → Huge APP (3MB No OTA/1MB SPIFFS) y vuelve a subir.Failed to connecto no encuentra el puerto: revisa el cable, el puerto del paso 2 y mantén pulsado PRG como en el punto 3.- La pantalla sigue negra pero sube bien: son los pines de la OLED; mira el paso 5.

Paso 8. Prepara la página web de tu Heltec Bluetooth
La página es un único archivo HTML. Tienes dos opciones:
- Usar la nuestra: abre directamente https://montesteam.org/app/chisme-25-heltec-texto.html
- Tener la tuya: copia el código de la página (sección «Código 2», al final), guárdalo como
chisme-25-heltec-texto.htmly súbelo a un sitio web con https (por ejemplo, GitHub Pages o tu propio hosting). Es obligatorio que seahttps://: el Bluetooth del navegador no funciona desde un archivo abierto del ordenador ni desde una direcciónhttp://.
Paso 9. Prepara el móvil
- Bluetooth y ubicación encendidos.
- Si en Ajustes → Bluetooth aparece «Heltec-Lince», pulsa Olvidar. No hay que emparejarla desde ahí.
- Cierra cualquier otra app o pestaña que esté usando la placa.
Paso 10. Abre la página y conecta la Heltec Bluetooth
- Abre la página en Chrome, escribiendo la dirección directamente.
- Pulsa Conectar Heltec.
- Elige Heltec-Lince en la lista.
- Verás pasar mensajes («Conectando…», «Buscando el servicio UART…», «Activando notificaciones…») y por fin «Conectado a Heltec-Lince. ¡Escribe algo!». En la placa, la pantalla cambiará a «Conectado».

Paso 11. Escribe y envía
- Teclea un mensaje o toca uno de los de ejemplo.
- Pulsa Enviar a la Heltec. El LED de la placa parpadea, el texto aparece en la pantalla y la página dice «La Heltec lo ha recibido ✔».
- Prueba un mensaje corto (se ve en letra grande) y uno largo (se ve en letra pequeña, en varias líneas).
- Ahora pulsa el botón PRG de la placa: la página dirá «¡Has pulsado el botón PRG de la Heltec!» y el móvil vibrará.
- Borrar la pantalla deja la placa en su pantalla de espera.
Cómo funciona el Heltec Bluetooth del letrero del Lince
Las dos placas hablan el mismo «idioma»
Las páginas de los Chismes 19 a 24 hablan con la micro:bit por un servicio Bluetooth llamado Nordic UART. Es como un «chat de texto» entre dos aparatos, y el navegador habla con él gracias a la API Web Bluetooth, que viene incluida en Chrome. En nuestro Heltec Bluetooth, la placa imita ese mismo servicio, y por eso la parte de conexión de la página es casi idéntica a la de la micro:bit, con sus reintentos y su reconexión automática.
| Pieza | Código (UUID) | Para qué sirve |
|---|---|---|
| Servicio | 6e400001-… | La «puerta» por la que se habla |
| RX | 6e400002-… | Por aquí escribe la página y lee la placa |
| TX | 6e400003-… | Por aquí escribe la placa y avisa a la página |
Las flechas de RX y TX se nombran desde el punto de vista de la placa: RX es lo que ella recibe, TX lo que ella transmite.
Una línea de texto, en trozos de 20 letras
La página manda el mensaje como una línea de texto que termina con un salto de línea (\n). Bluetooth solo permite enviar paquetes pequeños, así que el mensaje viaja en trozos de 20 bytes. La placa va juntando los trozos y, cuando llega el \n, sabe que el mensaje está completo.
La placa no pinta en cuanto recibe
Cuando llegan datos, el Bluetooth ejecuta la función onWrite. Esa función solo guarda el texto y levanta una bandera (hayLinea). Quien realmente pinta en la pantalla es el loop(), que revisa la bandera continuamente. Es una regla de oro con el ESP32: lo que pasa dentro del Bluetooth debe ser rápido, y el trabajo lento (como dibujar) se hace fuera.
Letra grande o pequeña
La pantalla mide 128 × 64 puntos. La función envolver() reparte el texto en líneas sin partir palabras:
- Letra grande: 10 letras por línea y 4 líneas (hasta unas 40 letras).
- Letra pequeña: 21 letras por línea y 8 líneas, si el mensaje no cabe en grande.
Después centra el bloque de texto en vertical.
La placa también habla
Cuando pinta un mensaje, la placa envía OK: y las primeras letras del texto por TX. Cuando pulsas PRG, envía PRG. La página las recibe en su función recibir() y las convierte en avisos.
Si se corta, vuelve a ser visible
En onDisconnect, la placa llama a startAdvertising(): así vuelve a «anunciarse» por Bluetooth y la página puede reconectar sola (hasta 20 intentos).
Micro:bit y Heltec Bluetooth, de un vistazo
| micro:bit | Heltec | |
|---|---|---|
| Se programa con | Bloques de MakeCode | Código C++ en Arduino IDE |
| Pantalla | 25 LEDs | OLED 128 × 64 puntos |
| Emparejamiento | Hay que activar «No se requiere emparejamiento» | No hace falta ningún ajuste |
| Nombre en la lista | BBC micro:bit | Heltec-Lince |
| Acelerómetro y brújula | Incluidos | No trae (habría que añadir un sensor) |

Retos para clase con Heltec Bluetooth
Prueba estos retos para dominar tu Heltec Bluetooth.
- Cambia el nombre: en el código aparece
BLEDevice::init("Heltec-Lince"). Pon el nombre de tu clase y comprueba que aparece en la lista de Bluetooth. - Mensajes de bienvenida: cambia el texto de
pantallaEspera()para que ponga el nombre de tu centro. - Letra pequeña o grande: ¿hasta cuántas letras salen en letra grande? Mándale mensajes de 10, 20, 30, 40 y 41 letras y anota cuándo cambia el tamaño.
- El mensaje más largo: el código acepta hasta 120 letras; la página, 60. ¿Se lee bien un mensaje de 60 letras en la pantalla?
- Contador de pulsaciones: haz que la página cuente cuántas veces pulsas PRG y lo muestre en grande.
- Reto avanzado: haz que la placa también muestre en la pantalla cuántos mensajes ha recibido desde que se encendió.
Si tu Heltec Bluetooth falla
- Lo primero de todo, si algo va raro: mira arriba, junto a la flecha de subir, y comprueba que la placa seleccionada es ESP32 Dev Module (paso 6). Elegir otra, como «Heltec WiFi LoRa 32», es la causa más fácil de pasar por alto.
- El Arduino IDE no ve la placa: cable de datos, otro puerto USB y controlador CP210x (paso 2).
- No sube el programa («Connecting…»): mantén pulsado PRG mientras empieza a subir.
- Sube bien, pero la pantalla sigue negra: son los pines de la OLED (paso 5) o el
OLED_RST. - La pantalla pone «Esperando conexion» y no pasa de ahí: es normal hasta que conectes desde la página.
- No aparece «Heltec-Lince» en la lista del móvil: comprueba que la placa está encendida y con el programa cargado, reiníciala con el botón RST y activa Bluetooth y ubicación en el móvil.
- Aparece en la lista, pero no conecta: olvídala en Ajustes → Bluetooth del móvil, cierra otras apps que la usen y reinicia la placa.
- Error «NotSupportedError»: olvida la placa en Ajustes → Bluetooth, borra la caché de la app «Bluetooth» del sistema y reinicia el móvil y la placa.
- Error «NetworkError»: que no haya otra app conectada y reinicia la placa (botón RST).
- Conecta pero el texto no sale: pulsa «Borrar la pantalla» y vuelve a enviarlo; comprueba que la placa sigue conectada.
- Salen letras distintas de las que escribí: la pantalla no sabe tildes ni ñ; la página las cambia (á por a, ñ por n…). Debajo del cuadro te dice qué leerá la Heltec.
- En iPhone no conecta: abre la página con Bluefy.
- La página debe abrirse con https://: no funciona desde un archivo local.
Preguntas frecuentes
¿Se puede usar Heltec Bluetooth con el móvil?
Sí. El ESP32 de la Heltec tiene Bluetooth BLE, y por eso Heltec Bluetooth funciona con el móvil. Cargando el programa de este Chisme, la placa se anuncia como «Heltec-Lince» y una página web abierta en Chrome puede conectar con ella y mandarle texto.
¿Necesito una app para el móvil?
No. Para usar Heltec Bluetooth basta con abrir la página en Chrome (Android) o en el navegador Bluefy (iPhone). La página usa Web Bluetooth, que viene incluido en el navegador.
¿Por qué no hay que emparejar la Heltec en los ajustes del móvil?
Porque la página conecta directamente desde el navegador. De hecho, es mejor no emparejarla desde Ajustes: si ya está emparejada, conviene «olvidarla» antes de probar.
¿Qué diferencia hay entre la Heltec y la micro:bit?
La micro:bit se programa con bloques y trae acelerómetro y brújula; la Heltec se programa con código en el Arduino IDE y trae pantalla OLED y radio LoRa. Las dos se pueden controlar desde el móvil con la misma página, porque la Heltec imita el servicio Bluetooth de la micro:bit.
¿Por qué no sale la pantalla de mi Heltec Bluetooth?
Casi siempre es una cuestión de pines. La TTGO LoRa32 v1.3 y la Heltec V2 conectan la pantalla a pines diferentes. Prueba con los valores de la tabla del principio.
Qué hemos aprendido
Con este proyecto de Heltec Bluetooth hemos visto que una placa con Bluetooth puede recibir órdenes de una página web igual que las manda, y que dos aparatos distintos se entienden si hablan el mismo protocolo (aquí, el servicio Nordic UART). Hemos aprendido que el texto viaja en líneas con un final que avisa de que el mensaje está completo y que se parte en trozos para que quepa en cada paquete. Y, de paso, hemos dado el primer paso con un entorno de programación real: instalar un programa, añadir soporte para una placa y usar librerías, la rutina de cualquier proyecto con Arduino.
¿Quieres seguir con Heltec Bluetooth y micro:bit? Mira el curso gratuito de micro:bit de MonteSTEAM y el resto de los Chismes del Lince.
Datos para WordPress
- Título de la entrada (H1): Chismes del Lince 25: Heltec Bluetooth, el letrero del Lince
- Slug (URL):
chismes-del-lince-25-heltec-bluetooth - Categorías: AIDARAC – Ciencia Ciudadana, Los Chismes del Lince, Proyecto AIDARAC (quita «Uncategorized»)
- Etiquetas: Heltec Bluetooth, Heltec LoRa 32, ESP32, Web Bluetooth, BLE, Arduino IDE, pantalla OLED, Chismes del Lince, steam
- Imagen destacada:
heltec-bluetooth-letrero.png
Rank Math (panel SEO de la entrada)
- Palabra clave principal:
Heltec Bluetooth - Palabras clave secundarias (Rank Math gratis admite hasta 5 en total):
Heltec LoRa 32,pantalla OLED,ESP32 Web Bluetooth - Título SEO (59 caracteres):
Heltec Bluetooth fácil: del móvil a la pantalla (Chisme 25) - Meta descripción (142 caracteres):
Heltec Bluetooth paso a paso: instala Arduino IDE, carga el código y escribe desde el móvil para ver tu texto en la pantalla OLED de la placa. - Permalink / URL:
chismes-del-lince-25-heltec-bluetooth - Texto alternativo de la imagen destacada (en Medios):
Heltec Bluetooth: placa Heltec LoRa 32 mostrando en su pantalla OLED un mensaje enviado desde 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
- Robots: index, follow (por defecto)
Lista de comprobación para que Rank Math dé luz verde
- Descripción SEO y URL: pega la meta descripción y el slug de arriba (ambos llevan «Heltec Bluetooth» seguido, tal cual).
- Título SEO: la palabra clave va al principio, hay un número (25) y una power word («fácil»). Si Rank Math no la reconoce en español, prueba con «gratis», «increíble» o «definitiva».
- Densidad: el texto repite «Heltec Bluetooth» unas 24 veces, para quedar por encima del 0,5 % que pide Rank Math. Si al pegarlo sigue marcando «baja», añade un par de menciones más en el primer y último párrafo.
- Imagen con la palabra clave en el alt: las dos primeras imágenes del texto ya llevan «Heltec Bluetooth» en el alt. Al subirlas, copia ese texto en el campo «Texto alternativo» de WordPress (a veces no lo hereda del editor).
- Enlaces externos: hay cuatro (Arduino IDE, Silicon Labs, documentación de Espressif 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 20 a 24, el curso de micro:bit y la categoría).
- Imágenes: sube las seis con los nombres
25-placa-heltec.jpg,25-gestor-tarjetas.png,25-bibliotecas.png,25-placa-seleccionada.png,25-pantalla-heltec.jpegy25-pagina-letrero.png, y sustituye los marcadoresSUBIR-IMAGEN-…. - Página web: sube el HTML a
https://montesteam.org/app/chisme-25-heltec-texto.html(es la dirección que citan los pasos 8 y 10). - FAQ: en «Preguntas frecuentes», añade el bloque FAQ de Rank Math y pega ahí las cinco preguntas, para que Google pueda mostrarlas como resultado enriquecido.
Código 1: programa de la placa (Arduino IDE)
/*
Chisme 25 · El letrero del Lince (Heltec / TTGO LoRa32 con ESP32)
Recibe texto por Bluetooth (Nordic UART) y lo muestra en la pantalla OLED.
Librerías (Gestor de bibliotecas): Adafruit SSD1306 y Adafruit GFX.
Placa en el Arduino IDE: "ESP32 Dev Module" (aunque tu placa sea una Heltec).
PINES DE LA OLED: dependen de la revisión de la placa.
- TTGO LoRa32 v1.x: SDA 21, SCL 22, sin pin de reset -> OLED_RST -1
- Heltec WiFi LoRa 32 V1/V2: SDA 4, SCL 15, RST 16
Si la pantalla se queda en negro, cambia estas tres líneas.
*/
#include <BLEDevice.h>
#include <BLEServer.h>
#include <BLEUtils.h>
#include <BLE2902.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.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 LED_PIN 25 // LED de la placa (en algunas revisiones es el 2)
#define PRG_PIN 0 // botón PRG
#define SERVICE_UUID "6e400001-b5a3-f393-e0a9-e50e24dcca9e"
#define UUID_RX "6e400002-b5a3-f393-e0a9-e50e24dcca9e" // la web escribe aquí
#define UUID_TX "6e400003-b5a3-f393-e0a9-e50e24dcca9e" // la placa avisa por aquí
Adafruit_SSD1306 display(128, 64, &Wire, OLED_RST);
BLEServer *servidor = nullptr;
BLECharacteristic *tx = nullptr;
volatile bool conectado = false;
bool conectadoAntes = false;
String entrada = ""; // lo que va llegando (trozos de ≤20 bytes)
String pendiente = ""; // línea completa lista para pintar
volatile bool hayLinea = false;
unsigned long ledHasta = 0;
void enviar(const String &m) {
if (conectado && tx) {
String s = m + "\n";
tx->setValue((uint8_t *)s.c_str(), s.length() > 20 ? 20 : s.length());
tx->notify();
}
}
class AlConectar : public BLEServerCallbacks {
void onConnect(BLEServer *s) { conectado = true; }
void onDisconnect(BLEServer *s) {
conectado = false;
BLEDevice::startAdvertising(); // vuelve a ser visible para reconectar
}
};
class AlRecibir : public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *c) {
String v = c->getValue().c_str();
for (size_t i = 0; i < v.length(); i++) {
char ch = v[i];
if (ch == '\n') { pendiente = entrada; entrada = ""; hayLinea = true; }
else if (ch != '\r' && entrada.length() < 120) entrada += ch;
}
}
};
/* Reparte el texto en líneas de "cols" letras sin partir palabras.
Devuelve cuántas líneas ha hecho (maxL+1 si no cabe). */
int envolver(const String &t, int cols, int maxL, String lineas[]) {
int n = 0, i = 0, L = t.length();
String cur = "";
while (i < L) {
int j = t.indexOf(' ', i);
if (j < 0) j = L;
String w = t.substring(i, j);
i = j + 1;
while ((int)w.length() > cols) { // palabra más larga que la línea
if (cur.length()) { if (n >= maxL) return maxL + 1; lineas[n++] = cur; cur = ""; }
if (n >= maxL) return maxL + 1;
lineas[n++] = w.substring(0, cols);
w = w.substring(cols);
}
if (w.length() == 0) continue;
if (cur.length() == 0) cur = w;
else if ((int)(cur.length() + 1 + w.length()) <= cols) cur += " " + w;
else { if (n >= maxL) return maxL + 1; lineas[n++] = cur; cur = w; }
}
if (cur.length()) { if (n >= maxL) return maxL + 1; lineas[n++] = cur; }
return n;
}
void pantallaEspera() {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.println("Heltec-Lince");
display.println();
display.println(conectado ? "Conectado." : "Esperando conexion");
display.println(conectado ? "Escribe en el movil" : "Bluetooth...");
display.display();
}
void mostrar(const String &t) {
if (t.length() == 0) { pantallaEspera(); return; }
String lineas[8];
int tam = 2, n = envolver(t, 10, 4, lineas); // letra grande: 10 x 4
if (n > 4) { tam = 1; n = envolver(t, 21, 8, lineas); if (n > 8) n = 8; }
display.clearDisplay();
display.setTextSize(tam);
int alto = tam == 2 ? 16 : 8;
int y0 = (64 - n * alto) / 2; // centrado en vertical
for (int i = 0; i < n; i++) {
display.setCursor(0, y0 + i * alto);
display.print(lineas[i]);
}
display.display();
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
pinMode(PRG_PIN, INPUT_PULLUP);
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);
pantallaEspera();
BLEDevice::init("Heltec-Lince");
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 AlRecibir());
svc->start();
BLEAdvertising *pub = BLEDevice::getAdvertising();
pub->addServiceUUID(SERVICE_UUID);
pub->setScanResponse(true);
BLEDevice::startAdvertising();
}
void loop() {
if (hayLinea) { // llegó un mensaje: pintarlo fuera del callback
String t = pendiente;
hayLinea = false;
Serial.println("Texto: " + t);
mostrar(t);
digitalWrite(LED_PIN, HIGH); ledHasta = millis() + 150;
enviar("OK:" + t.substring(0, 16)); // confirmación a la web
}
if (ledHasta && millis() > ledHasta) { digitalWrite(LED_PIN, LOW); ledHasta = 0; }
if (conectado != conectadoAntes) { // cambio de estado: refrescar pantalla de espera
conectadoAntes = conectado;
if (conectado) entrada = "";
pantallaEspera();
}
static bool prgAntes = true; // botón PRG: avisa a la web
bool prg = digitalRead(PRG_PIN);
if (!prg && prgAntes) { enviar("PRG"); delay(30); }
prgAntes = prg;
delay(10);
}
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 25 · El letrero del Lince (Heltec)</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.alt{background:transparent;color:var(--pine)}
button:disabled{opacity:.5;cursor:not-allowed}
button:focus-visible,input:focus-visible{outline:3px solid var(--slow);outline-offset:2px}
.fila{display:flex;gap:.5rem;justify-content:center;flex-wrap:wrap}
#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}
.oled{width:min(88vw,21rem);aspect-ratio:2/1;margin:.8rem auto;background:#05100c;border:3px solid var(--pine);
border-radius:.8rem;display:grid;place-items:center;padding:.5rem;overflow:hidden;
color:#7ff0ff;font:700 1.35rem/1.15 ui-monospace,Menlo,Consolas,monospace;word-break:break-word;
text-shadow:0 0 .4rem rgba(127,240,255,.6)}
.oled.vacia{color:#2c5d63;text-shadow:none;font-size:.95rem;font-weight:400}
input[type=text]{width:100%;max-width:21rem;font:inherit;font-size:1.3rem;text-align:center;padding:.7rem;border-radius:.9rem;
border:2px solid var(--line);background:var(--card);color:var(--ink)}
.chips{display:flex;gap:.4rem;justify-content:center;flex-wrap:wrap;margin:.6rem 0}
.chips button{min-height:2.6rem;padding:.4rem .8rem;font-size:.95rem}
.grande{width:100%;max-width:21rem;font-size:1.2rem;min-height:3.4rem;margin-top:.3rem}
.nota{color:var(--soft);font-size:.88rem;margin:.4rem 0;min-height:1.3em}
#eco{color:var(--ok);font-weight:700;min-height:1.4em}
.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 letrero del Lince</h1>
<p class="sub">Escribe en el móvil y míralo aparecer en la pantalla de la Heltec.</p>
<div class="fila"><button id="conectar">Conectar Heltec</button></div>
<div id="estado" role="status">Sin conectar.</div>
<div id="ayuda"></div>
<div class="oled vacia" id="oled" aria-label="Vista previa de la pantalla de la Heltec">Aquí verás el texto</div>
<input type="text" id="txt" maxlength="60" placeholder="HOLA LINCE" autocomplete="off" aria-label="Mensaje">
<p class="nota" id="vista">Hasta 60 letras. La pantalla no sabe escribir tildes ni la ñ: la página las cambia sola.</p>
<div class="chips" aria-label="Mensajes para empezar">
<button class="alt" data-m="HOLA">HOLA</button>
<button class="alt" data-m="Hola Lince">Hola Lince</button>
<button class="alt" data-m="MonteSTEAM">MonteSTEAM</button>
<button class="alt" data-m="Funciona!">Funciona!</button>
</div>
<div class="fila" style="flex-direction:column;align-items:center">
<button id="env" class="grande" disabled>Enviar a la Heltec</button>
<button id="borrar" class="alt grande" disabled>Borrar la pantalla</button>
</div>
<div id="eco" aria-live="polite"></div>
<details class="guia">
<summary>Guía de uso</summary>
<ol>
<li>Carga el programa <b>chisme-25-heltec-texto.ino</b> en la Heltec desde el Arduino IDE. En la pantalla verá «Esperando conexion».</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>Escribe un mensaje y pulsa <b>Enviar</b>. Si es corto sale en letra grande; si es largo, en letra pequeña.</li>
<li>Pulsa el botón <b>PRG</b> de la placa: la página lo notará (así compruebas que el Bluetooth funciona en los dos sentidos).</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));
const vibra = p => { if (navigator.vibrate) navigator.vibrate(p); };
let rx = null, dispositivo = null, conectando = false, reconectando = false, ocupado = false, buffer = "", pasoCx = "", diag = "";
/* La OLED solo escribe ASCII: quitamos tildes y símbolos raros */
const limpiar = s => s.normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^\x20-\x7e]/g, "").slice(0, 60);
function vista(){
const t = limpiar($("txt").value);
$("vista").textContent = t && t !== $("txt").value ? "La Heltec leerá: «" + t + "»" : "Hasta 60 letras. La pantalla no sabe escribir tildes ni la ñ: la página las cambia sola.";
}
$("txt").addEventListener("input", vista);
$("txt").addEventListener("keydown", e => { if (e.key === "Enter" && !$("env").disabled) $("env").click(); });
document.querySelectorAll("[data-m]").forEach(b => b.onclick = () => { $("txt").value = b.dataset.m; vista(); });
function previa(t){
const o = $("oled");
o.textContent = t || "Pantalla vacía"; o.classList.toggle("vacia", !t);
}
/* Envío: una línea terminada en \n, en trozos de 20 bytes */
const propsDe = c => ["read", "write", "writeWithoutResponse", "notify", "indicate"].filter(k => c.properties[k]).join(", ");
async function enviarTrozo(t){
const sr = rx.properties.writeWithoutResponse && typeof rx.writeValueWithoutResponse === "function";
const orden = rx.properties.write
? ["writeValueWithResponse", "writeValue"].concat(sr ? ["writeValueWithoutResponse"] : [])
: ["writeValueWithoutResponse"];
let ult = null;
for (const m of orden){
if (typeof rx[m] !== "function") continue;
try { await rx[m](t); return; } catch (e){ ult = e; ult.metodo = m; await espera(100); }
}
throw ult;
}
async function escribir(txt){
if (!rx) throw new Error("No se encuentra el canal de escritura. Características: " + diag);
const bytes = new TextEncoder().encode(txt + "\n");
for (let pos = 0; pos < bytes.length; pos += 20){
try { await enviarTrozo(bytes.slice(pos, pos + 20)); await espera(30); }
catch (e){
const err = new Error((e && e.message ? e.message : "fallo desconocido") + " [byte " + pos + " de " + bytes.length + ", método " + (e && e.metodo) + "]");
err.name = e && e.name ? e.name : "Error"; throw err;
}
}
}
async function mandar(texto){
if (ocupado) return;
ocupado = true; $("eco").textContent = "";
try {
await escribir(texto); previa(texto); vibra([30, 40, 30]); estado("¡Enviado! Mira la Heltec.");
} catch (e){ estado("No se pudo enviar (" + (e.name || "error") + ").", (e.message || "") + " Comprueba que la Heltec sigue conectada."); }
ocupado = false;
}
$("env").onclick = () => {
const t = limpiar($("txt").value).trim();
if (!t){ estado("Escribe un mensaje primero."); return; }
mandar(t);
};
$("borrar").onclick = () => mandar("");
/* Lo que nos cuenta la Heltec: «OK:texto» al pintar y «PRG» al pulsar el botón */
function linea(t){
if (t === "PRG"){ $("eco").textContent = "¡Has pulsado el botón PRG de la Heltec!"; vibra(60); }
else if (t.startsWith("OK:")){ $("eco").textContent = "La Heltec lo ha recibido ✔"; }
}
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); }
}
/* Conexión Bluetooth (mismo esquema que las páginas 21 a 24) */
function estado(t, ayuda){ if (!/^Error/.test(t)) pasoCx = t; $("estado").textContent = t; $("ayuda").textContent = ayuda || ""; }
function botones(on){ $("env").disabled = !on; $("borrar").disabled = !on; }
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 25.";
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;
rx = chars.find(c => c.uuid.startsWith("6e400002") && (c.properties.write || c.properties.writeWithoutResponse)) || null;
botones(true);
estado("Conectado a " + (dev.name || "Heltec") + ". ¡Escribe algo!");
}
async function reconectar(){
rx = null; botones(false);
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>
Etiqueta:AIDARAC, heltec bluetooh, ies monterroso, montesteam

