
Chismes del Lince 45: El árbol de la micro:bit (árbol de decisión)
Sierra Bermeja, por la mañana. El Lince de la Noche trota monte arriba con una micro:bit en el bolsillo y la cara de quien cree que lleva un secreto.
—¿Qué llevas ahí? —pregunta la Gata Montesa.
—Un sensor de movimiento. Quiero que sepa si voy parado, andando o corriendo. Estoy pensando en escribir doscientas condiciones…
—Lince, no escribas ni una. Enséñale tres movimientos y que dibuje ella las preguntas.
—¿Un árbol de decisión?
—Exacto. Tú te mueves y ella aprende.
Introducción
En este chisme una micro:bit aprende a distinguir si estamos parados, andando o corriendo. Es un ejemplo de micro:bit y árbol de decisión: la micro:bit mide el movimiento, el móvil recibe los datos por Bluetooth y una página dibuja el árbol de decisión mientras nos movemos. Es un arbol de decisión micro:bit.
No hay que construir el árbol a mano. Se enseña con ejemplos y el árbol aparece, como en la versión del chisme anterior, pero ahora con datos que recoge el propio alumno.
Qué necesitamos
- Una micro:bit V2 con su portapilas.
- Un bolsillo, una goma o una muñequera para llevarla.
- Un móvil Android con Chrome (en iPhone hace falta el navegador Bluefy).
- MakeCode con la extensión Bluetooth instalada.
El programa de la micro:bit
Para hacer este árbol de decisión micro:bit, crea un proyecto nuevo en MakeCode y añade la extensión Bluetooth. En Ajustes del proyecto activa «Sin emparejamiento». Cambia a JavaScript y pega este programa:
let maximo = 0
let minimo = 0
let fuerza = 0
bluetooth.startUartService()
input.setAccelerometerRange(AcceleratorRange.EightG)
bluetooth.onBluetoothConnected(function () {
basic.showIcon(IconNames.Yes)
})
bluetooth.onBluetoothDisconnected(function () {
basic.showIcon(IconNames.No)
})
basic.forever(function () {
maximo = 0
minimo = 99999
for (let index = 0; index <= 49; index++) {
fuerza = input.acceleration(Dimension.Strength)
maximo = Math.max(maximo, fuerza)
minimo = Math.min(minimo, fuerza)
basic.pause(20)
}
bluetooth.uartWriteLine("" + maximo + "," + minimo)
})
Cada segundo, la micro:bit mide la fuerza del movimiento 50 veces y se queda con la mayor y la menor. Envía dos números por Bluetooth, por ejemplo 1830,640.
Lee y predice: si la micro:bit está quieta sobre la mesa, ¿qué número crees que saldrá? (Pista: algo tira de ella hacia abajo.)
Si prefieres montarlo con bloques: un bloque para siempre, dentro un bucle para índice de 0 a 49 con tres variables (fuerza, maximo y minimo) y, al final, uart bluetooth escribir línea con el bloque unir.
La página del árbol
Pruébala aquí: El árbol de la micro:bit. Ábrela en Chrome, en el móvil y en vertical.
Si el Bluetooth funciona dentro de la entrada, este es el código del iframe para un bloque HTML personalizado de WordPress. Si no conecta, usa el enlace de arriba:
<iframe src="https://montesteam.org/app/chisme-45-arbol-microbit.html" title="Árbol de decisión con micro:bit" width="100%" height="1500" style="border:0" allow="bluetooth" loading="lazy"></iframe>

Código completo de la página
Copia todo el código, pégalo en un archivo de texto y guárdalo como chisme-45-arbol-microbit.html (codificación UTF-8). Para que el Bluetooth funcione, la página tiene que abrirse desde una dirección https://, no desde el archivo suelto.
<!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 45 · El árbol de la micro:bit</title>
<style>
:root{
--bg:#eef2ef; --ink:#10201c; --soft:#4d625b; --card:#ffffff; --line:#cfdad5;
--pine:#1f5b4a; --sel:#7b3fa0;
--c0:#2563a8; --c1:#d9822b; --c2:#8e44ad; /* parado · andando · corriendo */
--f0:#dce9f7; --f1:#fbe9d3; --f2:#ecdcf3;
}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){
--bg:#0f1a17; --ink:#e8f1ed; --soft:#9db3aa; --card:#172622; --line:#2a3d37; --pine:#5fc4a2; --sel:#c79bff;
--c0:#7db3ee; --c1:#f0a95f; --c2:#c99be0;
--f0:#16304a; --f1:#4a3318; --f2:#3b2646;}}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);font:17px/1.45 "Trebuchet MS","Segoe UI",system-ui,sans-serif;
padding-top:env(safe-area-inset-top,0px);padding-bottom:env(safe-area-inset-bottom,0px)}
main{max-width:30rem;margin:0 auto;padding:1.2rem 1rem 2rem}
h1{font-size:1.3rem;margin:.2rem 0 .1rem}
h2{font-size:1.05rem;margin:0 0 .5rem}
.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:.65rem 1rem;min-height:3rem;cursor:pointer}
button:disabled{opacity:.5;cursor:not-allowed}
button.alt{background:transparent;color:var(--pine)}
button:focus-visible{outline:3px solid var(--sel);outline-offset:2px}
.botones{display:flex;gap:.6rem;flex-wrap:wrap}
#estado{margin:.8rem 0 .2rem;color:var(--soft);min-height:1.5em}
#ayuda{margin:0 0 .6rem;color:var(--soft);font-size:.9rem;min-height:1.2em}
.card{background:var(--card);border:1px solid var(--line);border-radius:.9rem;padding:.9rem;margin:1rem 0}
.fila{display:flex;gap:.8rem}
.dato{flex:1;background:var(--bg);border:1px solid var(--line);border-radius:.8rem;padding:.5rem .8rem}
.dato b{display:block;font-size:1.7rem;font-variant-numeric:tabular-nums}
.lbl{color:var(--soft);font-size:.88rem}
.clases{display:grid;grid-template-columns:repeat(3,1fr);gap:.5rem}
.clases button{display:flex;flex-direction:column;align-items:center;gap:.1rem;padding:.6rem .2rem;font-size:.95rem}
.clases button span.e{font-size:1.6rem}
.clases button small{font-weight:400;opacity:.9}
.clases button.on{outline:4px solid var(--sel);outline-offset:2px}
#grab{min-height:1.6em;font-weight:700;margin:.6rem 0 0}
canvas{width:100%;aspect-ratio:1;display:block;border:1px solid var(--line);border-radius:.6rem;background:var(--bg)}
#arbolcaja svg{width:100%;height:auto;display:block}
svg text{fill:var(--ink);font:13px "Trebuchet MS",system-ui,sans-serif}
svg .nd rect{fill:var(--card);stroke:var(--soft);stroke-width:1.5}
svg .nd.h0 rect{fill:var(--f0);stroke:var(--c0)} svg .nd.h1 rect{fill:var(--f1);stroke:var(--c1)} svg .nd.h2 rect{fill:var(--f2);stroke:var(--c2)}
svg .nd.cam rect{stroke:var(--sel);stroke-width:4}
svg .rama{stroke:var(--soft);stroke-width:1.5;fill:none} svg .et{fill:var(--soft);font-size:11px}
#reglas{margin:.4rem 0 0;padding-left:1.1rem;font-size:.95rem} #reglas li{margin:.25rem 0}
#ahora{text-align:center;font-size:2rem;font-weight:800;min-height:3.2rem;line-height:1.2}
#ahora small{display:block;font-size:.9rem;font-weight:400;color:var(--soft)}
details{background:var(--card);border:1px solid var(--line);border-radius:.9rem;padding:.2rem .9rem;margin:1rem 0}
summary{cursor:pointer;font-weight:700;color:var(--pine);padding:.6rem 0}
details p,details li{font-size:.95rem;margin:.3rem 0}
pre{background:var(--bg);border:1px solid var(--line);border-radius:.6rem;padding:.6rem;overflow-x:auto;font-size:.82rem}
.nota{color:var(--soft);font-size:.9rem}
</style>
</head>
<body>
<main>
<h1>El árbol de la micro:bit</h1>
<p class="sub">Le enseñas tres movimientos y ella sola dibuja las preguntas que los distinguen.</p>
<div class="botones">
<button id="conectar">Conectar micro:bit</button>
<button id="demo" class="alt">Probar con datos de ejemplo</button>
</div>
<div id="estado" role="status">Sin conectar.</div>
<div id="ayuda"></div>
<div class="card">
<h2>Lo que mide ahora (cada segundo)</h2>
<div class="fila">
<div class="dato"><span class="lbl">Variación</span><b id="vVr">–</b><span class="lbl">mg</span></div>
<div class="dato"><span class="lbl">Fuerza máxima</span><b id="vMx">–</b><span class="lbl">mg</span></div>
</div>
</div>
<div class="card">
<h2>1 · Enséñale: pulsa y haz ese movimiento 10 segundos</h2>
<div class="clases" id="clases"></div>
<div id="grab" aria-live="polite"></div>
<div class="botones" style="margin-top:.6rem">
<button id="borrar" class="alt">Borrar datos</button>
<button id="csv" class="alt">Guardar CSV</button>
</div>
</div>
<div class="card">
<h2>2 · Cada punto es un segundo de movimiento</h2>
<canvas id="plano" aria-label="Variación y fuerza máxima de cada segundo"></canvas>
<p class="nota" id="notaPlano"></p>
</div>
<div class="card">
<h2>3 · El árbol que ha aprendido</h2>
<div id="arbolcaja"><svg id="arbol" role="img" aria-label="Árbol de decisión"></svg></div>
<p id="acierto" class="nota"></p>
<ul id="reglas"></ul>
</div>
<div class="card">
<h2>4 · Pruébalo: muévete</h2>
<div id="ahora">—</div>
</div>
<details>
<summary>¿Qué mide la micro:bit y cómo elige el árbol?</summary>
<p>La micro:bit tiene un acelerómetro: un chip que nota cuánto empuja el movimiento. Cada segundo calcula la <b>fuerza de aceleración</b> muchas veces y se queda con dos cosas: la <b>fuerza máxima</b> y la <b>variación</b> (máxima menos mínima).</p>
<p>Quieta marca unos 1000 mg: es la gravedad, que siempre tira de ella. Al andar, el pie da golpes y la variación sube. Al correr, los golpes son más fuertes.</p>
<p>El árbol prueba todas las preguntas del tipo «¿la variación es menor o igual que…?» y «¿la fuerza máxima es menor o igual que…?». Se queda con la que deja los grupos más <b>separados</b> y repite dentro de cada grupo. Dos preguntas como máximo, para que se pueda leer.</p>
<p>La micro:bit, por muy bien que mida, no es un instrumento de laboratorio: no dos micro:bit marcan igual y no es lo mismo llevarla en el bolsillo que en el tobillo. Por eso el árbol hay que entrenarlo siempre con la micro:bit puesta donde la vas a llevar.</p>
</details>
<details>
<summary>El programa de la micro:bit (MakeCode)</summary>
<p>Proyecto nuevo, añade la extensión <b>Bluetooth</b> y en Ajustes del proyecto activa «Sin emparejamiento». Cambia a JavaScript y pega:</p>
<pre>let maximo = 0
let minimo = 0
let fuerza = 0
bluetooth.startUartService()
input.setAccelerometerRange(AcceleratorRange.EightG)
bluetooth.onBluetoothConnected(function () {
basic.showIcon(IconNames.Yes)
})
bluetooth.onBluetoothDisconnected(function () {
basic.showIcon(IconNames.No)
})
basic.forever(function () {
maximo = 0
minimo = 99999
for (let index = 0; index <= 49; index++) {
fuerza = input.acceleration(Dimension.Strength)
maximo = Math.max(maximo, fuerza)
minimo = Math.min(minimo, fuerza)
basic.pause(20)
}
bluetooth.uartWriteLine("" + maximo + "," + minimo)
})</pre>
<p><b>Con bloques</b> (para que lo monten los alumnos):</p>
<ol>
<li><b>al iniciar</b>: Bluetooth → «servicio UART bluetooth»; Entrada → Más → «establecer intervalo del acelerómetro a 8g».</li>
<li><b>al conectar Bluetooth</b>: mostrar icono ✔. <b>al desconectar Bluetooth</b>: mostrar icono ✘.</li>
<li><b>para siempre</b>: poner <i>maximo</i> a 0; poner <i>minimo</i> a 99999.</li>
<li>Dentro, un bloque <b>para índice de 0 a 49</b>: poner <i>fuerza</i> a «aceleración (mg) fuerza»; poner <i>maximo</i> a «máx de maximo y fuerza»; poner <i>minimo</i> a «mín de minimo y fuerza»; pausa 20 ms.</li>
<li>Después del bucle: Bluetooth → «uart bluetooth escribir línea» con el bloque «unir» de Texto: <i>maximo</i>, «,», <i>minimo</i>.</li>
</ol>
</details>
</main>
<script>
/* ============ NOMBRES ============ */
const CLASES = [
{n:"Parado", e:"🧍", forma:"círculo"},
{n:"Andando", e:"🚶", forma:"triángulo"},
{n:"Corriendo", e:"🏃", forma:"cuadrado"}
];
const VARS = [ {n:"Variación", k:"vr"}, {n:"Fuerza máxima", k:"mx"} ];
const $ = id => document.getElementById(id);
const css = n => getComputedStyle(document.documentElement).getPropertyValue(n).trim();
/* ============ ESTADO ============ */
let datos = []; // cada punto: {vr, mx, c} c = 0 parado, 1 andando, 2 corriendo
let arbol = null;
let actual = null; // última medida: {vr, mx}
let grabando = -1, quedan = 0, ultimaLinea = 0;
let conectado = false, demoOn = false, demoReloj = null;
/* ============ EL ÁRBOL (algoritmo CART con dos preguntas como máximo) ============ */
const gini = (cn, n) => { let s = 1; for (const c of cn){ const p = c / n; s -= p * p; } return s; };
function cuentas(idx){ const c = [0,0,0]; for (const i of idx) c[datos[i].c]++; return c; }
/* La mejor pregunta: la que deja los dos grupos menos mezclados */
function mejorCorte(idx){
const n = idx.length, cn = cuentas(idx), g0 = gini(cn, n); let mejor = null;
for (let f = 0; f < 2; f++){
const k = VARS[f].k, o = idx.slice().sort((a,b) => datos[a][k] - datos[b][k]);
const izq = [0,0,0];
for (let j = 0; j < n - 1; j++){
izq[datos[o[j]].c]++;
const a = datos[o[j]][k], b = datos[o[j+1]][k];
if (a === b) continue;
const nl = j + 1, nr = n - nl, der = cn.map((v,i) => v - izq[i]);
const s = (nl * gini(izq, nl) + nr * gini(der, nr)) / n;
if (!mejor || s < mejor.s - 1e-12) mejor = {f, th: Math.floor((a + b) / 2), s};
}
}
return mejor && g0 - mejor.s > 1e-9 ? mejor : null;
}
function crece(idx, prof){
const cn = cuentas(idx), nodo = {cn, prof, cl: cn.indexOf(Math.max(...cn))};
const puro = cn.filter(x => x > 0).length <= 1;
if (!puro && prof < 2 && idx.length >= 2){
const m = mejorCorte(idx);
if (m){
const L = [], R = [];
for (const i of idx) (datos[i][VARS[m.f].k] <= m.th ? L : R).push(i);
nodo.f = m.f; nodo.th = m.th; nodo.si = crece(L, prof + 1); nodo.no = crece(R, prof + 1);
}
}
return nodo;
}
function aprende(){
const hay = cuentas(datos.map((_,i) => i)).filter(x => x > 0).length;
arbol = hay >= 2 ? crece(datos.map((_,i) => i), 0) : null;
}
function clasifica(m){ let n = arbol; while (n.f !== undefined) n = m[VARS[n.f].k] <= n.th ? n.si : n.no; return n; }
function camino(m){ const r = []; let n = arbol; r.push(n); while (n.f !== undefined){ n = m[VARS[n.f].k] <= n.th ? n.si : n.no; r.push(n); } return r; }
/* ============ DIBUJO: PLANO ============ */
function forma(ctx, c, x, y, r){
ctx.beginPath();
if (c === 0) ctx.arc(x, y, r, 0, 6.2832);
else if (c === 1){ ctx.moveTo(x, y - r - 1); ctx.lineTo(x + r + 1, y + r); ctx.lineTo(x - r - 1, y + r); ctx.closePath(); }
else ctx.rect(x - r, y - r, 2 * r, 2 * r);
ctx.fill();
}
function dibujaPlano(){
const cv = $("plano"), ctx = cv.getContext("2d"), dpr = window.devicePixelRatio || 1;
const W = cv.clientWidth, H = cv.clientHeight; if (!W) return;
if (cv.width !== Math.round(W * dpr)){ cv.width = Math.round(W * dpr); cv.height = Math.round(H * dpr); }
ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.clearRect(0, 0, W, H);
const L = 38, B = 28, T = 8, Rr = 8;
let xmax = 1500, ymax = 3000;
for (const d of datos){ xmax = Math.max(xmax, d.vr * 1.1); ymax = Math.max(ymax, d.mx * 1.1); }
if (actual){ xmax = Math.max(xmax, actual.vr * 1.1); ymax = Math.max(ymax, actual.mx * 1.1); }
const X = v => L + v / xmax * (W - L - Rr), Y = v => H - B - v / ymax * (H - B - T);
const col = [css("--c0"), css("--c1"), css("--c2")], fon = [css("--f0"), css("--f1"), css("--f2")];
/* zonas que dibuja el árbol: cada hoja es un rectángulo */
if (arbol){
(function zona(n, x0, x1, y0, y1){
if (n.f === undefined){ ctx.fillStyle = fon[n.cl]; ctx.fillRect(X(x0), Y(y1), X(x1) - X(x0), Y(y0) - Y(y1)); return; }
if (n.f === 0){ zona(n.si, x0, n.th, y0, y1); zona(n.no, n.th, x1, y0, y1); }
else { zona(n.si, x0, x1, y0, n.th); zona(n.no, x0, x1, n.th, y1); }
})(arbol, 0, xmax, 0, ymax);
ctx.strokeStyle = css("--soft"); ctx.lineWidth = 1.5; ctx.setLineDash([5, 4]);
(function lineas(n, x0, x1, y0, y1){
if (n.f === undefined) return;
ctx.beginPath();
if (n.f === 0){ ctx.moveTo(X(n.th), Y(y0)); ctx.lineTo(X(n.th), Y(y1)); lineas(n.si, x0, n.th, y0, y1); lineas(n.no, n.th, x1, y0, y1); }
else { ctx.moveTo(X(x0), Y(n.th)); ctx.lineTo(X(x1), Y(n.th)); lineas(n.si, x0, x1, y0, n.th); lineas(n.no, x0, x1, n.th, y1); }
ctx.stroke();
})(arbol, 0, xmax, 0, ymax);
ctx.setLineDash([]);
}
/* ejes */
ctx.strokeStyle = css("--soft"); ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(L, T); ctx.lineTo(L, H - B); ctx.lineTo(W - Rr, H - B); ctx.stroke();
ctx.fillStyle = css("--soft"); ctx.font = "11px system-ui"; ctx.textAlign = "right"; ctx.textBaseline = "middle";
ctx.fillText(Math.round(ymax), L - 4, T + 4); ctx.fillText("0", L - 4, H - B);
ctx.textAlign = "center"; ctx.textBaseline = "top"; ctx.fillText("Variación (mg) →", L + (W - L) / 2, H - B + 12); ctx.fillText(Math.round(xmax), W - 18, H - B + 3);
ctx.save(); ctx.translate(11, T + (H - B - T) / 2); ctx.rotate(-Math.PI / 2); ctx.textBaseline = "middle"; ctx.fillText("Fuerza máxima (mg) →", 0, 0); ctx.restore();
/* puntos */
ctx.globalAlpha = .85;
for (const d of datos){ ctx.fillStyle = col[d.c]; forma(ctx, d.c, X(d.vr), Y(d.mx), 4.5); }
ctx.globalAlpha = 1;
/* la medida de ahora */
if (actual){ ctx.strokeStyle = css("--ink"); ctx.lineWidth = 3; ctx.beginPath(); ctx.arc(X(actual.vr), Y(actual.mx), 9, 0, 6.2832); ctx.stroke(); }
}
/* ============ DIBUJO: ÁRBOL ============ */
const BW = 96, BH = 54, GX = 10, GY = 38;
function dibujaArbol(){
const s = $("arbol");
if (!arbol){ s.setAttribute("viewBox", "0 0 300 60"); s.innerHTML = '<text x="150" y="35" text-anchor="middle">Aún no hay árbol: enséñale al menos dos movimientos.</text>'; return; }
const ruta = actual ? new Set(camino(actual)) : new Set();
let k = 0, prof = 0;
(function pos(n){ prof = Math.max(prof, n.prof); if (n.f === undefined) n.cx = k++; else { pos(n.si); pos(n.no); n.cx = (n.si.cx + n.no.cx) / 2; } })(arbol);
const W = k * (BW + GX) + GX, H = (prof + 1) * (BH + GY) - GY + 12;
const px = n => GX + n.cx * (BW + GX) + BW / 2, py = n => 6 + n.prof * (BH + GY);
let ramas = "", cajas = "";
(function pinta(n){
const x = px(n), y = py(n), hoja = n.f === undefined;
if (!hoja) for (const [h, et] of [[n.si, "sí"], [n.no, "no"]]){
ramas += '<path class="rama" d="M' + x + ' ' + (y + BH) + ' C' + x + ' ' + (y + BH + GY / 2) + ' ' + px(h) + ' ' + (py(h) - GY / 2) + ' ' + px(h) + ' ' + py(h) + '"/>' +
'<text class="et" x="' + ((x + px(h)) / 2 + (h === n.si ? -9 : 9)) + '" y="' + (y + BH + GY / 2 + 3) + '" text-anchor="middle">' + et + '</text>';
}
const total = n.cn.reduce((a, b) => a + b, 0);
const l1 = hoja ? CLASES[n.cl].e + " " + CLASES[n.cl].n : VARS[n.f].n;
const l2 = hoja ? total + " seg." : "≤ " + n.th + " mg ?";
cajas += '<g class="nd' + (hoja ? " h" + n.cl : "") + (ruta.has(n) ? " cam" : "") + '"><rect x="' + (x - BW / 2) + '" y="' + y + '" width="' + BW + '" height="' + BH + '" rx="9"/>' +
'<text x="' + x + '" y="' + (y + 23) + '" text-anchor="middle" font-weight="700">' + l1 + '</text>' +
'<text x="' + x + '" y="' + (y + 42) + '" text-anchor="middle">' + l2 + '</text></g>';
if (!hoja){ pinta(n.si); pinta(n.no); }
})(arbol);
s.setAttribute("viewBox", "0 0 " + W + " " + H); s.innerHTML = ramas + cajas;
}
/* ============ TEXTO: REGLAS Y ACIERTOS ============ */
function reglas(){
const ul = $("reglas"); ul.innerHTML = "";
if (!arbol){ $("acierto").textContent = ""; return; }
const filas = [];
const frase = lim => { // une las condiciones de la misma medida: «590 < variación ≤ 1936»
const partes = [];
for (let f = 0; f < 2; f++){
const lo = lim[f].lo, hi = lim[f].hi, n = VARS[f].n.toLowerCase();
if (lo === null && hi === null) continue;
if (lo !== null && hi !== null) partes.push(lo + " < " + n + " ≤ " + hi);
else if (hi !== null) partes.push(n + " ≤ " + hi);
else partes.push(n + " > " + lo);
}
return partes.length ? "Si " + partes.join(" y ") : "Siempre";
};
(function rec(n, lim){
if (n.f === undefined){ filas.push(frase(lim) + " → " + CLASES[n.cl].e + " " + CLASES[n.cl].n.toLowerCase()); return; }
const a = lim.map(x => ({...x})), b = lim.map(x => ({...x}));
a[n.f].hi = a[n.f].hi === null ? n.th : Math.min(a[n.f].hi, n.th);
b[n.f].lo = b[n.f].lo === null ? n.th : Math.max(b[n.f].lo, n.th);
rec(n.si, a); rec(n.no, b);
})(arbol, [{lo: null, hi: null}, {lo: null, hi: null}]);
for (const f of filas){ const li = document.createElement("li"); li.textContent = f; ul.appendChild(li); }
let ok = 0; for (const d of datos) if (clasifica(d).cl === d.c) ok++;
$("acierto").textContent = "El árbol acierta " + ok + " de " + datos.length + " segundos de los que ha visto. Con movimientos nuevos puede fallar más.";
}
function ahora(){
const a = $("ahora");
if (!arbol){ a.textContent = "—"; return; }
if (!actual){ a.textContent = "Esperando datos…"; return; }
const c = clasifica(actual).cl;
a.innerHTML = CLASES[c].e + " " + CLASES[c].n + "<small>Dice el árbol</small>";
}
function recuentos(){
const cn = cuentas(datos.map((_, i) => i));
document.querySelectorAll("#clases button").forEach((b, i) => b.querySelector("small").textContent = cn[i] + " seg.");
$("notaPlano").textContent = datos.length ? "Forma: círculo parado · triángulo andando · cuadrado corriendo. La zona de color es lo que dice el árbol." : "Aún no hay puntos.";
}
function todo(){ aprende(); recuentos(); dibujaPlano(); dibujaArbol(); reglas(); ahora(); }
/* ============ BOTONES DE LAS CLASES ============ */
CLASES.forEach((c, i) => {
const b = document.createElement("button");
b.innerHTML = '<span class="e">' + c.e + '</span>' + c.n + '<small>0 seg.</small>';
b.onclick = () => {
if (!conectado && !demoOn){ $("grab").textContent = "Conecta la micro:bit o usa los datos de ejemplo."; return; }
grabando = i; quedan = 10; ultimaLinea = performance.now(); marcar();
$("grab").textContent = "Grabando «" + c.n + "»… quedan 10 s";
};
$("clases").appendChild(b);
});
function marcar(){ document.querySelectorAll("#clases button").forEach((b, i) => b.classList.toggle("on", i === grabando)); }
setInterval(() => { // si no llegan datos mientras se graba, avisa
if (grabando >= 0 && performance.now() - ultimaLinea > 3500){ $("grab").textContent = "No llegan datos de la micro:bit."; }
}, 1000);
/* ============ LLEGAN LOS DATOS ============ */
function nuevaMedida(mx, mn){
const vr = Math.max(0, mx - mn);
actual = {vr, mx};
ultimaLinea = performance.now();
$("vVr").textContent = vr; $("vMx").textContent = mx;
if (grabando >= 0){
datos.push({vr, mx, c: grabando}); quedan--;
if (quedan > 0) $("grab").textContent = "Grabando «" + CLASES[grabando].n + "»… quedan " + quedan + " s";
else { $("grab").textContent = "Listo: «" + CLASES[grabando].n + "» guardado."; grabando = -1; marcar(); }
todo();
} else { dibujaPlano(); dibujaArbol(); ahora(); }
}
function linea(texto){
const m = texto.match(/(\d+)\D+(\d+)/); // «máximo,mínimo»
if (m) nuevaMedida(parseInt(m[1], 10), parseInt(m[2], 10));
}
let buffer = "";
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); }
}
/* ============ DATOS DE EJEMPLO (inventados, para probar sin micro:bit) ============ */
let semilla = 7;
const azar = () => { semilla = (semilla * 16807) % 2147483647; return semilla / 2147483647; };
const entre = (a, b) => a + (b - a) * azar();
function medidaInventada(c){
let mn, mx;
if (c === 0){ mn = entre(960, 1000); mx = mn + entre(10, 90); }
else if (c === 1){ mn = entre(400, 800); mx = entre(1500, 2500); }
else { mn = entre(100, 500); mx = entre(2600, 4300); }
return [Math.round(mx), Math.round(mn)];
}
$("demo").onclick = () => {
if (demoOn){
demoOn = false; clearInterval(demoReloj); grabando = -1; marcar(); $("demo").textContent = "Probar con datos de ejemplo";
estado("Sin conectar."); return;
}
if (conectado) return;
demoOn = true; $("demo").textContent = "Parar los datos de ejemplo";
estado("Datos de ejemplo: están inventados, no vienen de ninguna micro:bit.", "Pulsa un movimiento y mira cómo se dibuja el árbol. La «medida de ahora» va cambiando sola.");
let fase = 0, cuenta = 0;
demoReloj = setInterval(() => { // en cada paso inventa un segundo de un movimiento distinto
cuenta++; if (cuenta % 4 === 0) fase = (fase + 1) % 3;
const [mx, mn] = medidaInventada(grabando >= 0 ? grabando : fase);
nuevaMedida(mx, mn);
}, 600);
};
$("borrar").onclick = () => { datos = []; grabando = -1; marcar(); $("grab").textContent = ""; todo(); };
$("csv").onclick = () => {
if (!datos.length) return;
const txt = "movimiento,variacion_mg,fuerza_maxima_mg\n" + datos.map(d => CLASES[d.c].n.toLowerCase() + "," + d.vr + "," + d.mx).join("\n") + "\n";
const a = document.createElement("a"); a.href = URL.createObjectURL(new Blob([txt], {type: "text/csv"}));
a.download = "arbol-microbit.csv"; a.click(); URL.revokeObjectURL(a.href);
};
/* ============ BLUETOOTH (igual que en los chismes 19 a 22) ============ */
const UART_SERVICE = "6e400001-b5a3-f393-e0a9-e50e24dcca9e";
const UART_TX = "6e400003-b5a3-f393-e0a9-e50e24dcca9e"; // la micro:bit envía por aquí
let paso = "", diag = "", conectando = false;
const espera = ms => new Promise(r => setTimeout(r, ms));
function estado(t, ayuda){ if (!/^Error/.test(t)) paso = t; $("estado").textContent = t; $("ayuda").textContent = ayuda || ""; }
function explicar(e){
const nombre = e && e.name ? e.name : "Error", 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 que el programa llame a bluetooth.startUartService().";
else if (nombre === "NotSupportedError") ayuda = "No se pudo activar el envío de datos. Olvida la micro:bit en Ajustes → Bluetooth y reinicia el móvil y la micro:bit.";
else if (nombre === "NetworkError") ayuda = "No se ha podido abrir la conexión. En MakeCode activa «Sin emparejamiento», quita la micro:bit de los dispositivos Bluetooth del sistema y reiníciala.";
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 || demoOn) return;
if (!navigator.bluetooth){ estado("Este navegador no permite Bluetooth.", "Usa Chrome o Edge (ordenador o Android). Safari y Firefox no sirven."); 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", () => {
conectado = false; grabando = -1; marcar(); estado("Desconectado.", "Pulsa Conectar para volver a empezar.");
});
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) + " [" + 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… (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;
conectado = true;
estado("Conectado a " + (dev.name || "micro:bit") + ". Ponla donde la vas a llevar y enséñale un movimiento.");
} catch (e){ explicar(e); }
finally { conectando = false; $("conectar").disabled = false; }
};
window.addEventListener("resize", () => { dibujaPlano(); });
todo();
</script>
</body>
</html>
Cómo probarlo
- Copia el programa a la micro:bit y guárdala en el bolsillo.
- En la página pulsa Conectar micro:bit y elige la micro:bit.
- Pulsa Parado y quédate quieto 10 segundos. Después Andando y, por último, Corriendo.
- Mira cómo cambian el plano, el árbol y las reglas con cada segundo nuevo.
- En «Pruébalo» muévete y comprueba qué dice el árbol.
Sin micro:bit, el botón Probar con datos de ejemplo usa datos inventados para ver cómo funciona.
Qué estamos midiendo realmente
La micro:bit lleva un acelerómetro, un chip que nota cuánto empuja el movimiento. Lo mide en mg, milésimas de la gravedad. Quieta sobre la mesa marca unos 1000 mg, porque la gravedad siempre tira de ella.
Cada segundo guardamos dos datos: la fuerza máxima y la variación, que es la máxima menos la mínima. Al andar, cada pisada da un golpe y la variación sube. Al correr, los golpes son más fuertes.
Ojo: medir no significa medir bien. No hay dos micro:bit que marquen igual y no es lo mismo llevarla en el bolsillo que en el tobillo. Por eso hay que entrenar el árbol con la micro:bit puesta donde se va a llevar.
Para el aula
- ¿Qué umbrales salen si la micro:bit va en el bolsillo y cuáles si va en la mano?
- ¿Dos micro:bit dan el mismo árbol? ¿Y dos alumnos con la misma micro:bit?
- ¿Cuántos segundos de cada movimiento hacen falta para que el árbol deje de cambiar?
- Guarda los datos en CSV y dibújalos en una hoja de cálculo. ¿Se ve la separación?
Retos
- Haz que funcione y comprueba el árbol con los tres movimientos.
- Cambia la micro:bit del bolsillo al tobillo y entrénala de nuevo.
- Compara tu árbol con el de un compañero.
- Cambia
index <= 49porindex <= 99: la ventana pasa de 1 a 2 segundos. ¿Mejora el árbol? - Convierte la actividad en una pequeña investigación: define la pregunta, recoge datos de toda la clase y saca conclusiones.
Qué hemos aprendido
Un árbol de decisión puede aprender de datos que recogemos nosotros. La micro:bit mide, la página prueba preguntas y se queda con las que mejor separan los movimientos.
También hemos visto que el árbol depende de cómo medimos: el mismo movimiento da otros números en el bolsillo o en el tobillo.
Para seguir investigando
- Cuentapasos con micro:bit y Bluetooth, otro uso del acelerómetro y de la conexión por Bluetooth.
- El nivel de burbuja, el acelerómetro de la micro:bit para medir inclinación.
- Simón dice con gestos, reconocer gestos con la micro:bit.
- Acelerómetro en Wikipedia, para saber cómo funciona el sensor.


