
Chismes del Lince 21: La brújula
Con el cuenta pasos y el nivel de burbuja aprendimos a mandar datos de la micro:bit al móvil. Hoy la conversación va en los dos sentidos: la micro:bit manda el rumbo de su brújula, el móvil lo dibuja en una brújula de verdad que gira, y desde el móvil podemos darle una orden a la micro:bit para que se calibre.
Cuando apuntes al norte, el aro de la brújula se pondrá verde y el móvil vibrará.
Lo que necesitas
- Un micro:bit V2 con su portapilas.
- 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.
- El ordenador con MakeCode para programar.
- Un sitio despejado: la brújula se vuelve loca cerca de imanes, altavoces, cables y metal grande. El patio va mejor que el aula.
Paso 1. Programa el micro:bit
Crea un proyecto nuevo en MakeCode y usa solo bloques.
- Añade la extensión Bluetooth (Extensiones → Bluetooth). No añadas la de Radio.
- Monta este programa:
al iniciar
bluetooth servicio uart
al presionarse el botón A
calibrar brújula(está en Entrada → Más)
para siempre
uart bluetooth escribir númerocondirección de la brújula (°)uart bluetooth escribir líneacon el texto vacíopausa (ms) 100
bluetooth al recibir datos nueva línea()
siuart bluetooth leer hastanueva línea()="CAL"entoncescalibrar brújula
La línea vacía es importante: es la que marca el final de cada número, y la página necesita ese final para leerlo.

Si el bloque de recibir datos te da guerra, hay un atajo: pulsa el botón JavaScript, pega el código de abajo y vuelve a Bloques; MakeCode lo convierte solo.
javascript
bluetooth.startUartService()
input.onButtonPressed(Button.A, function () {
input.calibrateCompass()
})
bluetooth.onUartDataReceived(serial.delimiters(Delimiters.NewLine), function () {
if (bluetooth.uartReadUntil(serial.delimiters(Delimiters.NewLine)) == "CAL") {
input.calibrateCompass()
}
})
basic.forever(function () {
bluetooth.uartWriteNumber(input.compassHeading())
bluetooth.uartWriteLine("")
basic.pause(100)
})
Paso 2. Sin emparejamiento (muy importante)
Antes de descargar, hay que cambiar un ajuste para que la página pueda conectar sin pedir códigos:
- Pulsa el engranaje ⚙️ → Configuración del proyecto.
- Marca No se requiere emparejamiento (No Pairing Required: Anyone can connect via Bluetooth).
Paso 3. Pásalo al micro:bit
- Descarga el programa y cópialo a la unidad MICROBIT.
- Espera a que deje de parpadear el led amarillo de atrás.
- Reinicia el micro:bit con el botón de atrás.
Si cambias algo del programa, repite los pasos 2 y 3.
Paso 4. Prepara el móvil
- Bluetooth y ubicación encendidos.
- Si en Ajustes → Bluetooth aparece “BBC micro:bit”, pulsa Olvidar. No hay que emparejarla desde ahí.
- Cierra cualquier otra app que use el micro:bit.
Paso 5. Abre la página y conecta
- Abre en Chrome, escribiendo la dirección directamente: https://montesteam.org/app/chisme-21-brujula.html
- Pulsa Conectar micro:bit.
- Elige BBC micro:bit […] en la lista.
- Cuando la página ponga Conectado, la brújula empezará a moverse.
Paso 6. Calibra la brújula
Es el único paso nuevo. La brújula de la micro:bit necesita aprender dónde está antes de medir bien. Hay que calibrarla la primera vez y cada vez que cambies de sitio o el rumbo te parezca raro.
- Pulsa Calibrar la micro:bit en la página (o el botón A de la micro:bit).
- En la matriz de LEDs saldrá un texto que se desplaza y un punto parpadeando.
- Dibuja el círculo: inclina la micro:bit en todas direcciones para que el punto ruede por la pantalla y vaya encendiendo LEDs, hasta completar el círculo.
- Al terminar saldrá una carita sonriente.
Paso 7. Mide
Sujeta el micro:bit en horizontal, con la matriz de LEDs hacia arriba, y gira despacio sobre ti mismo.
- La punta roja señala el norte.
- El número grande son los grados (0° es el norte, 90° el este, 180° el sur, 270° el oeste).
- Debajo aparece el nombre del rumbo: Norte, Noreste, Este…
- Cuando apuntas al norte, el aro se pone verde y el móvil vibra.
La página lleva además una guía de uso plegable con estos mismos pasos, por si te pierdes en el campo.
Retos para clase
- Sal al patio y encuentra el norte. ¿Hacia qué lado miran las puertas del colegio?
- Compara la micro:bit con otra brújula (o la del móvil). ¿Marcan lo mismo? ¿Y si te acercas a un coche o a una verja de metal?
- Haz una tabla con el rumbo al que miran cinco cosas del patio: la portería, la entrada, el árbol…
- Esconde un “tesoro” y escribe sus pistas en rumbos: “da 10 pasos hacia 90°, luego 5 hacia 180°”.
- Reto avanzado: que la micro:bit muestre en su matriz de LEDs una N cuando apunte al norte.
Si algo falla
- No aparece en la lista: comprueba que está encendido, que el programa está descargado y que activaste «No se requiere emparejamiento».
- Se conecta pero la brújula no se mueve: comprueba que el bloque de
escribir númerova seguido de unescribir líneacon el texto vacío, y que hay una pausa. - El norte no es el norte: vuelve a calibrar, y aléjate de imanes, altavoces y metal.
- El botón «Calibrar la micro:bit» no hace nada: usa el botón A de la micro:bit, que hace lo mismo.
- En iPhone no conecta: abre la página con Bluefy.
Qué hemos aprendido
La micro:bit lleva un magnetómetro, un sensor que nota el campo magnético de la Tierra, y con él calcula hacia dónde está el norte. Como los imanes cercanos lo engañan, hay que calibrarlo para que aprenda su entorno. La orden de calibrar viaja del móvil a la micro:bit por el mismo Bluetooth, así que ahora la conversación es de ida y vuelta. Y ojo: marca el norte magnético, que no es exactamente el geográfico.
<!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 21 · La brújula del Lince</title>
<style>
:root{
--bg:#eef2ef; --ink:#10201c; --soft:#4d625b; --card:#ffffff; --line:#cfdad5;
--pine:#1f5b4a; --slow:#2563a8; --ok:#1f7a4d; --norte:#c8321f; --oro:#b8892b; --cara:#f8faf6;
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; --cara:#13211d;}}
:root[data-theme="dark"]{--bg:#0f1a17; --ink:#e8f1ed; --soft:#9db3aa; --card:#172622; --line:#2a3d37;
--pine:#5fc4a2; --slow:#7db3ee; --ok:#6fd89f; --norte:#ff6a55; --oro:#e0b45a; --cara:#13211d;}
*{box-sizing:border-box}
html{scroll-padding-top:env(safe-area-inset-top,0px)}
body{margin:0;background:var(--bg);color:var(--ink);font:17px/1.45 "Trebuchet MS","Segoe UI",system-ui,sans-serif}
main{max-width:30rem;margin:0 auto;padding:1.2rem 1rem 2rem;text-align:center}
h1{font-size:1.3rem;margin:.2rem 0 .1rem}
.sub{color:var(--soft);margin:0 0 1rem;font-size:.95rem}
button{font:inherit;font-weight:700;border:2px solid var(--pine);background:var(--pine);color:var(--bg);
border-radius:.8rem;padding:.75rem 1.1rem;min-height:3rem;cursor:pointer}
button:disabled{opacity:.55;cursor:wait}
button:focus-visible{outline:3px solid var(--slow);outline-offset:2px}
#estado{margin:.8rem 0 .2rem;color:var(--soft);min-height:1.5em}
#ayuda{margin:0 0 .4rem;color:var(--soft);font-size:.9rem;min-height:1.2em}
.caja{width:min(88vw,22rem);margin:.6rem auto;filter:drop-shadow(0 .5rem 1rem rgba(0,0,0,.25))}
svg{display:block;width:100%;height:auto}
.aro{fill:var(--oro);stroke:var(--ink);stroke-width:1.5;transition:fill .25s}
.caja.norte .aro{fill:var(--ok)}
.cara{fill:var(--cara)}
.tk{stroke:var(--soft);stroke-width:1}
.tk.l{stroke:var(--ink);stroke-width:2}
.let{fill:var(--ink);font:700 15px "Trebuchet MS",system-ui,sans-serif;text-anchor:middle}
.let.n{fill:var(--norte);font-size:19px}
.agN{fill:var(--norte)} .agS{fill:var(--soft)}
.marca{fill:var(--norte);stroke:var(--cara);stroke-width:1.5}
.eje{fill:var(--oro);stroke:var(--ink);stroke-width:1.5}
.grados{font-size:4rem;font-weight:800;line-height:1;font-variant-numeric:tabular-nums;margin-top:.4rem}
.punto{font-size:1.5rem;font-weight:700;color:var(--pine);min-height:1.6em}
.nota{color:var(--soft);font-size:.9rem}
button.alt{background:transparent;color:var(--pine)}
button:disabled{cursor:not-allowed}
.botones{display:flex;gap:.6rem;justify-content:center;flex-wrap:wrap}
.guia{text-align:left;background:var(--card);border:1px solid var(--line);border-radius:.8rem;padding:.2rem .9rem;margin-top:1rem}
.guia summary{font-weight:700;cursor:pointer;padding:.6rem 0;min-height:2.5rem}
.guia ol{margin:.2rem 0 .6rem;padding-left:1.3rem}
.guia li{margin:.45rem 0}
.guia .aviso{color:var(--soft);font-size:.9rem;margin:.2rem 0 .7rem}
</style>
</head>
<body>
<main>
<h1>La brújula del Lince</h1>
<p class="sub">Micro:bit bien plana, móvil para mirar el rumbo.</p>
<div class="botones">
<button id="conectar">Conectar micro:bit</button>
<button id="cal" class="alt" disabled>Calibrar la micro:bit</button>
</div>
<div id="estado" role="status">Sin conectar.</div>
<div id="ayuda"></div>
<div class="caja" id="caja">
<svg viewBox="0 0 200 200" role="img" aria-label="Brújula">
<circle cx="100" cy="100" r="97" class="aro"/>
<circle cx="100" cy="100" r="89" class="cara"/>
<g id="dial"></g>
<polygon points="100,3 93,17 107,17" class="marca"/>
<circle cx="100" cy="100" r="6" class="eje"/>
</svg>
</div>
<div class="grados" id="grados">0°</div>
<div class="punto" id="punto"></div>
<p class="nota">Sujeta la micro:bit en horizontal, con la matriz hacia arriba. La punta roja siempre señala el norte.</p>
<details class="guia" open>
<summary>Guía de uso</summary>
<ol>
<li><b>Conecta.</b> Pulsa «Conectar micro:bit» y elige BBC micro:bit en la lista.</li>
<li><b>Calibra</b> (la primera vez, y cada vez que cambies de sitio o el rumbo parezca raro). Pulsa «Calibrar la micro:bit» o el botón A de la micro:bit. Saldrá un texto que se desplaza y un punto parpadeando en la matriz de LEDs.</li>
<li><b>Dibuja el círculo.</b> Inclina la micro:bit en todas direcciones para que el punto ruede por la pantalla y vaya encendiendo LEDs, hasta completar el círculo. Al terminar saldrá una carita sonriente.</li>
<li><b>Mide.</b> Sujétala en horizontal, con los LEDs hacia arriba, y gira despacio. Cuando apuntes al norte, el aro se pondrá verde y el móvil vibrará.</li>
</ol>
<p class="aviso">Aléjala de imanes, altavoces, cables y objetos de metal grandes: engañan a la brújula. Marca el norte magnético, no exactamente el geográfico. Mientras calibras, los números de la página pueden bailar: es normal.</p>
</details>
</main>
<script>
const UART_SERVICE = "6e400001-b5a3-f393-e0a9-e50e24dcca9e";
const UART_TX = "6e400003-b5a3-f393-e0a9-e50e24dcca9e";
const $ = id => document.getElementById(id);
const NS = "http://www.w3.org/2000/svg";
const NOMBRES = ["Norte","Noreste","Este","Sureste","Sur","Suroeste","Oeste","Noroeste"];
let objetivo = 0, actual = 0, hayDatos = false, wasN = false, buffer = "", conectando = false, rx = null;
const espera = ms => new Promise(r => setTimeout(r, ms));
let paso = "", diag = "";
function estado(t, ayuda){ if (!/^Error/.test(t)) paso = t; $("estado").textContent = t; $("ayuda").textContent = ayuda || ""; }
/* Dibuja la rosa de los vientos una sola vez */
(function dibujar(){
const d = $("dial");
const el = (n, a, txt) => { const e = document.createElementNS(NS, n); for (const k in a) e.setAttribute(k, a[k]); if (txt) e.textContent = txt; d.appendChild(e); };
for (let a = 0; a < 360; a += 10){
const largo = a % 30 === 0;
el("line", { x1: 100, y1: 12, x2: 100, y2: largo ? 24 : 19, class: "tk" + (largo ? " l" : ""), transform: "rotate(" + a + " 100 100)" });
}
[["N",0],["E",90],["S",180],["O",270]].forEach(([t, a]) =>
el("text", { x: 100, y: 45, class: "let" + (t === "N" ? " n" : ""), transform: "rotate(" + a + " 100 100)" }, t));
el("polygon", { points: "100,56 92,100 108,100", class: "agN" });
el("polygon", { points: "100,144 92,100 108,100", class: "agS" });
})();
function girar(){
let dif = ((objetivo - actual + 540) % 360) - 180; // camino más corto, sin saltos 359→0
actual += dif * 0.18;
$("dial").setAttribute("transform", "rotate(" + (-actual).toFixed(2) + " 100 100)");
requestAnimationFrame(girar);
}
function pintar(){
const h = ((Math.round(objetivo) % 360) + 360) % 360;
$("grados").textContent = hayDatos ? h + "°" : "—";
$("punto").textContent = hayDatos ? NOMBRES[Math.round(h / 45) % 8] : "";
const n = hayDatos && (h <= 4 || h >= 356);
$("caja").classList.toggle("norte", n);
if (n && !wasN && navigator.vibrate) navigator.vibrate(40);
wasN = n;
}
function linea(texto){
const m = texto.match(/-?\d+/);
if (!m) return;
const h = ((+m[0] % 360) + 360) % 360;
if (!hayDatos){ actual = h; hayDatos = true; }
objetivo = h;
pintar();
}
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);
}
}
function explicar(e){
const nombre = e && e.name ? e.name : "Error";
const msg = e && e.message ? e.message : String(e);
let ayuda = "";
if (nombre === "NotFoundError" && /cancel/i.test(msg)) ayuda = "No has elegido ningún dispositivo. Pulsa Conectar y elige BBC micro:bit.";
else if (nombre === "NotFoundError") ayuda = "La micro:bit no tiene el servicio UART. Revisa el bloque «bluetooth servicio uart» y copia el .hex nuevo.";
else if (nombre === "NotSupportedError") ayuda = "No se pudo activar el envío de datos. Olvida la micro:bit en Ajustes → Bluetooth, borra la caché de la app «Bluetooth» del sistema y reinicia el móvil y la micro:bit.";
else if (nombre === "NetworkError") ayuda = "No se pudo abrir la conexión. Comprueba «No se requiere emparejamiento» en MakeCode, que no haya otra app conectada y reinicia la micro:bit.";
else if (nombre === "SecurityError") ayuda = "La página debe abrirse con https://.";
else if (nombre === "NotAllowedError") ayuda = "Permiso denegado. Permite el Bluetooth y los dispositivos cercanos para el navegador.";
estado("Error (" + nombre + "): " + msg,
"Falló en el paso: «" + paso + "». " + ayuda + (diag ? " Características: " + diag : ""));
}
async function conectarGatt(dev){
let ultimoError;
for (let intento = 1; intento <= 4; intento++){
try {
estado("Conectando… (intento " + intento + " 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;
}
$("conectar").onclick = async () => {
if (conectando) 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 micro:bit…");
const dev = await navigator.bluetooth.requestDevice({ acceptAllDevices: true, optionalServices: [UART_SERVICE] });
dev.addEventListener("gattserverdisconnected", () => {
rx = null; $("cal").disabled = true;
hayDatos = false; pintar();
estado("Desconectado.", "Pulsa Conectar para volver a empezar.");
});
const svc = await conectarGatt(dev);
buffer = ""; hayDatos = false; diag = "";
estado("Leyendo las características del servicio…");
const chars = await svc.getCharacteristics();
diag = chars.map(c => c.uuid.slice(0, 8) + " [" + Object.keys(c.properties).filter(k => c.properties[k]).join(", ") + "]").join(" · ");
const candidatas = chars.filter(c => c.properties.notify || c.properties.indicate)
.sort((a, b) => (b.uuid === UART_TX) - (a.uuid === UART_TX));
if (!candidatas.length) throw new Error("El servicio UART no tiene ninguna característica que envíe datos.");
let ch = null, ultimoErr;
for (const c of candidatas){
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;
$("cal").disabled = !rx;
estado("Conectado a " + (dev.name || "micro:bit") + ". Si te pide calibrar, dibuja un círculo con la bolita.");
} catch (e){
explicar(e);
} finally {
conectando = false; $("conectar").disabled = false;
}
};
$("cal").onclick = async () => {
if (!rx) return;
try {
const datos = new TextEncoder().encode("CAL\n");
if (rx.properties.writeWithoutResponse && rx.writeValueWithoutResponse) await rx.writeValueWithoutResponse(datos);
else await rx.writeValue(datos);
estado("Orden enviada. Mira la pantalla de la micro:bit y dibuja el círculo.");
} catch (e){
estado("No se pudo enviar la orden (" + e.name + ").", "Pulsa el botón A de la micro:bit para calibrar.");
}
};
pintar();
requestAnimationFrame(girar);
</script>
</body>
</html>



