
Chismes del Lince 30: El radar de redes del Lince
6 oct 2026 · @José Luis Usero Vílchez
Esa noche Sierra Bermeja estaba tan quieta que el Lince de la Noche podía oír su propio bigote. Sentado en lo alto de una peña, con el móvil en la mano, tenía cara de haber perdido algo.
—No hay nada —murmuró—. Ni un ruido, ni una luz. El campo está vacío.
—Eso crees tú —dijo una voz a su espalda.
Justicia Ciega estaba apoyada en un alcornoque, con las gafas rojas bien puestas y la cabeza ladeada, como quien escucha una conversación al otro lado de una pared. El Lince dio un salto que no estaba en su manual.
—¿Cómo haces eso? —protestó—. Siempre apareces sin hacer ruido.
—Es mi trabajo. Oigo cosas que tú no ves. —Señaló hacia la carretera, a dos kilómetros—. Hay un coche con el móvil conectado. Detrás del cerro, un pastor con un ordenador en la mochila. Y aquí mismo, un lince muy nervioso: cuarenta latidos más de lo normal. Me gusta oírlo todo: los latidos y también las señales de los aparatos.
La Gata Montesa, que había estado a punto de dormirse en una rama, abrió un ojo.
—Qué suerte. Yo solo tengo bigotes.
—Pues yo quiero ese oído —dijo el Lince—. Voy a construir un detector gigante. Con una antena parabólica, un amplificador y…
—¿Y la antena que ya llevas encima? —cortó la Gata—. La Heltec tiene una antena Wi-Fi dentro. Ahora mismo está apagada, pero oye igual que tu amiga: las señales de los routers llegan a cualquier sitio. Solo hay que ponerse a escuchar y contar cuántas hay, por dónde llegan y con qué fuerza.
Justicia Ciega sonrió sin cambiar de postura.
—Te lo pongo fácil. Yo oigo los latidos por la piel. Tú, de las redes, solo sabrás lo que te digan los datos.
—Perfecto —dijo el Lince, ya buscando el cable USB—. Entonces, construyamos un radar.

Introducción
En este chisme construimos un escáner wifi ESP32: una Heltec LoRa 32 que escucha las redes Wi-Fi de alrededor, sin conectarse a ninguna, y dibuja en su pantalla OLED cuántas hay, en qué canal están y con qué potencia llegan. Después se lo cuenta al móvil por Bluetooth, que lo muestra en una gráfica y deja comparar el aula, el patio y el pasillo.
No necesitamos ningún sensor ni ningún cable: la antena Wi-Fi ya está dentro del chip. Lo que aprendemos es a escucharla, a interpretar lo que oye y a decidir qué datos merece la pena guardar y cuáles es mejor no tocar.
Por qué este chisme
El aire de un instituto está lleno de señales que no vemos: el Wi-Fi del centro, el de los móviles que hacen de punto de acceso, el de los vecinos de la calle de al lado. Todas comparten una banda muy estrecha, y cuando hay muchas, se estorban. Es una de las razones por las que el Wi-Fi «va fatal» en algunos sitios y bien en otros, aunque esté el mismo router.
Este chisme convierte esa idea en algo que se puede medir. Y de paso plantea una pregunta que importa a cualquier proyecto con datos: ¿qué información necesitamos de verdad? Para saber si un canal está saturado nos basta el canal y la potencia. No hace falta saber cómo se llama la red de cada vecino, así que la placa ni siquiera lo lee. Es privacidad desde el diseño, y la tecnología lo permite.
También encaja con AIDARAC: sensor → dato → conjunto de datos → análisis → conclusión. Aquí el sensor es la propia antena.
Cómo funciona un escáner wifi ESP32
La pregunta de la Gata era: ¿cómo se enteran los móviles de que hay una red cerca sin haberse conectado antes? La respuesta son las balizas.
Un router Wi-Fi emite varias veces por segundo (unas diez) un mensaje corto que dice «estoy aquí, esta es mi red, uso este canal». Es como un faro. Cualquiera que escuche en ese canal lo recibe, sin pedir permiso y sin conectarse. Un escáner wifi ESP32 hace exactamente eso: sintoniza un canal, escucha unas décimas de segundo, anota las balizas que oye, y pasa al siguiente.
Hay dos formas de escanear. En el escaneo activo la placa grita «¿hay alguien?» y espera respuestas. En el pasivo solo escucha. El nuestro es pasivo: no transmite nada, y por eso decimos que escucha sin molestar.
Las redes se reparten en canales, y cada canal es una frecuencia. En la banda de 2,4 GHz hay 13 canales en Europa, separados solo 5 MHz entre sí, pero cada red ocupa unos 20 MHz. Resultado: los canales vecinos se pisan.
| Canal | Frecuencia central (MHz) | Se pisa con |
|---|---|---|
| 1 | 2412 | 2, 3, 4 y 5 |
| 6 | 2437 | de 2 a 10 |
| 11 | 2462 | de 7 a 13 |
| 13 | 2472 | de 9 a 12 |
Solo los canales 1, 6 y 11 están tan separados que no se molestan entre sí. Por eso los routers suelen configurarse en uno de ellos. Si hay muchas redes en el mismo canal (o en canales que se pisan), tienen que turnarse para hablar y todas van más lentas.
La potencia con la que llega cada baliza se llama RSSI (indicador de intensidad de señal recibida) y se mide en dBm. Siempre sale un número negativo, y cuanto más cerca de cero, mejor se oye. La escala es logarítmica: cada 10 dBm menos, la potencia que llega es diez veces menor. Con −50 dBm la señal es mil veces más fuerte que con −80 dBm.
Qué necesitamos
| Elemento | Para qué sirve |
|---|---|
| Placa Heltec LoRa 32 (o TTGO LoRa32) con ESP32 clásico y pantalla OLED | Escucha las redes, dibuja y envía por Bluetooth |
| Cable USB | Cargar el programa y dar corriente |
| Arduino IDE | Programar la placa |
| Librerías Adafruit SSD1306 y Adafruit GFX | Dibujan en la pantalla OLED (las del chisme 25) |
| Móvil Android con Chrome | Abre la página y recibe los datos |
No hace falta ningún sensor, ningún cable ni ninguna soldadura.
Construcción
Es el montaje más sencillo de la serie, porque no hay nada que montar. Lo único delicado es un ajuste del Arduino IDE, porque Wi-Fi y Bluetooth juntos ocupan mucho espacio.
- Conecta la placa al ordenador con el cable USB.
- En el Arduino IDE, elige la placa ESP32 Dev Module.
- Abre Herramientas → Partition Scheme y elige Huge APP (3MB No OTA/1MB SPIFFS).
- Carga el programa del apartado siguiente.
- La pantalla dirá «Escuchando redes…» y, a los pocos segundos, mostrará una barra por cada canal.
Si al compilar aparece Sketch too big o text section exceeds available space in board, es que falta el paso 3: con el esquema normal, la parte del programa no cabe. No es un fallo del código.
Si la pantalla no se enciende, revisa los pines de la OLED (OLED_SDA, OLED_SCL y OLED_RST) al principio del programa. En la placa que hemos usado en este chisme, la pantalla responde en los pines 21 y 22, pero según la documentación de la Heltec WiFi LoRa 32 V2, en esa placa está en los pines 4 y 15. Si tienes dudas, un escáner I2C te dice dónde está.


El código de la placa
Guarda este programa como chisme-30-radar-redes-lince.ino y cárgalo con el Arduino IDE (placa «ESP32 Dev Module», con el esquema de particiones «Huge APP» que vimos en la construcción). Necesitas las mismas librerías que en el Chisme 25: Adafruit SSD1306 y Adafruit GFX. Si tu pantalla usa otros pines, solo tienes que cambiar los tres #define del principio.
/*
Chisme 30 · El radar de redes del Lince (Heltec / TTGO LoRa32 con ESP32)
El ESP32 ESCUCHA las redes Wi-Fi que ya emiten sus balizas (sin conectarse
a ninguna y sin transmitir nada) y cuenta cuantas hay en cada canal de la
banda de 2,4 GHz y con que potencia (RSSI, en dBm) llegan hasta aqui.
Lo dibuja en la pantalla OLED y se lo cuenta al movil por Bluetooth.
PRIVACIDAD: el programa NUNCA lee los nombres de las redes (SSID) ni envia
direcciones MAC. Para que "Red 3" siga siendo la misma red entre escaneos,
guarda en la RAM una huella revuelta con un numero aleatorio que cambia en
cada arranque; no se envia a ninguna parte y se borra al reiniciar la placa.
Las redes se numeran en orden de potencia la primera vez que se oyen.
NO HACE FALTA CABLEAR NADA ni usar sensores.
Librerias: Adafruit SSD1306 y Adafruit GFX (las del Chisme 25).
Placa en el Arduino IDE: "ESP32 Dev Module".
Si al compilar dice "Sketch too big" (Wi-Fi + Bluetooth ocupan mucho):
Herramientas -> Partition Scheme -> "Huge APP (3MB No OTA/1MB SPIFFS)".
Mensajes por Bluetooth (una linea cada uno, de 20 caracteres como maximo),
despues de cada escaneo:
T:14,-48 total de redes y potencia de la mas fuerte (dBm)
R:3,11,-61 "Red 3": canal 11, -61 dBm (una linea por red, las 20 mas fuertes)
L:11,2 canal mas tranquilo entre 1, 6 y 11, y cuantas redes le molestan
E fin del escaneo
*/
#include <WiFi.h>
#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 N_CANALES 13 // canales 1 a 13 (los de Europa)
#define MS_POR_CANAL 300 // cuanto escucha en cada canal (las balizas salen cada ~100 ms)
#define MAX_REDES 40 // cuantas redes distintas sabe numerar
#define MAX_ENVIO 20 // cuantas redes manda al movil en cada escaneo
#define SERVICE_UUID "6e400001-b5a3-f393-e0a9-e50e24dcca9e"
#define UUID_TX "6e400003-b5a3-f393-e0a9-e50e24dcca9e" // la placa envia por aqui
Adafruit_SSD1306 display(128, 64, &Wire, OLED_RST);
BLECharacteristic *tx = nullptr;
volatile bool conectado = false;
int cuenta[N_CANALES + 1]; // redes en cada canal (indices 1 a 13)
int mejorRssi[N_CANALES + 1]; // la potencia mas fuerte en cada canal
int total = 0; // redes en total
int mejorGlobal = -127; // la potencia mas fuerte de todas
int canalLibre = 1, molestias = 0;
struct Red { int canal; int rssi; int num; uint32_t huella; };
Red lista[MAX_REDES]; // redes oidas en este escaneo, de mas fuerte a mas debil
int nLista = 0;
uint32_t huellas[MAX_REDES]; // las redes que ya conocemos (su numero es su posicion + 1)
int nHuellas = 0;
uint32_t sal; // numero aleatorio de este arranque
/* ---------- Bluetooth ---------- */
class AlConectar : public BLEServerCallbacks {
void onConnect(BLEServer *s) { conectado = true; }
void onDisconnect(BLEServer *s) {
conectado = false;
BLEDevice::startAdvertising(); // vuelve a ser visible para reconectar
}
};
void enviar(const String &m) {
if (conectado && tx) {
String s = m + "\n";
tx->setValue((uint8_t *)s.c_str(), s.length());
tx->notify();
delay(30); // da tiempo a que el movil reciba cada linea
}
}
/* ---------- Numerar las redes sin guardar nada identificable ---------- */
uint32_t huellaDe(const uint8_t *mac) { // mezcla (FNV-1a) de la MAC con el numero aleatorio
uint32_t h = sal ^ 2166136261UL;
for (int i = 0; i < 6; i++) { h ^= mac[i]; h *= 16777619UL; }
return h;
}
int numeroDeRed(uint32_t h) { // 1, 2, 3... en el orden en que las vamos conociendo
for (int i = 0; i < nHuellas; i++) if (huellas[i] == h) return i + 1;
if (nHuellas < MAX_REDES) { huellas[nHuellas++] = h; return nHuellas; }
return 0; // ya no caben mas
}
/* ---------- Escaneo ---------- */
void escanear() {
// true = incluir redes ocultas, true = escaneo PASIVO (solo escucha, no pregunta a nadie)
int n = WiFi.scanNetworks(false, true, true, MS_POR_CANAL);
for (int c = 0; c <= N_CANALES; c++) { cuenta[c] = 0; mejorRssi[c] = -127; }
total = 0;
mejorGlobal = -127;
nLista = 0;
for (int i = 0; i < n; i++) { // OJO: nunca se llama a WiFi.SSID(): no leemos los nombres
int canal = WiFi.channel(i);
int rssi = WiFi.RSSI(i);
if (canal < 1 || canal > N_CANALES) continue;
cuenta[canal]++;
total++;
if (rssi > mejorRssi[canal]) mejorRssi[canal] = rssi;
if (rssi > mejorGlobal) mejorGlobal = rssi;
if (nLista < MAX_REDES) {
lista[nLista] = { canal, rssi, 0, huellaDe(WiFi.BSSID(i)) };
nLista++;
}
}
WiFi.scanDelete(); // libera la memoria del escaneo
for (int i = 1; i < nLista; i++) { // ordenar de mas fuerte a mas debil (insercion)
Red x = lista[i];
int j = i - 1;
while (j >= 0 && lista[j].rssi < x.rssi) { lista[j + 1] = lista[j]; j--; }
lista[j + 1] = x;
}
for (int i = 0; i < nLista; i++) lista[i].num = numeroDeRed(lista[i].huella);
// Canal mas tranquilo entre 1, 6 y 11 (los unicos que no se pisan entre si).
// Una red en el canal c molesta a los canales de c-2 a c+2.
int candidatos[3] = {1, 6, 11};
molestias = 9999;
for (int k = 0; k < 3; k++) {
int ch = candidatos[k], puntos = 0;
for (int c = ch - 2; c <= ch + 2; c++)
if (c >= 1 && c <= N_CANALES) puntos += cuenta[c];
if (puntos < molestias) { molestias = puntos; canalLibre = ch; }
}
Serial.printf("Redes: %d mas fuerte: %d dBm canal tranquilo: %d (%d)\n",
total, total ? mejorGlobal : 0, canalLibre, molestias);
for (int i = 0; i < nLista; i++)
Serial.printf(" Red %2d: canal %2d, %d dBm\n", lista[i].num, lista[i].canal, lista[i].rssi);
}
/* ---------- Pantalla ---------- */
void mensaje(const char *l1, const char *l2) {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0); display.print(l1);
display.setCursor(0, 12); display.print(l2);
display.setCursor(0, 54); display.print(conectado ? "Bluetooth: conectado" : "Bluetooth: esperando");
display.display();
}
void dibujar() {
display.clearDisplay();
display.setTextSize(1);
display.setCursor(0, 0);
display.printf("Redes:%d Mejor:%ddBm", total, total ? mejorGlobal : 0);
display.setCursor(0, 9);
display.printf("Libre: canal %d (%d)", canalLibre, molestias);
for (int c = 1; c <= N_CANALES; c++) { // una barra por canal: su altura es la potencia
int x = 2 + (c - 1) * 9;
if (cuenta[c] > 0) {
int h = map(constrain(mejorRssi[c], -95, -35), -95, -35, 3, 32);
display.fillRect(x, 52 - h, 7, h, SSD1306_WHITE);
} else {
display.drawFastHLine(x, 52, 7, SSD1306_WHITE);
}
}
display.drawFastHLine(0, 53, 128, SSD1306_WHITE);
const int marcas[3] = {1, 6, 11};
for (int k = 0; k < 3; k++) { // numeros bajo los canales 1, 6 y 11
display.setCursor(2 + (marcas[k] - 1) * 9, 56);
display.print(marcas[k]);
}
display.display();
}
/* ---------- Envio al movil ---------- */
void contarAlMovil() {
enviar("T:" + String(total) + "," + String(total ? mejorGlobal : 0));
for (int i = 0; i < nLista && i < MAX_ENVIO; i++)
if (lista[i].num > 0)
enviar("R:" + String(lista[i].num) + "," + String(lista[i].canal) + "," + String(lista[i].rssi));
enviar("L:" + String(canalLibre) + "," + String(molestias));
enviar("E");
}
/* ---------- Arranque ---------- */
void setup() {
Serial.begin(115200);
delay(500);
Serial.println();
Serial.println("Chisme 30 - El radar de redes del Lince");
sal = esp_random(); // cambia en cada arranque: las huellas no se pueden comparar entre placas
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);
mensaje("Radar de redes", "Arrancando...");
BLEDevice::init("Heltec-Radar");
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();
WiFi.mode(WIFI_STA); // modo estacion, pero NO nos conectamos a nada
WiFi.disconnect();
delay(100);
}
bool primeraVez = true;
void loop() {
if (primeraVez) {
mensaje("Escuchando redes...", "(unos 4 segundos)");
primeraVez = false;
} else {
// La grafica anterior se queda en la pantalla mientras escucha.
// Solo se enciende un circulito abajo a la derecha: "estoy escuchando".
display.fillCircle(121, 58, 4, SSD1306_WHITE);
display.display();
}
escanear(); // ~4 s (la pantalla sigue mostrando la grafica anterior)
dibujar(); // nueva grafica: sustituye a la anterior de golpe
contarAlMovil();
}
Qué hace cada parte
El escaneo pasivo. La línea clave es WiFi.scanNetworks(false, true, true, 300). Los dos true significan «incluye las redes ocultas» y «escanea en modo pasivo»: la placa se queda quieta en cada canal 300 milisegundos y solo escucha las balizas que los routers ya emiten solos. No pregunta nada a nadie. Con 13 canales, un escaneo completo tarda unos 4 segundos.
La privacidad, dentro del código. Fíjate en lo que no aparece: nunca se llama a WiFi.SSID(), así que la placa ni siquiera conoce los nombres de las redes. Para poder decir «esta es la misma Red 3 de hace un momento» sin guardar la dirección MAC, la función huellaDe() la mezcla con un número aleatorio (sal) que se genera en cada arranque. Esa huella vive solo en la memoria RAM de la placa, no sale por Bluetooth, y no se puede comparar con la de otra placa ni con la de otro día.
La gráfica que no desaparece. En loop(), mientras la placa escucha, la gráfica anterior se queda en pantalla y solo se enciende un circulito abajo a la derecha («estoy escuchando»). Cuando termina el escaneo, la nueva gráfica sustituye a la vieja de golpe. Antes borrábamos la pantalla en cada escaneo y la gráfica solo se veía una fracción de segundo; este pequeño cambio lo arregló.
La página web
La placa ya dibuja su gráfica en la OLED, pero la pantalla es diminuta: no cabe una lista de redes ni se pueden comparar lugares. Por eso el móvil o el ordenador harán de «pantalla grande»: una página que recibe los datos por Bluetooth, dibuja el espectro de 2,4 GHz, lista las redes y permite guardar medidas en distintos sitios para compararlas. Guarda este archivo como chisme-30-radar-redes-lince.html y ábrelo con Chrome (desde una dirección https://, como en los chismes anteriores).
<!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 30 · El radar de redes 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:34rem;margin:0 auto;padding:1rem 1rem 2rem;text-align:center}
h1{font-size:1.6rem;margin:.2rem 0 .1rem}
h2{font-size:1.05rem;margin:1.3rem 0 .4rem;text-align:left}
.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.sec{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}
#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}
#redes{font-size:3.2rem;font-weight:700;line-height:1.1;margin:.8rem 0 0;color:var(--pine);font-variant-numeric:tabular-nums}
#redes small{font-size:1.2rem;color:var(--soft);font-weight:400}
#fuerte{color:var(--soft);min-height:1.5em}
svg{width:100%;height:auto;display:block;margin:.6rem 0 0;background:var(--card);border:1px solid var(--line);border-radius:.8rem}
.eje{stroke:var(--line);stroke-width:1}
.txt{fill:var(--soft);font:11px system-ui,sans-serif}
.txt.dest{fill:var(--ink);font-weight:700}
.num{fill:var(--ink);font:bold 11px system-ui,sans-serif;text-anchor:middle}
#libre{margin-top:.6rem;background:var(--card);border:1px solid var(--line);border-radius:.8rem;padding:.6rem .9rem;text-align:left;font-size:.95rem;min-height:2.6rem}
.fila{display:flex;gap:.5rem;margin-top:.4rem}
input{flex:1;min-width:0;font:inherit;border:2px solid var(--line);background:var(--card);color:var(--ink);border-radius:.7rem;padding:.55rem .7rem}
table{width:100%;border-collapse:collapse;margin-top:.6rem;font-size:.9rem;font-variant-numeric:tabular-nums}
th,td{border-bottom:1px solid var(--line);padding:.35rem .3rem;text-align:left}
th{color:var(--soft);font-weight:700;font-size:.8rem}
.punto{display:inline-block;width:.8rem;height:.8rem;border-radius:50%;margin-right:.35rem;vertical-align:-.05rem}
.mejor{font-weight:700;color:var(--ok)}
.tabla-scroll{overflow-x:auto}
.priv{font-size:.82rem;color:var(--soft);margin-top:1rem;text-align:left}
.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 radar de redes del Lince 📡</h1>
<p class="sub">La placa escucha las redes Wi-Fi de alrededor, sin conectarse a ninguna.</p>
<button id="conectar">Conectar Heltec</button>
<div id="estado" role="status">Sin conectar.</div>
<div id="ayuda"></div>
<div id="redes" aria-live="polite">— <small>redes</small></div>
<div id="fuerte"></div>
<div id="actualiz" class="priv" style="text-align:center;margin-top:.2rem" aria-live="polite"></div>
<svg id="espectro" viewBox="0 0 580 230" role="img" aria-label="Redes Wi-Fi por canal en la banda de 2,4 GHz"></svg>
<div id="libre">Cuando la placa termine su primer escaneo (unos 4 segundos) aparecerá aquí el canal más tranquilo.</div>
<h2>Redes que se oyen ahora</h2>
<div class="tabla-scroll">
<table id="ahora" aria-label="Redes detectadas">
<thead><tr><th>Red</th><th>Canal</th><th>Potencia (dBm)</th></tr></thead>
<tbody></tbody>
</table>
</div>
<div id="nota-lista" class="priv"></div>
<h2>Comparar lugares</h2>
<div class="fila">
<input id="lugar" type="text" placeholder="Lugar (aula, patio, pasillo…)" maxlength="30" aria-label="Nombre del lugar">
<button id="guardar" class="sec" disabled>Guardar medida</button>
</div>
<div class="tabla-scroll">
<table id="tabla" aria-label="Medidas guardadas">
<thead><tr><th>Lugar</th><th>Redes</th><th>Más fuerte (dBm)</th><th>Canal tranq.</th></tr></thead>
<tbody></tbody>
</table>
</div>
<h2>Cada red en cada lugar (dBm)</h2>
<div class="tabla-scroll">
<table id="matriz" aria-label="Potencia de cada red en cada lugar">
<thead></thead>
<tbody></tbody>
</table>
</div>
<div id="nota-matriz" class="priv">Cuanto más cerca de 0, mejor señal (−50 es mucho mejor que −80). En negrita verde, el lugar donde cada red se oye mejor. «—» significa que en ese lugar no se oyó.</div>
<div class="fila"><button id="csv" class="sec" disabled>Descargar CSV</button></div>
<p class="priv"><b>Privacidad:</b> las redes se llaman «Red 1», «Red 2»… en el orden en que la placa las va conociendo (la más fuerte la primera vez es la Red 1). La placa no lee los nombres de las redes y no envía direcciones MAC: solo canal y potencia. Para reconocer una red entre escaneos usa una huella revuelta con un número aleatorio, solo en su memoria, que se borra al reiniciarla. Las medidas viven en esta página: si la cierras, se borran.</p>
<details class="guia">
<summary>Guía de uso</summary>
<ol>
<li>No hace falta cablear nada: la placa usa su propia antena Wi-Fi.</li>
<li>Carga <b>chisme-30-radar-redes-lince.ino</b> con el Arduino IDE. En la pantalla verás «Escuchando redes…» y, a los pocos segundos, las barras por canal.</li>
<li>Abre esta página con <b>Chrome</b> y pulsa <b>Conectar Heltec</b>. Elige <b>Heltec-Radar</b>.</li>
<li>Escribe el lugar donde estás (por ejemplo «aula») y pulsa <b>Guardar medida</b>. Repite en el patio y en el pasillo <b>sin reiniciar la placa</b> (así «Red 3» sigue siendo la misma red) y compara las tablas.</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 N_CANALES = 13;
const $ = id => document.getElementById(id);
const espera = ms => new Promise(r => setTimeout(r, ms));
let dispositivo = null, conectando = false, reconectando = false, buffer = "", pasoCx = "", diag = "";
/* Escaneo que se está recibiendo y último escaneo completo */
let nuevo = null, actual = null, medidas = [];
/* ---------- Colores: cada red tiene el suyo, según su número ---------- */
const color = n => "hsl(" + ((n * 47) % 360) + " 60% 48%)";
/* ---------- Dibujo del espectro de 2,4 GHz ----------
Cada red es una "montaña" de 22 MHz de ancho. Como los canales están
separados solo 5 MHz, las montañas se pisan: eso es lo que dibujamos. */
const X = c => (c + 1.5) * 34; // posición horizontal del canal c
const Y = rssi => 190 - (Math.max(-100, Math.min(-30, rssi)) + 100) / 70 * 160; // potencia → altura
function dibujar(){
let s = "";
[-90, -70, -50].forEach(p => {
s += '<line class="eje" x1="30" x2="570" y1="' + Y(p) + '" y2="' + Y(p) + '"/>';
s += '<text class="txt" x="4" y="' + (Y(p) + 4) + '">' + p + '</text>';
});
s += '<line class="eje" x1="30" x2="570" y1="190" y2="190"/>';
for (let c = 1; c <= N_CANALES; c++){
const dest = (c === 1 || c === 6 || c === 11);
s += '<text class="txt' + (dest ? ' dest' : '') + '" x="' + X(c) + '" y="208" text-anchor="middle">' + c + '</text>';
}
s += '<text class="txt" x="290" y="224" text-anchor="middle">Canal (banda de 2,4 GHz) · potencia en dBm</text>';
if (actual){
// de más débil a más fuerte, para que las fuertes queden delante
[...actual.redes].sort((a, b) => a.rssi - b.rssi).forEach(r => {
const x = X(r.canal), y = Y(r.rssi), col = color(r.num);
s += '<polygon points="' + (x - 75) + ',190 ' + (x - 53) + ',' + y + ' ' + (x + 53) + ',' + y + ' ' + (x + 75) + ',190" style="fill:' + col + ';fill-opacity:.25;stroke:' + col + ';stroke-width:1.5"/>';
s += '<text class="num" x="' + x + '" y="' + (y - 4) + '" style="fill:' + col + '">' + r.num + '</text>';
});
}
$("espectro").innerHTML = s;
}
function etiquetaPotencia(p){
if (p >= -50) return "muy fuerte (casi junto al router)";
if (p >= -60) return "buena";
if (p >= -70) return "aceptable";
if (p >= -80) return "débil";
return "muy débil";
}
function celda(tr, texto, cls){
const td = document.createElement("td");
if (cls) td.className = cls;
td.textContent = texto;
tr.appendChild(td);
return td;
}
function pintarAhora(){
const tb = $("ahora").querySelector("tbody");
tb.innerHTML = "";
actual.redes.forEach(r => {
const tr = document.createElement("tr");
const td = document.createElement("td");
const punto = document.createElement("span");
punto.className = "punto"; punto.style.background = color(r.num);
td.appendChild(punto); td.appendChild(document.createTextNode("Red " + r.num));
tr.appendChild(td);
celda(tr, r.canal); celda(tr, r.rssi);
tb.appendChild(tr);
});
$("nota-lista").textContent = actual.total > actual.redes.length
? "Se muestran las " + actual.redes.length + " redes más fuertes de " + actual.total + "."
: "";
}
/* ---------- Lo que manda la placa ----------
T:14,-48 total de redes y potencia de la más fuerte
R:3,11,-61 «Red 3»: canal 11, -61 dBm (una línea por red, de más fuerte a más débil)
L:11,2 canal más tranquilo (entre 1, 6 y 11) y redes que le molestan
E fin del escaneo */
function terminar(){
actual = nuevo; nuevo = null;
$("redes").innerHTML = actual.total + ' <small>' + (actual.total === 1 ? 'red' : 'redes') + '</small>';
$("fuerte").textContent = actual.total ? "La más fuerte llega a " + actual.fuerte + " dBm: " + etiquetaPotencia(actual.fuerte) + "." : "No se oye ninguna red.";
$("libre").textContent = "Canal más tranquilo: " + actual.libre + " (le " + (actual.molestias === 1 ? "molesta 1 red" : "molestan " + actual.molestias + " redes") + "). Solo los canales 1, 6 y 11 no se pisan entre sí.";
$("guardar").disabled = false;
dibujar();
pintarAhora();
$("actualiz").textContent = "Escaneo de las " + new Date().toLocaleTimeString("es-ES") + ". Se mantiene hasta el siguiente (unos 5 s).";
}
function linea(t){
let m;
if ((m = /^T:(\d+),(-?\d+)$/.exec(t))) { nuevo = { total: +m[1], fuerte: +m[2], redes: [], libre: 0, molestias: 0 }; $("actualiz").textContent = "Recibiendo datos nuevos… (mientras tanto se ve el último escaneo)"; return; }
if (!nuevo) return;
if ((m = /^R:(\d+),(\d+),(-?\d+)$/.exec(t))) { nuevo.redes.push({ num: +m[1], canal: +m[2], rssi: +m[3] }); return; }
if ((m = /^L:(\d+),(\d+)$/.exec(t))) { nuevo.libre = +m[1]; nuevo.molestias = +m[2]; return; }
if (t === "E") terminar();
}
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); }
}
/* ---------- Comparar lugares ---------- */
function pintarTabla(){
const tb = $("tabla").querySelector("tbody");
tb.innerHTML = "";
medidas.forEach(m => {
const tr = document.createElement("tr");
celda(tr, m.lugar); celda(tr, m.total); celda(tr, m.total ? m.fuerte : "—"); celda(tr, m.libre);
tb.appendChild(tr);
});
// Matriz: una fila por red y una columna por lugar
const cab = $("matriz").querySelector("thead"), cu = $("matriz").querySelector("tbody");
cab.innerHTML = ""; cu.innerHTML = "";
if (medidas.length){
const h = document.createElement("tr");
const th0 = document.createElement("th"); th0.textContent = "Red"; h.appendChild(th0);
medidas.forEach(m => { const th = document.createElement("th"); th.textContent = m.lugar; h.appendChild(th); });
cab.appendChild(h);
const nums = [...new Set(medidas.flatMap(m => m.redes.map(r => r.num)))].sort((a, b) => a - b);
nums.forEach(n => {
const valores = medidas.map(m => { const r = m.redes.find(x => x.num === n); return r ? r.rssi : null; });
const mejor = Math.max(...valores.filter(v => v !== null));
const tr = document.createElement("tr");
const td = document.createElement("td");
const punto = document.createElement("span");
punto.className = "punto"; punto.style.background = color(n);
td.appendChild(punto); td.appendChild(document.createTextNode("Red " + n));
tr.appendChild(td);
valores.forEach(v => celda(tr, v === null ? "—" : v, v !== null && v === mejor && valores.filter(x => x !== null).length > 1 ? "mejor" : ""));
cu.appendChild(tr);
});
}
$("csv").disabled = !medidas.length;
}
$("guardar").onclick = () => {
if (!actual) return;
const lugar = $("lugar").value.trim().slice(0, 30) || ("Lugar " + (medidas.length + 1));
medidas.push({ lugar, total: actual.total, fuerte: actual.fuerte, libre: actual.libre,
redes: actual.redes.map(r => ({ num: r.num, canal: r.canal, rssi: r.rssi })) });
$("lugar").value = "";
pintarTabla();
};
$("csv").onclick = () => {
const esc = v => '"' + String(v).replace(/"/g, '""') + '"';
const filas = ["lugar,red,canal,rssi_dbm"];
medidas.forEach(m => m.redes.forEach(r => filas.push([esc(m.lugar), "Red " + r.num, r.canal, r.rssi].join(","))));
const a = document.createElement("a");
a.href = URL.createObjectURL(new Blob([filas.join("\n")], { type: "text/csv" }));
a.download = "radar-redes.csv";
a.click();
setTimeout(() => URL.revokeObjectURL(a.href), 1000);
};
/* ---------- Conexión Bluetooth (mismo esquema que las páginas 21 a 29) ---------- */
const propsDe = c => ["read", "write", "writeWithoutResponse", "notify", "indicate"].filter(k => c.properties[k]).join(", ");
function estado(t, ayuda){ if (!/^Error/.test(t)) pasoCx = t; $("estado").textContent = t; $("ayuda").textContent = ayuda || ""; }
function explicar(e){
const nombre = e && e.name ? e.name : "Error", m = e && e.message ? e.message : String(e);
let a = "";
if (nombre === "NotFoundError" && /cancel/i.test(m)) a = "No has elegido ningún dispositivo. Pulsa Conectar y elige Heltec-Radar.";
else if (nombre === "NotFoundError") a = "La placa no tiene el servicio UART. Comprueba que has cargado el programa del Chisme 30.";
else if (nombre === "NotSupportedError") a = "No se pudo activar el envío de datos. Olvida la Heltec en Ajustes → Bluetooth y reinicia el móvil y la placa.";
else if (nombre === "NetworkError") a = "No se pudo abrir la conexión. Comprueba que no haya otra app conectada y reinicia la placa (botón RST).";
else if (nombre === "SecurityError") a = "La página debe abrirse con https://.";
else if (nombre === "NotAllowedError") a = "Permiso denegado. Permite el Bluetooth y los dispositivos cercanos para el navegador.";
estado("Error (" + nombre + "): " + m, "Falló en el paso: «" + pasoCx + "». " + a + (diag ? " Características: " + diag : ""));
}
async function conectarGatt(dev){
let ultimoError;
for (let k = 1; k <= 4; k++){
try {
estado("Conectando… (intento " + k + " de 4)");
const srv = dev.gatt.connected ? dev.gatt : await dev.gatt.connect();
await espera(600);
estado("Buscando el servicio UART…");
return await srv.getPrimaryService(UART_SERVICE);
} catch (e){
ultimoError = e;
if (e.name === "NotFoundError" || e.name === "SecurityError") break;
try { if (dev.gatt.connected) dev.gatt.disconnect(); } catch (_){}
await espera(800);
}
}
throw ultimoError;
}
async function enlazar(dev){
const svc = await conectarGatt(dev);
buffer = ""; diag = "";
estado("Leyendo las características del servicio…");
const chars = await svc.getCharacteristics();
diag = chars.map(c => c.uuid.slice(0, 8) + " [" + propsDe(c) + "]").join(" · ");
const cand = chars.filter(c => c.properties.notify || c.properties.indicate)
.sort((a, b) => (b.uuid === UART_TX) - (a.uuid === UART_TX));
if (!cand.length) throw new Error("El servicio UART no tiene ninguna característica que envíe datos.");
let ch = null, ultimoErr;
for (const c of cand){
c.addEventListener("characteristicvaluechanged", recibir);
for (let i = 1; i <= 2 && !ch; i++){
try { estado("Activando notificaciones en " + c.uuid.slice(0, 8) + "… (intento " + i + ")"); await c.startNotifications(); ch = c; }
catch (e){ ultimoErr = e; await espera(1000); }
}
if (ch) break;
c.removeEventListener("characteristicvaluechanged", recibir);
}
if (!ch) throw ultimoErr;
estado("Conectado a " + (dev.name || "Heltec") + ". Esperando el primer escaneo (unos 4 s)…");
}
async function reconectar(){
if (reconectando) return;
reconectando = true;
for (let i = 1; i <= 20; i++){
estado("Conexión perdida. Reintentando… (" + i + " de 20)", "Mantén la Heltec cerca y encendida.");
await espera(3000);
try { await enlazar(dispositivo); reconectando = false; return; } catch (_) {}
}
reconectando = false;
estado("No se pudo reconectar.", "Pulsa Conectar para volver a empezar.");
}
$("conectar").onclick = async () => {
if (conectando || reconectando) return;
if (!navigator.bluetooth){ estado("Este navegador no permite Bluetooth.", "Usa Chrome o Edge (en ordenador o en Android)."); return; }
conectando = true; $("conectar").disabled = true;
try {
estado("Buscando la Heltec…");
const dev = await navigator.bluetooth.requestDevice({ acceptAllDevices: true, optionalServices: [UART_SERVICE] });
dispositivo = dev;
dev.addEventListener("gattserverdisconnected", reconectar);
await enlazar(dev);
} catch (e){ explicar(e); }
finally { conectando = false; $("conectar").disabled = false; }
};
dibujar();
</script>
</body>
</html>
Qué hace cada parte
El protocolo. La placa y la página se hablan con líneas de texto muy cortas (de 20 caracteres como máximo, porque es lo que cabe en un paquete Bluetooth): T:14,-48 (hay 14 redes y la más fuerte llega a −48 dBm), R:3,11,-61 (la Red 3 está en el canal 11 con −61 dBm), L:11,2 (el canal más tranquilo es el 11 y le molestan 2 redes) y E (fin del escaneo). La función linea() de la página va montando un escaneo con esas piezas y, cuando llega la E, lo dibuja entero. Así la gráfica anterior se queda en pantalla mientras llegan los datos nuevos.
El espectro. Cada red se dibuja como una «montaña» (un polígono SVG) centrada en su canal y de unos 22 MHz de ancho. Como los canales están separados solo 5 MHz, las montañas de canales vecinos se solapan, y eso es justo lo que queremos ver. La altura de cada una es su potencia: cuanto más alta, más fuerte se oye. Cada red tiene su propio color según su número, así que puedes seguir la Red 3 de una gráfica a otra.
Comparar lugares. El botón «Guardar medida» copia el último escaneo con el nombre del lugar que hayas escrito. La tabla «Cada red en cada lugar» pone una fila por red y una columna por lugar, y resalta en verde el sitio donde cada red se oye mejor. El botón «Descargar CSV» exporta las medidas (lugar,red,canal,rssi_dbm) para analizarlas después en una hoja de cálculo. Todo vive en la memoria de la página: si la cierras, no queda nada guardado.
Cómo probarlo
- Carga el programa en la placa. En la pantalla verás «Escuchando redes…» y, a los 4 segundos, una fila de barras: una por canal, más altas cuanto más fuerte se oye la mejor red de ese canal. Arriba, el número de redes y la potencia de la más fuerte; debajo, el canal más tranquilo. Si abres el Monitor Serie a 115200 baudios verás la lista completa.
- Abre la página con Chrome, pulsa Conectar Heltec y elige Heltec-Radar. A los pocos segundos aparecerán el número de redes, el espectro con sus montañas de colores y la tabla.
- Prueba de la pared. Con la placa en la mano, acércate al router más cercano y fíjate en su potencia. Después aléjate, sal al pasillo y cierra una puerta de por medio. Debes ver que su número baja (por ejemplo, de −50 a −75 dBm).
- Prueba de la mano. Tapa la placa con la mano o con una caja metálica (una lata de galletas sirve). Algunas redes deberían desaparecer o debilitarse mucho.
- Comparar sitios. Escribe «aula», pulsa Guardar medida, camina hasta el patio, escribe «patio» y vuelve a guardar. No reinicies la placa entre medidas: así la «Red 3» del aula sigue siendo la «Red 3» del patio y la tabla de comparación tiene sentido.
Si algo falla, el mensaje de estado de la página te dice en qué paso se ha quedado y qué revisar.
Qué estamos midiendo realmente
La potencia recibida (RSSI). El número que sale en dBm no es la potencia que emite el router, sino la que llega a la antena de esta placa, en este punto y en este instante. Se mide en decibelios-milivatio, una escala logarítmica: −50 dBm es mil veces más potencia que −80 dBm. Por eso 30 unidades de diferencia son una barbaridad, no un 40 % más. Los valores típicos van de unos −30 (pegado al router) a −90 (casi al límite de lo que se oye).
Por qué baja la señal. Con la distancia (la energía se reparte por un espacio cada vez mayor) y con los obstáculos: las paredes de ladrillo, el hormigón, el cristal con lámina metálica, los armarios metálicos y hasta las personas, que somos sobre todo agua, absorben parte de la onda. Una misma red puede cambiar varios dBm solo porque se cruce un compañero entre ella y la placa.
¿Medimos bien? Aquí va la lección de siempre: medir no significa medir bien. Esta placa no es un instrumento calibrado. El RSSI depende de la antena, de la orientación de la placa y del chip; dos placas distintas pueden dar valores diferentes para la misma red en el mismo sitio, y una misma placa fluctúa unos cuantos dBm de un escaneo a otro. Sirve muy bien para comparar (aquí se oye mejor que allí, con la misma placa) y mal para dar valores absolutos.
Lo que el escaneo se puede dejar. Cada canal se escucha solo 300 milisegundos. Las balizas salen cada ~100 ms, así que casi siempre se oyen, pero una red muy débil puede aparecer en un escaneo y faltar en el siguiente. Que una red no salga no significa que no exista.
Una simplificación confesada. Para calcular el «canal más tranquilo» contamos como «molestas» las redes de los canales a dos de distancia (c−2 a c+2). En realidad una red de 20 MHz ocupa unos 4 canales a cada lado; es una regla práctica, no la física completa. Y además solo contamos redes, no cuánto tráfico mueve cada una: un canal con dos redes casi paradas puede ir mejor que otro con una sola red que descarga vídeo.
Dato, medida y conclusión. El dato es «−61 dBm en el canal 11». La medida es ese dato en su contexto (dónde, cuándo, con qué placa). La conclusión «la red del patio es mala» ya es una interpretación. Y cuidado con la correlación: que el wifi vaya mal justo cuando hay muchas redes no demuestra que sean ellas la causa; puede ser el número de alumnos conectados a la vez.
Para el aula
Preguntas para empezar. ¿Qué variable estamos midiendo? ¿En qué unidad? ¿Por qué -50 es «mejor» que -80 si 50 es menor que 80? ¿Qué error puede tener cada medida y cómo lo podríamos estimar?
Actividad 1: el mapa de señal del centro. Divide al grupo en equipos y reparte lugares: aula, pasillo, patio, biblioteca, gimnasio. Cada equipo mide su sitio con la misma placa y sin reiniciarla entre lugares (o, si usan placas distintas, solo comparan número de redes y canal tranquilo, no las «Red N»). Con los CSV se hace una tabla común y se dibuja un plano con colores.
Actividad 2: hipótesis y experimento. Antes de medir, el grupo escribe una hipótesis («el patio tendrá menos redes pero con más potencia») y diseña cómo comprobarla: cuántas medidas, cuánto tiempo en cada sitio, qué se mantiene igual. Después se discute: ¿la hipótesis se cumple, no se cumple o los datos no alcanzan?
Actividad 3: ¿cuánto fluctúa? Deja la placa quieta 5 minutos y apunta la potencia de la Red 1 en cada escaneo. Calcula la media y la variación (máximo menos mínimo). Esa variación es una estimación de la incertidumbre de la medida. Repite con la placa en otra orientación.
Actividad 4: ¿quién bloquea la señal? Mide la misma red con la puerta abierta y cerrada, con una persona delante, con la placa dentro de una mochila o de una caja de metal. Ordena los materiales de los que más a los que menos atenuación producen.
Actividad 5: a distintas horas. Mide el mismo lugar en el recreo, en clase y a última hora. ¿Cambia el número de redes? Un buen momento para hablar de correlación y causalidad: los móviles y los puntos de acceso personales de los alumnos también emiten.
Actividad 6: dos placas. Si hay dos placas, ponlas juntas y compara qué potencia dan para la misma red. ¿Qué ocurre si los valores no coinciden? Es el punto de partida para hablar de calibración.
Conexión con ciencia ciudadana. Imagina que cada clase del instituto hace este mapa y los datos se juntan: tendríamos el mapa de ocupación del espectro del centro. Eso es exactamente un conjunto de datos recogido entre muchos, con un método común, que es el camino que la serie va siguiendo: sensor → dato → conjunto de datos → análisis → conclusión.
Retos
Reto 1: haz que funcione. Carga el programa, conecta la página y localiza en la gráfica la red más fuerte y la más débil.
Reto 2: modifica algo. Cambia MS_POR_CANAL a 120 y a 500. ¿Cuánto tarda ahora el escaneo? ¿Aparecen o desaparecen redes débiles? Después cambia la regla del canal tranquilo (de c−2..c+2 a c−4..c+4) y observa si cambia la recomendación.
Reto 3: añade un aviso. Haz que la placa encienda el LED o muestre un mensaje en la OLED cuando la red más fuerte baje de −80 dBm: un «detector de zonas sin cobertura» para ir paseando por el centro.
Reto 4: guarda los datos. Descarga el CSV de varios lugares y ábrelo en una hoja de cálculo. Calcula la media de potencia por lugar.
Reto 5: represéntalos. Haz con la hoja de cálculo un gráfico de barras de «número de redes por lugar» y otro de «potencia de la Red 1 por lugar». ¿Qué lugar es el mejor para trabajar con el móvil?
Reto 6: automatízalo. Haz que la página guarde una medida sola cada minuto y dibuje la evolución del número de redes a lo largo de la mañana. ¿Se nota el recreo?
Reto 7: que otro dispositivo participe. Con una segunda placa o con el móvil mide el mismo punto. ¿Coinciden? ¿Por qué sí o por qué no?
Reto 8: conviértelo en una pequeña investigación. Elige una pregunta («¿cómo afecta cerrar la puerta del aula a la señal del router del pasillo?»), escribe una hipótesis, diseña el experimento con al menos 10 repeticiones por condición, analiza los datos y redacta una conclusión que diga también qué no puedes afirmar.
Qué hemos aprendido
Hemos construido un escáner wifi con ESP32 que no se conecta a nada, no lee ningún nombre y, aun así, nos cuenta mucho: cuántas redes hay, en qué canal viven, con qué potencia llegan y cuál es el canal menos saturado. Hemos visto que los canales de 2,4 GHz se pisan entre sí, que solo el 1, el 6 y el 11 conviven sin molestarse, que la señal cae con la distancia y con las paredes, y que 30 dBm de diferencia son mil veces más potencia.
También hemos aprendido algo sobre cómo se construye tecnología con cabeza: pudimos haber guardado los nombres de las redes, y no lo hicimos. Con una huella aleatoria que vive solo en la RAM bastaba para distinguir «Red 1» de «Red 2», y el programa puede cumplir su misión sin saber quién es quién.
Y, sobre todo, que una medida no es una verdad: la potencia fluctúa, depende de la placa y de la antena, y sirve sobre todo para comparar sitios, no para dar valores absolutos.
Justicia Ciega ya ha dicho que le gustaría tener esto en la montura de sus gafas rojas. Dice que el radar le da otro «odo» y que, como el murciélago que lo inspira, ya no necesita ver para saber qué hay alrededor. La Gata Montesa le ha recordado que un radar sabe dónde hay ondas, no quién las emite, y que eso es justo lo que hace que sea un buen aparato. El Lince, por su parte, ha vuelto a la cueva pensando en el próximo chisme.
Para seguir investigando
Este chisme usa la misma conexión Bluetooth que los anteriores de la Heltec. Si quieres repasar de dónde viene cada pieza:
- Chisme 25: El letrero del Lince: la pantalla OLED y el primer enlace Bluetooth con la Heltec.
- Chisme 26: Termómetro Bluetooth con Heltec y DHT22: tu primer sensor y la primera serie de datos.
- Chisme 27: Semáforo Bluetooth con Heltec: la placa reacciona a un dato.
- Chisme 28: Lince seguro, localización GPS con dos Heltec: otra forma de comunicar dispositivos entre sí.
- Todos los Chismes del Lince: la colección completa.
Para profundizar en el escáner Wi-Fi del ESP32, la documentación oficial de Espressif sobre Wi-Fi explica todas las opciones de WiFi.scanNetworks() y del resto de la librería.
Etiqueta:2, 4 GHz, Arduino IDE, Bluetooth, canales wifi, ciencia ciudadana, escáner wifi, esp32, Heltec LoRa 32, OLED, privacidad, RSSI



