
Chismes del Lince 50: giroscopio arduino y el horizonte del Lince
Atardecer en Sierra Bermeja. Sobre el Peñón de los Reales, los buitres leonados dibujaban círculos enormes sin mover ni una pluma.
El Lince de la Noche los miraba desde una roca, con las orejas hacia delante y cara de envidia.
—Qué bien se les ve —suspiró—. No aletean ni una vez. Se dejan llevar por el aire caliente y ven toda la sierra de golpe.
La Gata Montesa subía por la pendiente con una mochila mucho más ligera que la suya.
—Lince, tú no vuelas.
—Ya lo sé.
—Ni vas a volar.
—¡Ya lo sé! —El Lince se levantó de un salto—. El Lince no vuela. Lo dije muy serio en la última reunión. Pero un lince con ideas no se rinde porque le falten alas.
La Gata levantó una ceja, que en una gata es un gesto muy elaborado.
—¿Y qué vas a hacer?
—Un aerodeslizador. Un cojín de aire, unas hélices y yo encima. Si no tengo alas, ¡tendré un vehículo!
—Muy bien. ¿Y cómo sabe un aerodeslizador si va derecho o torcido?
El Lince abrió la boca. La cerró. Miró a los buitres.
—Ellos lo notan con el cuerpo —dijo la Gata—. Tu aerodeslizador no tiene cuerpo: necesita un sensor. Si se inclina de más, acabas entre los pinsapos con las patas al aire.
—Un sensor que sepa cuánto está inclinado… —murmuró el Lince—. ¡Un horizonte!
—Un horizonte —asintió ella—. Y justo tengo uno en la mochila.
Sacó una placa Arduino Mega y una plaquita azul, más pequeña que un sello. Llevaba dentro un acelerómetro y un giroscopio.
—Hoy aprendemos a usar un giroscopio Arduino —dijo—. El aerodeslizador, otro día.
El Lince la miró con respeto.
—Gata, a veces me das miedo.
—Solo a veces.
Introducción al giroscopio Arduino
Hoy construimos un horizonte artificial: un cubo en la pantalla del móvil que se inclina exactamente igual que un sensor de verdad que tenemos en la mano. Es el chisme 50 y empezamos con el giroscopio Arduino, con un camino de datos poco habitual.
El sensor es un MPU-6050, que mide giros y aceleraciones. Un Arduino Mega lo lee y manda los números por el cable USB. En el ordenador, un programa de Python recoge esos números y los reparte por la red Wi-Fi. El móvil los recibe en una página web que dibuja los cubos.
Aprenderemos tres cosas: cómo se lee un giroscopio, por qué un solo sensor no basta y cómo se combinan dos sensores para medir mejor. Además, la página enseña en directo la deriva del giroscopio, algo que los libros cuentan y casi nunca se ve.
Es un prototipo educativo: sirve para entender la tecnología, no para pilotar nada de verdad. Por ahora, el aerodeslizador sigue siendo un dibujo.
Por qué este chisme del giroscopio Arduino
Saber cuánto está inclinado algo es una necesidad de muchos aparatos: un dron, un móvil que gira la pantalla, un coche que detecta un vuelco o un aerodeslizador que no quiere tirar a su piloto.
Ya hicimos algo parecido con el acelerómetro de la micro:bit en El nivel de burbuja. Funcionaba bien con la placa quieta, pero había un problema: si la mueves, el acelerómetro se confunde. Este chisme resuelve ese problema.
También sabemos que el móvil lleva dentro sensores de movimiento, como vimos en Los sentidos del Lince. Aquí sacamos el sensor fuera, lo conectamos a mano y vemos qué hace cada pieza.
Al terminar, el alumnado sabrá:
- leer un sensor por el bus I2C con un Arduino;
- mandar datos por el puerto serie y recogerlos en el ordenador;
- qué es un servidor web y por qué el móvil puede conectarse a él;
- qué es la deriva de un giroscopio;
- cómo combinar dos sensores imperfectos para obtener una medida mejor.
Y, de paso, una idea que repite toda la serie: medir no significa medir bien.
Cómo funciona el giroscopio Arduino
La pregunta es sencilla: ¿cómo puede un chip saber cuánto está inclinado? Hay dos maneras, y cada una tiene su fallo.
Primera manera: sentir la gravedad
El acelerómetro mide aceleraciones. Aunque esté quieto, nota siempre una: la gravedad, que tira hacia abajo con 1 g. Con la placa plana, casi toda la gravedad cae sobre el eje Z. Si la inclinas, se reparte entre los ejes, y con un poco de trigonometría sacamos el ángulo.
El fallo: no distingue entre la gravedad y un empujón. Si mueves la placa de golpe, el ángulo sale mal. Además, la medida tiembla un poco.
Segunda manera: contar cuánto gira
El giroscopio no mide el ángulo. Mide la velocidad de giro, en grados por segundo. Para saber cuánto ha girado hay que sumar: ángulo nuevo = ángulo anterior + velocidad × tiempo. Es suave y rápido, y no se deja engañar por empujones.
El fallo: cada suma arrastra un error minúsculo, y los errores se acumulan. Es la deriva. Con un error de solo 0,5 grados por segundo, en un minuto el cubo se ha ido 30 grados.
La solución: usar los dos
La Gata lo explica con una frase: fíate del giroscopio para lo rápido y del acelerómetro para lo lento. En cada medida, casi todo el ángulo viene del giroscopio y un poquito del acelerómetro corrige la deriva. Se llama filtro complementario:
\text{ángulo} = 0{,}98 \cdot (\text{ángulo anterior} + \text{giro} \cdot \Delta t) + 0{,}02 \cdot \text{ángulo del acelerómetro}
El 0,98 es la confianza en el giroscopio. En la página hay una barra para cambiarla y ver qué pasa en los dos extremos.
Por dentro, el MPU-6050 tiene minipiezas de silicio que se mueven. El chip mide ese movimiento y lo convierte en números. Se llama «MEMS», y es la misma tecnología que lleva el móvil.
Más información sobre el giroscopio y sobre el acelerómetro en Wikipedia.
Qué necesitamos para el giroscopio Arduino
Material
- Un Arduino Mega 2560 y su cable USB.
- Un módulo con el chip MPU-6050 (el nuestro es el HW-123, rotulado «ITG/MPU»).
- Cuatro cables para conectarlo: alimentación, tierra y los dos hilos del bus I2C.
- Un ordenador con Windows, y un móvil conectado a la misma red Wi-Fi que el ordenador.
Programas
- El IDE de Arduino, para programar el Mega.
- Thonny, que ya trae Python y la biblioteca
pyserialque lee el puerto serie. - Un navegador en el móvil. Vale cualquiera: esta página no necesita Bluetooth.
Los pines
Muchos módulos llegan con la tira de pines sin soldar. Si no puedes soldarla, funciona apretando los pines contra los agujeros, pero el contacto falla a ratos. Lo comprobaremos en el programa de prueba.

Construcción: conectar el giroscopio Arduino paso a paso
Antes del proyecto, una prueba. Si el sensor no responde, es mejor saberlo ahora que buscar el fallo entre cientos de líneas de código.
Paso 1: el cableado
Con el Arduino desenchufado, conecta cuatro cables:
| Módulo HW-123 | Arduino Mega |
|---|---|
| VCC | 3.3V (no al de 5V) |
| GND | GND |
| SDA | pin 20 (SDA) |
| SCL | pin 21 (SCL) |
Los pines AD0, INT, XDA y XCL se quedan sin conectar. En el Mega, SDA y SCL son los pines 20 y 21, no los A4 y A5 del Arduino Uno.
Paso 2: asegurar el contacto
Si la tira de pines no está soldada, aprieta cada pin contra su agujero. Si puedes, solda la tira: es lo más fiable.
Paso 3: el programa de prueba
Abre el IDE de Arduino y elige la placa Arduino Mega or Mega 2560 y su puerto COM. Copia este programa, súbelo y abre el Monitor Serie a 9600 baudios.
// Prueba del módulo HW-123 (ITG/MPU) con Arduino Mega
// Busca el módulo por I2C y le pregunta quién es (registro WHO_AM_I, 0x75).
// Repite la búsqueda cada 3 segundos: así puedes apretar o mover los cables viendo qué pasa.
//
// CONEXIONES (módulo -> Arduino Mega):
// VCC -> 3.3V (el pin 3.3V de la fila de alimentación; NO el de 5V)
// GND -> GND
// SDA -> pin 20 (SDA)
// SCL -> pin 21 (SCL)
// AD0, INT, XDA, XCL -> sin conectar
//
// Abre el Monitor Serie a 9600 baudios.
#include <Wire.h>
// Lee un registro del módulo (el "carnet de identidad" está en el 0x75)
byte leerRegistro(byte direccion, byte registro) {
Wire.beginTransmission(direccion);
Wire.write(registro);
Wire.endTransmission(false);
Wire.requestFrom((int)direccion, 1);
return Wire.available() ? Wire.read() : 0xFF;
}
void explicar(byte carnet) {
if (carnet == 0x68) Serial.println(F(" -> MPU-6050: giroscopio + acelerometro. El modulo funciona."));
else if (carnet == 0x70) Serial.println(F(" -> MPU-6500: giroscopio + acelerometro."));
else if (carnet == 0x71) Serial.println(F(" -> MPU-9250: giroscopio + acelerometro + brujula."));
else if (carnet == 0xFF) Serial.println(F(" -> No responde bien: contacto flojo en SDA o SCL."));
else Serial.println(F(" -> Numero que no conozco: apuntalo y dimelo."));
}
void buscar() {
int encontrados = 0;
for (byte dir = 1; dir < 127; dir++) {
Wire.beginTransmission(dir);
byte error = Wire.endTransmission();
if (error == 0) {
encontrados++;
Serial.print(F("Dispositivo en la direccion 0x"));
Serial.println(dir, HEX);
if (dir == 0x68 || dir == 0x69) {
byte carnet = leerRegistro(dir, 0x75);
Serial.print(F(" carnet (registro 0x75) = 0x"));
Serial.println(carnet, HEX);
explicar(carnet);
}
}
}
if (encontrados == 0) {
Serial.println(F("No encuentro nada. Revisa VCC a 3.3V, GND, SDA al pin 20 y SCL al pin 21."));
} else if (encontrados > 8) {
Serial.println(F("BUS BLOQUEADO: contestan casi todas las direcciones. SDA o SCL estan pegados a GND."));
}
Serial.println(F("---"));
}
void setup() {
Serial.begin(9600);
delay(500);
Serial.println(F("Prueba del modulo HW-123 (ITG/MPU)"));
// Paso 1: las lineas SDA y SCL deben leer 1 (con el modulo conectado)
pinMode(SDA, INPUT_PULLUP);
pinMode(SCL, INPUT_PULLUP);
delay(10);
Serial.print(F("Linea SDA (pin 20): ")); Serial.println(digitalRead(SDA));
Serial.print(F("Linea SCL (pin 21): ")); Serial.println(digitalRead(SCL));
Serial.println(F("(1 = bien, 0 = esa linea esta pegada a GND)"));
Serial.println(F("---"));
Wire.begin();
#if defined(WIRE_HAS_TIMEOUT)
Wire.setWireTimeout(3000, true); // si el bus se bloquea, no se queda colgado
#endif
}
void loop() {
buscar();
delay(3000);
}
Paso 4: leer el resultado
Si todo va bien, verás esto:
Dispositivo en la direccion 0x68
carnet (registro 0x75) = 0x68
-> MPU-6050: giroscopio + acelerometro. El modulo funciona.
Si no, esta tabla te dice qué revisar:
| Lo que sale | Qué significa | Qué hacer |
|---|---|---|
| No encuentro nada | El módulo no contesta | Apretar los pines, revisar VCC a 3.3V y que SDA vaya al 20 y SCL al 21 |
| SDA o SCL marcan 0 | Una línea está pegada a GND | Buscar cables o pines que se toquen |
| BUS BLOQUEADO | Contestan casi todas las direcciones | Lo mismo: SDA o SCL a GND |
| carnet 0x70 o 0x71 | Es un MPU-6500 o MPU-9250 | Sirve, pero el código cambiaría un poco |
| carnet 0xFF | Responde con contacto flojo | Soldar o apretar los pines |
El número 0x68 es la dirección del sensor en el bus I2C, y el 0x75 es el registro donde el chip dice quién es. Es como pedirle el carnet de identidad.

El código del giroscopio Arduino: lee el sensor y habla por USB
Este programa va en el Arduino Mega. Hace cuatro cosas: despierta al MPU-6050, calibra el giroscopio, lee el sensor 50 veces por segundo y manda cada lectura por el cable USB como una línea de texto.
Guárdalo en una carpeta llamada chisme50_horizonte_mega, con el archivo chisme50_horizonte_mega.ino dentro: el IDE de Arduino exige que la carpeta y el programa se llamen igual. No hay que cambiar nada. Elige la placa Arduino Mega or Mega 2560 y súbelo.
Cada línea que manda tiene siete números separados por comas: tiempo, ax, ay, az, gx, gy, gz. Los tres primeros son la aceleración en g; los tres últimos, el giro en grados por segundo.
// Chisme 50 - El horizonte del Lince (parte 1: el Arduino Mega)
// Lee el MPU-6050 (acelerometro + giroscopio) y manda los datos por el cable USB.
//
// CONEXIONES (las mismas de la prueba):
// MPU-6050 VCC -> 3.3V GND -> GND
// SDA -> pin 20 SCL -> pin 21
//
// QUE MANDA: una linea de texto 50 veces por segundo, con 7 numeros separados por comas:
// tiempo_ms, ax, ay, az, gx, gy, gz
// - ax, ay, az: aceleracion en "g" (1 g = lo que nos tira la gravedad hacia abajo)
// - gx, gy, gz: velocidad de giro en grados por segundo
//
// IMPORTANTE: Monitor Serie a 115200 baudios. Y cuando uses chisme50-puente.py, CIERRA el Monitor Serie
// (solo un programa puede tener abierto el puerto a la vez).
#include <Wire.h>
const uint8_t MPU = 0x68; // direccion del MPU-6050 (la que encontro la prueba)
// ===== Escalas del sensor (las de fabrica) =====
// Acelerometro en +-2 g -> 16384 cuentas por cada g
// Giroscopio en +-250 grados/s -> 131 cuentas por cada grado/segundo
const float CUENTAS_POR_G = 16384.0;
const float CUENTAS_POR_GPS = 131.0;
// ===== Calibracion del giroscopio =====
// Un giroscopio quieto deberia dar 0, pero siempre da un poquito (su "sesgo").
// Al arrancar lo medimos y lo restamos.
float sesgoGx = 0, sesgoGy = 0, sesgoGz = 0;
unsigned long ultimoEnvio = 0;
const unsigned long CADA_MS = 20; // 20 ms = 50 mediciones por segundo
// Pide al sensor 14 bytes seguidos (desde el registro 0x3B) y los convierte en 7 numeros:
// aceleracion x,y,z, temperatura, giro x,y,z
bool leerCrudo(int16_t *v) {
Wire.beginTransmission(MPU);
Wire.write(0x3B); // primer registro de datos
if (Wire.endTransmission(false) != 0) return false;
if (Wire.requestFrom((int)MPU, 14) != 14) return false;
for (int i = 0; i < 7; i++) {
int16_t alto = Wire.read(); // cada numero viene en 2 bytes: primero el alto
int16_t bajo = Wire.read();
v[i] = (alto << 8) | bajo;
}
return true;
}
void escribirRegistro(uint8_t registro, uint8_t valor) {
Wire.beginTransmission(MPU);
Wire.write(registro);
Wire.write(valor);
Wire.endTransmission();
}
void setup() {
Serial.begin(115200);
Wire.begin();
escribirRegistro(0x6B, 0x00); // el MPU-6050 arranca "dormido": lo despertamos
escribirRegistro(0x1A, 0x03); // filtro interno: quita temblores de mas de ~44 Hz
Serial.println("# Calibrando el giroscopio: NO muevas el sensor (2 segundos)");
delay(500);
// Media de 200 lecturas con el sensor quieto
long sumaX = 0, sumaY = 0, sumaZ = 0;
int buenas = 0;
int16_t v[7];
for (int i = 0; i < 200; i++) {
if (leerCrudo(v)) {
sumaX += v[4];
sumaY += v[5];
sumaZ += v[6];
buenas++;
}
delay(5);
}
if (buenas > 0) {
sesgoGx = sumaX / (float)buenas / CUENTAS_POR_GPS;
sesgoGy = sumaY / (float)buenas / CUENTAS_POR_GPS;
sesgoGz = sumaZ / (float)buenas / CUENTAS_POR_GPS;
Serial.print("# Sesgo del giroscopio (grados/s): ");
Serial.print(sesgoGx, 2); Serial.print(", ");
Serial.print(sesgoGy, 2); Serial.print(", ");
Serial.println(sesgoGz, 2);
} else {
Serial.println("# ERROR: el MPU-6050 no responde. Revisa el cableado.");
}
Serial.println("# Listo");
}
void loop() {
unsigned long ahora = millis();
if (ahora - ultimoEnvio < CADA_MS) return; // todavia no toca
ultimoEnvio = ahora;
int16_t v[7];
if (!leerCrudo(v)) {
Serial.println("# error de lectura");
return;
}
// Convertimos las "cuentas" del sensor a unidades de verdad
float ax = v[0] / CUENTAS_POR_G;
float ay = v[1] / CUENTAS_POR_G;
float az = v[2] / CUENTAS_POR_G;
float gx = v[4] / CUENTAS_POR_GPS - sesgoGx;
float gy = v[5] / CUENTAS_POR_GPS - sesgoGy;
float gz = v[6] / CUENTAS_POR_GPS - sesgoGz;
// Una linea: tiempo,ax,ay,az,gx,gy,gz
Serial.print(ahora); Serial.print(',');
Serial.print(ax, 3); Serial.print(',');
Serial.print(ay, 3); Serial.print(',');
Serial.print(az, 3); Serial.print(',');
Serial.print(gx, 2); Serial.print(',');
Serial.print(gy, 2); Serial.print(',');
Serial.println(gz, 2);
}
Lee y predice
- Si quitamos la calibración del arranque y dejamos el sensor quieto, ¿el giroscopio marcará exactamente cero?
- Hemos puesto 50 mediciones por segundo (una cada 20 ms). ¿Qué pasaría con la deriva si mandáramos solo 5?
Si quieres comprobar que el Arduino manda datos antes de seguir, abre el Monitor Serie a 115200 baudios: verás pasar las líneas. Ábrelo solo para mirar y ciérralo después.
Del cable USB al móvil: el puente en Python
El Arduino Mega no tiene Wi-Fi ni Bluetooth, pero el ordenador sí tiene Wi-Fi. Entonces, ¿cómo llegan los datos al móvil? La Gata lo resuelve: el ordenador hace de puente.
Un programa de Python, chisme50-puente.py, hace tres cosas. Lee el puerto serie del Arduino. Abre una página web en el ordenador. Y reparte cada línea de datos a todos los navegadores que estén mirando esa página, el del PC y el del móvil.
Para repartir los datos usa una técnica llamada eventos del servidor. El navegador abre una conexión y se queda esperando; el programa empuja los datos por ella en cuanto llegan. Es como un grifo abierto: el móvil no tiene que pedirlos cada vez.
[embed: node/28a1ae96-52a3]De izquierda a derecha: el sensor habla con el Arduino por I2C, el Arduino con el ordenador por USB y el ordenador con el móvil por Wi-Fi.
Preparar el puente
- Guarda
chisme50-puente.pyychisme50-horizonte.htmlen la misma carpeta. - Comprueba que Thonny tiene la biblioteca
pyserial. Si no la tiene, ve a Herramientas → Administrar paquetes, buscapyseriale instálala. - Copia este programa en un archivo llamado
chisme50-puente.py.
# Chisme 50 - El horizonte del Lince (parte 2: el puente en el ordenador)
#
# Este programa hace de "puente" entre el Arduino y el movil:
# 1) lee por el cable USB (puerto serie) las lineas que manda el Arduino Mega
# 2) abre una pagina web en el ordenador, por la red Wi-Fi
# 3) manda cada linea a todos los navegadores que esten mirando (PC y movil)
#
# COMO USARLO
# - Deja este archivo y chisme50-horizonte.html en la MISMA carpeta.
# - Necesitas la biblioteca pyserial: pip install pyserial (Thonny ya la trae)
# - Cierra el Monitor Serie del IDE de Arduino (solo uno puede usar el puerto).
# - Ejecuta: python chisme50-puente.py
# o con puerto: python chisme50-puente.py --puerto COM5
# o SIN Arduino: python chisme50-puente.py --simular (datos inventados, para probar)
# - El programa escribe una direccion como http://192.168.1.35:8000
# Abrela en el navegador del movil (con el movil en la MISMA red Wi-Fi que el PC).
import argparse
import math
import os
import queue
import random
import socket
import sys
import threading
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
CARPETA = os.path.dirname(os.path.abspath(__file__))
# Aqui guardamos una "cola" por cada navegador conectado
clientes = []
cerrojo = threading.Lock()
def difundir(linea):
"""Reparte una linea de datos a todos los navegadores conectados."""
with cerrojo:
for cola in clientes:
try:
cola.put_nowait(linea)
except queue.Full:
pass # un navegador muy lento se pierde algun dato; no frena a los demas
class Servidor(BaseHTTPRequestHandler):
def log_message(self, *args):
pass # no llenamos la consola de mensajes
def do_GET(self):
if self.path in ("/", "/index.html", "/chisme50-horizonte.html"):
self.enviar_pagina()
elif self.path == "/datos":
self.enviar_datos()
else:
self.send_error(404)
def enviar_pagina(self):
ruta = os.path.join(CARPETA, "chisme50-horizonte.html")
try:
with open(ruta, "rb") as f:
contenido = f.read()
except OSError:
self.send_error(404, "Falta chisme50-horizonte.html junto a chisme50-puente.py")
return
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(contenido)))
self.send_header("Cache-Control", "no-cache")
self.end_headers()
self.wfile.write(contenido)
def enviar_datos(self):
# "Server-Sent Events": una conexion que se queda abierta y por la que
# vamos empujando lineas de texto al navegador segun llegan.
self.send_response(200)
self.send_header("Content-Type", "text/event-stream")
self.send_header("Cache-Control", "no-cache")
self.send_header("Connection", "keep-alive")
self.end_headers()
cola = queue.Queue(maxsize=300)
with cerrojo:
clientes.append(cola)
try:
while True:
try:
linea = cola.get(timeout=10)
self.wfile.write(("data: " + linea + "\n\n").encode("utf-8"))
except queue.Empty:
self.wfile.write(b": sigo aqui\n\n") # latido para que no se corte
self.wfile.flush()
except OSError:
pass # el navegador se ha ido
finally:
with cerrojo:
clientes.remove(cola)
def ip_local():
"""Averigua la direccion del PC en la red (no envia nada, solo pregunta la ruta)."""
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.connect(("10.255.255.255", 1))
return s.getsockname()[0]
except OSError:
return "127.0.0.1"
finally:
s.close()
def buscar_arduino():
from serial.tools import list_ports
for p in list_ports.comports():
texto = ((p.description or "") + " " + (p.manufacturer or "")).lower()
if p.vid in (0x2341, 0x2A03, 0x1A86, 0x0403) or "arduino" in texto:
return p.device
return None
def leer_serie(puerto):
import serial
try:
with serial.Serial(puerto, 115200, timeout=1) as s:
print("Leyendo el Arduino en", puerto)
time.sleep(2) # el Arduino se reinicia solo al abrir el puerto
while True:
linea = s.readline().decode("utf-8", "ignore").strip()
if linea:
difundir(linea)
except serial.SerialException as e:
print("\nSe ha perdido el puerto serie:", e)
print("Comprueba el cable y que el Monitor Serie del IDE este cerrado.")
os._exit(1)
def simular():
"""Datos inventados: un sensor que se balancea y un giroscopio con un poco de sesgo
(asi se ve la deriva sin necesidad de tener el Arduino conectado)."""
print("MODO SIMULACION: los datos son inventados.")
t0 = time.time()
d2r = math.pi / 180
while True:
t = time.time() - t0
roll = 30 * math.sin(2 * math.pi * t / 6)
pitch = 20 * math.sin(2 * math.pi * t / 9 + 1)
gx = 30 * (2 * math.pi / 6) * math.cos(2 * math.pi * t / 6) + 0.8 + random.gauss(0, 0.2)
gy = 20 * (2 * math.pi / 9) * math.cos(2 * math.pi * t / 9 + 1) - 0.5 + random.gauss(0, 0.2)
ax = -math.sin(pitch * d2r) + random.gauss(0, 0.02)
ay = math.sin(roll * d2r) * math.cos(pitch * d2r) + random.gauss(0, 0.02)
az = math.cos(roll * d2r) * math.cos(pitch * d2r) + random.gauss(0, 0.02)
difundir("%d,%.3f,%.3f,%.3f,%.2f,%.2f,%.2f" % (t * 1000, ax, ay, az, gx, gy, random.gauss(0, 0.2)))
time.sleep(0.02)
def main():
ap = argparse.ArgumentParser(description="Puente Arduino -> navegador (PC y movil)")
ap.add_argument("--puerto", help="puerto serie del Arduino, por ejemplo COM5")
ap.add_argument("--simular", action="store_true", help="datos inventados, sin Arduino")
ap.add_argument("--web", type=int, default=8000, help="puerto de la pagina (por defecto 8000)")
a = ap.parse_args()
if a.simular:
threading.Thread(target=simular, daemon=True).start()
else:
try:
import serial # noqa: F401
from serial.tools import list_ports
except ImportError:
sys.exit("Falta pyserial. Instalalo con: pip install pyserial")
puerto = a.puerto or buscar_arduino()
if not puerto:
print("No encuentro el Arduino. Puertos que veo:")
for p in list_ports.comports():
print(" ", p.device, "-", p.description)
sys.exit("Vuelve a ejecutar con: python chisme50-puente.py --puerto COMx")
threading.Thread(target=leer_serie, args=(puerto,), daemon=True).start()
servidor = ThreadingHTTPServer(("0.0.0.0", a.web), Servidor)
servidor.daemon_threads = True
print()
print("Abre esta direccion en el movil (misma red Wi-Fi):")
print(" http://%s:%d" % (ip_local(), a.web))
print("En este ordenador: http://localhost:%d" % a.web)
print("(Ctrl+C para parar)")
try:
servidor.serve_forever()
except KeyboardInterrupt:
print("\nAdios")
if __name__ == "__main__":
main()
Qué hace cada parte
leer_serieabre el puerto del Arduino a 115200 baudios y lee línea a línea.difundirmete cada línea en la cola de cada navegador conectado.Servidorentrega la página (/) y mantiene abierta la conexión de datos (/datos).simularinventa datos de un sensor que se balancea, para probar sin Arduino.ip_localaverigua la dirección del ordenador para que la teclees en el móvil.
Qué datos salen de tu ordenador
Ninguno sale a internet. Los datos viajan solo por tu red local. Mientras el programa esté abierto, cualquier dispositivo de la misma red que conozca la dirección podría ver los cubos. En el instituto, cierra el programa al terminar. Los datos no se guardan en ningún sitio.
La página web del giroscopio Arduino: tres cubos y un filtro
La página recibe las líneas del Arduino y calcula la inclinación de tres maneras a la vez: solo con el acelerómetro, solo con el giroscopio y con los dos combinados. Cada manera mueve su propio cubo, y una gráfica de los últimos 10 segundos las compara.
Guárdala como chisme50-horizonte.html, en la misma carpeta que chisme50-puente.py. No la abras haciendo doble clic: se abre escribiendo en el navegador la dirección que muestra el puente, porque necesita su conexión de datos.
Por esa razón tampoco puede ir a la zona de apps de MonteSteam, como otros chismes. Una página publicada con https no puede hablar con una dirección local, así que aquí el ordenador es el servidor.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>El horizonte del Lince</title>
<style>
:root {
--fondo: #f6f8f7;
--tarjeta: #ffffff;
--texto: #1c2b27;
--suave: #5d6f69;
--borde: #d7e0dc;
--acc: #8a97a0; /* acelerómetro: gris */
--gir: #e0762b; /* giroscopio: naranja */
--fus: #0f8f7e; /* combinado: verde azulado */
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--fondo); color: var(--texto);
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; line-height: 1.4; }
main { max-width: 980px; margin: 0 auto; padding: 16px; }
h1 { font-size: 1.5rem; margin: 4px 0 2px; }
.sub { color: var(--suave); margin: 0 0 12px; font-size: .95rem; }
#estado { display: inline-block; padding: 4px 10px; border-radius: 999px; font-size: .85rem;
background: #fff3d6; border: 1px solid #ecd48f; }
#estado.ok { background: #dff3ec; border-color: #9fd4c3; }
.cubos { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin: 14px 0; }
.tarjeta { background: var(--tarjeta); border: 1px solid var(--borde); border-radius: 12px; padding: 10px; }
.tarjeta h2 { font-size: 1rem; margin: 0; }
.tarjeta p { margin: 2px 0 0; font-size: .82rem; color: var(--suave); }
.raya { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 6px; }
.escena { height: 190px; perspective: 700px; display: flex; align-items: center; justify-content: center; }
.camara { transform-style: preserve-3d; transform: rotateX(-26deg) rotateY(-28deg); }
.cuerpo { position: relative; width: 0; height: 0; transform-style: preserve-3d; }
/* El sensor es una placa: 140 (eje X) x 18 (eje Z) x 100 (eje Y) */
.cara { position: absolute; display: flex; align-items: center; justify-content: center;
font-size: 12px; font-weight: 600; color: #fff; border: 1px solid rgba(0,0,0,.25);
backface-visibility: visible; }
.arriba, .abajo { width: 140px; height: 100px; margin: -50px 0 0 -70px; }
.frente, .atras { width: 140px; height: 18px; margin: -9px 0 0 -70px; }
.izq, .der { width: 100px; height: 18px; margin: -9px 0 0 -50px; }
.arriba { transform: rotateX(90deg) translateZ(9px); }
.abajo { transform: rotateX(-90deg) translateZ(9px); }
.frente { transform: translateZ(50px); }
.atras { transform: rotateY(180deg) translateZ(50px); }
.der { transform: rotateY(90deg) translateZ(70px); }
.izq { transform: rotateY(-90deg) translateZ(70px); }
.gris .cara { background: #8a97a0; } .gris .arriba { background: #9fabb3; }
.naranja .cara { background: #e0762b; } .naranja .arriba { background: #ee9150; }
.verde .cara { background: #0f8f7e; } .verde .arriba { background: #2fae9c; }
.datos { font-variant-numeric: tabular-nums; font-size: .9rem; margin-top: 4px; }
.datos b { font-weight: 650; }
.mandos { display: flex; flex-wrap: wrap; gap: 14px 24px; align-items: center; margin: 6px 0 10px;
background: var(--tarjeta); border: 1px solid var(--borde); border-radius: 12px; padding: 10px 12px; }
.mandos label { font-size: .9rem; }
button { font: inherit; padding: 6px 12px; border-radius: 8px; border: 1px solid var(--borde);
background: #fff; cursor: pointer; }
button:hover { background: #eef4f1; }
input[type=range] { width: 200px; vertical-align: middle; }
#deriva { font-variant-numeric: tabular-nums; }
canvas { width: 100%; height: 220px; background: #fff; border: 1px solid var(--borde); border-radius: 12px; display: block; }
.leyenda { font-size: .85rem; color: var(--suave); margin: 6px 2px 14px; display: flex; gap: 16px; flex-wrap: wrap; }
.nota { font-size: .88rem; color: var(--suave); }
@media (max-width: 720px) {
.cubos { grid-template-columns: 1fr; }
.escena { height: 170px; }
}
</style>
</head>
<body>
<main>
<h1>El horizonte del Lince</h1>
<p class="sub">Tres maneras de saber cómo está inclinado el sensor: con el acelerómetro, con el giroscopio, o con los dos a la vez.</p>
<span id="estado">Conectando con el puente…</span>
<div class="cubos">
<section class="tarjeta">
<h2><span class="raya" style="background:var(--acc)"></span>Solo acelerómetro</h2>
<p>Mira hacia dónde tira la gravedad.</p>
<div class="escena"><div class="camara"><div class="cuerpo gris" id="c-acc">
<div class="cara arriba">MPU-6050</div><div class="cara abajo"></div>
<div class="cara frente"></div><div class="cara atras"></div>
<div class="cara izq"></div><div class="cara der"></div>
</div></div></div>
<div class="datos">Roll <b id="a-roll">0.0</b>° · Pitch <b id="a-pitch">0.0</b>°</div>
</section>
<section class="tarjeta">
<h2><span class="raya" style="background:var(--gir)"></span>Solo giroscopio</h2>
<p>Suma cuánto ha girado en cada instante.</p>
<div class="escena"><div class="camara"><div class="cuerpo naranja" id="c-gir">
<div class="cara arriba">MPU-6050</div><div class="cara abajo"></div>
<div class="cara frente"></div><div class="cara atras"></div>
<div class="cara izq"></div><div class="cara der"></div>
</div></div></div>
<div class="datos">Roll <b id="g-roll">0.0</b>° · Pitch <b id="g-pitch">0.0</b>°</div>
</section>
<section class="tarjeta">
<h2><span class="raya" style="background:var(--fus)"></span>Los dos combinados</h2>
<p>El giroscopio manda, la gravedad corrige.</p>
<div class="escena"><div class="camara"><div class="cuerpo verde" id="c-fus">
<div class="cara arriba">MPU-6050</div><div class="cara abajo"></div>
<div class="cara frente"></div><div class="cara atras"></div>
<div class="cara izq"></div><div class="cara der"></div>
</div></div></div>
<div class="datos">Roll <b id="f-roll">0.0</b>° · Pitch <b id="f-pitch">0.0</b>°</div>
</section>
</div>
<div class="mandos">
<label>Confianza en el giroscopio: <b id="alfa-txt">0.98</b><br>
<input type="range" id="alfa" min="0" max="0.995" step="0.005" value="0.98"></label>
<button id="recentrar">Recentrar giroscopio</button>
<label><input type="checkbox" id="inv-roll"> invertir roll</label>
<label><input type="checkbox" id="inv-pitch"> invertir pitch</label>
<label>Gráfica:
<select id="eje"><option value="roll">Roll</option><option value="pitch">Pitch</option></select></label>
<span>Deriva del giroscopio: <b id="deriva">0.0</b>°</span>
</div>
<canvas id="grafica"></canvas>
<div class="leyenda">
<span><span class="raya" style="background:var(--acc)"></span>Acelerómetro (tiembla)</span>
<span><span class="raya" style="background:var(--gir)"></span>Giroscopio (se va desviando)</span>
<span><span class="raya" style="background:var(--fus)"></span>Combinado</span>
<span id="tasa"></span>
</div>
<p class="nota">Roll = inclinar hacia los lados · Pitch = inclinar hacia delante o atrás. Si el cubo se mueve al revés que el sensor,
marca «invertir». Con la confianza a 1 solo manda el giroscopio; a 0, solo el acelerómetro.</p>
</main>
<script>
const R2D = 180 / Math.PI;
const $ = id => document.getElementById(id);
// ---- Ángulos de cada método (en grados) ----
let acc = { roll: 0, pitch: 0 };
let gir = { roll: 0, pitch: 0 };
let fus = { roll: 0, pitch: 0 };
let alfa = 0.98; // confianza en el giroscopio
let ultimoT = null; // tiempo de la muestra anterior (ms, el que manda el Arduino)
let hayDatos = false;
let ultimaLlegada = 0;
let contador = 0; // muestras recibidas en el último segundo
let historial = []; // para la gráfica: {t, acc, gir, fus} del eje elegido (en segundos)
function estado(texto, ok) {
$("estado").textContent = texto;
$("estado").className = ok ? "ok" : "";
}
// Ángulo según el acelerómetro: la gravedad "cae" hacia abajo y nos dice la inclinación
function anguloAcelerometro(ax, ay, az) {
return {
roll: Math.atan2(ay, az) * R2D,
pitch: Math.atan2(-ax, Math.hypot(ay, az)) * R2D
};
}
function recentrar(a) {
// Empezamos (o volvemos a empezar) el giroscopio desde el ángulo que dice el acelerómetro
gir.roll = fus.roll = a.roll;
gir.pitch = fus.pitch = a.pitch;
}
let ultimoAcc = { roll: 0, pitch: 0 };
function procesar(linea) {
if (linea.charAt(0) === "#") { estado(linea.slice(1).trim(), false); return; }
const p = linea.split(",").map(Number);
if (p.length !== 7 || p.some(isNaN)) return; // línea rota: la ignoramos
const [t, ax, ay, az, gx, gy] = p;
ultimaLlegada = performance.now();
contador++;
const a = anguloAcelerometro(ax, ay, az);
acc = a; ultimoAcc = a;
if (ultimoT === null || t < ultimoT) { // primera muestra, o el Arduino se reinició
ultimoT = t;
recentrar(a);
historial = [];
return;
}
const dt = (t - ultimoT) / 1000; // segundos entre dos muestras
ultimoT = t;
if (dt <= 0 || dt > 0.5) return;
// Giroscopio solo: sumamos (velocidad de giro × tiempo). Los pequeños errores también se suman.
gir.roll += gx * dt;
gir.pitch += gy * dt;
// Filtro complementario: casi todo del giroscopio + un poquito del acelerómetro en cada paso
fus.roll = alfa * (fus.roll + gx * dt) + (1 - alfa) * a.roll;
fus.pitch = alfa * (fus.pitch + gy * dt) + (1 - alfa) * a.pitch;
hayDatos = true;
const eje = $("eje").value;
historial.push({ t: performance.now() / 1000, a: a[eje], g: gir[eje], f: fus[eje] });
const corte = performance.now() / 1000 - 10;
while (historial.length && historial[0].t < corte) historial.shift();
}
// ---- Dibujo de los cuerpos 3D ----
// Mapa de ejes: X del sensor = derecha de la pantalla, Y = hacia el fondo, Z = hacia arriba.
function girar(id, ang) {
const roll = ang.roll * ($("inv-roll").checked ? -1 : 1);
const pitch = ang.pitch * ($("inv-pitch").checked ? -1 : 1);
$(id).style.transform = "rotateZ(" + pitch.toFixed(1) + "deg) rotateX(" + (-roll).toFixed(1) + "deg)";
}
function texto(pref, ang) {
$(pref + "-roll").textContent = ang.roll.toFixed(1);
$(pref + "-pitch").textContent = ang.pitch.toFixed(1);
}
// ---- Gráfica ----
const lienzo = $("grafica");
const ctx = lienzo.getContext("2d");
function color(v) { return getComputedStyle(document.documentElement).getPropertyValue(v).trim(); }
function dibujarGrafica() {
const dpr = window.devicePixelRatio || 1;
const w = lienzo.clientWidth, h = lienzo.clientHeight;
if (lienzo.width !== Math.round(w * dpr)) { lienzo.width = Math.round(w * dpr); lienzo.height = Math.round(h * dpr); }
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, w, h);
const izq = 40, der = 8, arr = 10, aba = 22;
const X = (t, ahora) => izq + (t - (ahora - 10)) / 10 * (w - izq - der);
const Y = v => arr + (90 - Math.max(-90, Math.min(90, v))) / 180 * (h - arr - aba);
ctx.font = "11px system-ui, sans-serif";
ctx.fillStyle = "#5d6f69"; ctx.strokeStyle = "#e3eae7"; ctx.lineWidth = 1;
for (const v of [-90, -45, 0, 45, 90]) {
ctx.beginPath(); ctx.moveTo(izq, Y(v)); ctx.lineTo(w - der, Y(v)); ctx.stroke();
ctx.fillText(v + "°", 6, Y(v) + 4);
}
ctx.fillText("últimos 10 segundos", izq, h - 6);
if (historial.length < 2) return;
const ahora = performance.now() / 1000;
const series = [["a", "--acc"], ["g", "--gir"], ["f", "--fus"]];
for (const [k, c] of series) {
ctx.beginPath(); ctx.strokeStyle = color(c); ctx.lineWidth = k === "f" ? 2.4 : 1.6;
historial.forEach((m, i) => { const x = X(m.t, ahora), y = Y(m[k]); i ? ctx.lineTo(x, y) : ctx.moveTo(x, y); });
ctx.stroke();
}
}
// ---- Bucle de dibujo (una vez por fotograma, no 50 veces por segundo) ----
function fotograma() {
girar("c-acc", acc); girar("c-gir", gir); girar("c-fus", fus);
texto("a", acc); texto("g", gir); texto("f", fus);
$("deriva").textContent = (gir.roll - fus.roll).toFixed(1);
dibujarGrafica();
if (hayDatos && performance.now() - ultimaLlegada > 2000) {
estado("Conectado, pero no llegan datos del Arduino (¿cable? ¿Monitor Serie abierto?)", false);
hayDatos = false;
}
requestAnimationFrame(fotograma);
}
requestAnimationFrame(fotograma);
setInterval(() => {
$("tasa").textContent = contador ? contador + " datos/s" : "";
if (contador && $("estado").className !== "ok") estado("Recibiendo datos del sensor", true);
contador = 0;
}, 1000);
// ---- Mandos ----
$("alfa").addEventListener("input", e => { alfa = Number(e.target.value); $("alfa-txt").textContent = alfa.toFixed(3).replace(/0$/, ""); });
$("recentrar").addEventListener("click", () => recentrar(ultimoAcc));
$("eje").addEventListener("change", () => { historial = []; });
// ---- Conexión con chisme50-puente.py ----
const fuente = new EventSource("/datos");
fuente.onopen = () => estado("Conectado al puente. Esperando datos…", false);
fuente.onerror = () => estado("Sin conexión con el puente (¿sigue abierto chisme50-puente.py?)", false);
fuente.onmessage = e => procesar(e.data);
</script>
</body>
</html>
Qué hace cada parte
anguloAcelerometrocalcula el ángulo con la gravedad (arcotangente de los ejes).procesarrecibe cada línea, suma el giro (gir.roll += gx * dt) y aplica el filtro complementario (fus.roll = alfa * ...).girarpone cada cubo en su ángulo con una transformación 3D de CSS.dibujarGraficapinta las tres curvas de los últimos 10 segundos.EventSourcees lo que conecta la página con el puente.
Cambios guiados: prueba a cambiar el 0.98 inicial (let alfa) por 0.9 y por 0.999. ¿Qué cubo se parece más al del acelerómetro? ¿Y cuál empieza a derivar?
Cómo probar el giroscopio Arduino paso a paso
Tienes ya el Arduino programado, el puente y la página. Ahora hay que ponerlos a trabajar juntos, en este orden.
- Cierra el IDE de Arduino del todo. Solo un programa puede tener abierto el puerto serie, y el IDE lo mantiene ocupado mientras el Monitor Serie esté abierto.
- Deja el sensor quieto y enchufa el Arduino. Durante dos segundos calibra el giroscopio.
- Abre Thonny y mira abajo a la derecha. Si pone algo como «MicroPython (Raspberry Pi Pico)», haz clic ahí y elige Local Python 3. Si no, Thonny intentará hablar con la Pico y no te dejará ejecutar el programa.
- Abre
chisme50-puente.pyy pulsa Run (el triángulo verde). Si Windows pregunta por el cortafuegos, permite el acceso en redes privadas. - En la consola verás «Leyendo el Arduino en COM5» (el número puede ser otro) y una dirección parecida a
http://192.168.1.35:8000. - Abre esa dirección en el navegador del ordenador (también sirve
http://localhost:8000). Si ves los cubos moverse, todo funciona. - Abre la misma dirección en el móvil, conectado a la misma red Wi-Fi que el ordenador.
Si quieres ensayar antes de tener el sensor a mano, ejecuta el puente con el argumento --simular. En Thonny se pone en el menú Ver → Argumentos del programa (en inglés, View → Program arguments). También puedes escribir python chisme50-puente.py --simular en una consola.
Qué debería pasar
- Con el sensor quieto, los tres cubos están iguales.
- Si lo inclinas despacio, los tres lo siguen.
- Si lo sacudes o lo deslizas rápido por la mesa, el cubo gris del acelerómetro se descontrola.
- Si lo dejas mucho rato quieto, el número de Deriva del giroscopio va creciendo poco a poco. Cuánto, depende de tu sensor. Pulsa Recentrar para empezar de nuevo.
Si algo falla
| Lo que ves | Qué pasa | Qué hacer |
|---|---|---|
| Falta pyserial | No está la biblioteca | Thonny → Herramientas → Administrar paquetes → instalar pyserial |
| PermissionError 13, «Acceso denegado» | Otro programa tiene el puerto | Cierra el IDE de Arduino y cualquier ejecución anterior del puente (botón Stop de Thonny). Desenchufa y vuelve a enchufar el Arduino |
| No encuentro el Arduino | El programa no adivina el puerto | Mira el número en el Administrador de dispositivos, sección Puertos (COM y LPT), y ejecuta con --puerto COM5 |
| Thonny no te deja ejecutar | Está configurado para la Pico | Cambia el intérprete a Local Python 3 |
| El móvil no abre la dirección | No se ven en la red | Comprueba que están en la misma Wi-Fi. Algunas redes de centros impiden que dos aparatos se vean: conecta el ordenador al punto de acceso del propio móvil |
| «Conectado, pero no llegan datos» | El Arduino no envía | Revisa el cable USB y que el Monitor Serie esté cerrado |
| El cubo gira al revés | Depende de cómo pongas el sensor | Marca «invertir roll» o «invertir pitch» |
Qué estamos midiendo realmente con el giroscopio Arduino
Los cubos parecen medir la inclinación. En realidad el sensor no mide ningún ángulo.
Lo que mide el MPU-6050 son dos cosas: aceleración, en g, y velocidad de giro, en grados por segundo. Los ángulos los calcula nuestro programa a partir de esos datos. Es la diferencia entre un dato (lo que sale del sensor) y una medida (lo que deducimos de él).
Resolución, ruido y sesgo
Con las escalas de fábrica, el sensor da 16384 cuentas por cada g y 131 por cada grado por segundo. Esa es su resolución. Pero no toda cifra que sale es fiable: la medida tiembla (ruido) y el giroscopio quieto da un valor distinto de cero (sesgo).
Por eso el Arduino calibra al arrancar. Mide el sesgo con el sensor quieto y lo resta después. Si mueves el sensor durante esos dos segundos, la calibración sale mal y la deriva será peor. Calibrar es medir el error para poder corregirlo.
Lo que no podemos medir
No calculamos el giro horizontal (la dirección a la que mira el sensor). Con el sensor plano, la gravedad no cambia aunque lo gires sobre la mesa, así que el acelerómetro no se entera. El giroscopio sí lo notaría, pero sin nada que lo corrija se desviaría sin parar. Para eso hace falta una brújula.
Qué conclusiones valen
Dos MPU-6050 distintos darán valores distintos, porque cada uno tiene su propio sesgo. El 0,98 del filtro lo hemos elegido a ojo: es razonable, no es el mejor posible.
Y una advertencia: esto es un prototipo para entender la tecnología. No sirve como instrumento de precisión ni para controlar nada que pueda hacer daño. Medir no significa medir bien.
Para el aula: experimentos con el giroscopio Arduino
El giroscopio Arduino da mucho juego para trabajar el método científico. Estas cinco actividades se hacen con el mismo montaje.
| Actividad | Qué hacen | Qué descubren |
|---|---|---|
| Calibración | Reinician el Arduino cinco veces con el sensor quieto y apuntan el sesgo que escribe al arrancar | Que el sesgo no es siempre igual, y que cambia de un sensor a otro |
| Medir la deriva | Con el sensor quieto, pulsan Recentrar y anotan la deriva cada 30 segundos durante 5 minutos | Una tabla y una gráfica: ¿crece en línea recta? ¿Cuántos grados por minuto? |
| La sacudida | Deslizan el sensor rápido por la mesa sin inclinarlo | Qué cubo se engaña y por qué: el acelerómetro no distingue gravedad de empujón |
| Elegir la confianza | Proponen un valor para la barra del giroscopio y diseñan una prueba con un ángulo conocido (45° con un transportador) | Que «mejor» necesita un criterio: ¿más suave o más fiel? |
| Dos aparatos | Colocan el móvil y el sensor juntos y comparan sus ángulos | Qué ocurre cuando dos instrumentos dan resultados distintos |
Preguntas para discutir
- ¿Qué variables mide el sensor y en qué unidades? ¿Cuál de los ángulos de la pantalla se ha medido de verdad?
- ¿Qué error puede tener cada medida? ¿Cómo lo comprobarías?
- ¿Qué ocurre si el Arduino manda solo 5 datos por segundo en vez de 50?
- ¿Por qué el giroscopio solo no sirve? ¿Y el acelerómetro solo?
- ¿Cómo automatizarías la recogida de los datos de la deriva?
Retos con el giroscopio Arduino
Reto 1. Haz que funcione. Los tres cubos se mueven al inclinar el sensor y el móvil los ve igual que el ordenador.
Reto 2. Modifica algo. Cambia la confianza inicial (let alfa) y la velocidad de envío del Arduino (CADA_MS, de 20 a 10). Mira qué cambia en la gráfica.
Reto 3. Añade un dato. El MPU-6050 también mide la temperatura de su propio chip, en el valor v[3] del programa. Según el fabricante, se pasa a grados con v[3] / 340.0 + 36.53. Mándala como un octavo número y muéstrala en la página. Ojo: es la temperatura del chip, no la de la habitación.
Reto 4. Guarda los datos. En chisme50-puente.py, abre un archivo al principio y escribe en él cada línea:
archivo = open("datos.csv", "a") # al principio del programa
def difundir(linea):
archivo.write(linea + "\n") # una línea nueva
archivo.flush() # que se guarde enseguida
# ... el resto de la función igual
Solo guarda datos del sensor, nada personal. Cuando termines, ábrelo en una hoja de cálculo.
Reto 5. Representa los datos. Dibuja en la hoja de cálculo el ángulo del giroscopio y el del acelerómetro durante un minuto. ¿Dónde se separan?
Reto 6. Automatiza. Haz que la página cambie de color y pite cuando la inclinación pase de 30 grados: una alarma de vuelco.
Reto 7. Haz que otro dispositivo participe. Abre la misma dirección en dos móviles a la vez. El puente reparte los datos a todos los navegadores. ¿Ven lo mismo? ¿Y si uno se conecta tarde?
Reto 8. Conviértelo en una investigación. ¿Depende la deriva del giroscopio de la temperatura? Diseña un experimento: mide la deriva con el sensor frío y después con el chip algo más caliente (por ejemplo, tras un rato conectado). Decide cuántas repeticiones harás antes de sacar conclusiones, y qué no podrías afirmar.
Qué hemos aprendido con el giroscopio Arduino
- Un giroscopio no mide ángulos, mide velocidad de giro; el ángulo se calcula sumando, y esa suma arrastra la deriva.
- Un acelerómetro da el ángulo sin deriva, pero tiembla y se engaña con los empujones.
- Combinar dos sensores imperfectos da una medida mejor que cualquiera de los dos.
- Un Arduino, un ordenador y un móvil pueden trabajar juntos: cada uno hace lo que mejor sabe.
- Calibrar es medir el error para corregirlo, y medir no significa medir bien.
Escena final: el aerodeslizador
Semanas después, en lo alto del Peñón de los Reales, la Gata miró el horizonte.
—Lince, ¿recuerdas lo que dijiste?
—Que el Lince no vuela —respondió él, muy digno—. Y sigue siendo verdad.
Entonces se oyó un zumbido. Desde detrás de los pinsapos apareció un aerodeslizador plateado, tan nivelado como una mesa, con un cubo verde parpadeando en el panel.
—Pero nadie ha dicho que el Lince no pueda engancharse a uno —añadió la Gata.
El Lince tomó carrerilla, saltó y las piezas encajaron con un «clac» enorme, como la cabeza de un robot gigante que recibe a su piloto.
—¡Acoplamiento completado! —gritó el Lince.
—Como Mazinger Z —suspiró la Gata—. Qué friki es esta sierra.
Y el aerodeslizador, siempre derecho gracias a su giroscopio, se alzó sobre Sierra Bermeja. Los buitres, por una vez, miraban desde abajo.
Para seguir investigando
Otros chismes del Lince relacionados
- El nivel de burbuja: inclinación con el acelerómetro de la micro:bit.
- El sismógrafo de la mano: otro uso de los sensores de movimiento, con el móvil.
- Los sentidos del Lince: los sensores que ya lleva tu móvil.
Fuentes y documentación
- Giroscopio, en Wikipedia.
- Acelerómetro, en Wikipedia.
- Uso de eventos enviados por el servidor, en MDN.
- Biblioteca Wire (I2C), en la documentación de Arduino.
Si quieres ir más lejos con el giroscopio Arduino, busca el filtro de Kalman y el filtro de Madgwick: son las versiones avanzadas del filtro complementario que hemos usado.
Etiqueta:acelerómetro, Arduino Mega, filtro complementario, giroscopio, I2C, MPU-6050, Puerto serie, python, servidor web



