
Chismes del Lince 53: El sismógrafo del Lince
Sierra Bermeja, de madrugada. El Lince de la Noche estaba tumbado en el suelo, con la oreja pegada a la roca roja y los ojos cerrados.
—Chsss —dijo—. Callaos. Creo que viene algo.
—¿Qué viene? —preguntó la Gata Montesa, sentada a su lado, sin ninguna prisa.
—Un gigante. Un gigante malvado, de los que aplastan pueblos. Lo noto en el suelo: pum… pum… pum…
—Eso es tu corazón, Lince.
—¿Seguro?
—Y un poco el de ese cerdo salvaje que pasa por el camino.
El Lince abrió un ojo, ofendido.
—Pues es una idea magnífica. Si un gigante viniera, el suelo temblaría mucho antes de que lo viera aparecer por el horizonte. Las vibraciones viajan por la tierra a varios kilómetros por segundo, mucho más rápido que cualquier gigante, por zancudo que sea.
—Eso es verdad —admitió la Gata—. Pero no se puede vigilar con la oreja pegada al suelo toda la noche. Necesitas algo que escuche por ti, que no se duerma y que apunte la hora exacta de cada temblor.
—¿Y eso existe?
—Se llama sismógrafo, y tienes uno en la mochila. Un sismógrafo con Arduino: un sensor de movimiento pegado al suelo que mide cuánto vibra, un detector que avisa cuando algo no es normal y una página para ver las ondas en directo en el móvil.
—¿Y detectará al gigante?
—No hay gigantes, Lince.
—Todavía.
Introducción
En este chisme construimos un sismógrafo con Arduino. Un sensor MPU-6050 pegado al suelo mide cuánto vibra, el Arduino manda 100 medidas por segundo al ordenador, y una página web en el móvil dibuja las ondas en directo, detecta sacudidas y las guarda en un archivo CSV.
Como en Sierra Bermeja no suele temblar lo bastante para que un sensor casero lo note, la página incluye un botón para simular un terremoto con sus ondas P y S. Así podemos estudiar cómo es un sismograma sin esperar a que la Tierra colabore.
Por qué este chisme
Parece que en Sierra Bermeja no pasa nada, pero la tierra tiembla más de lo que creemos: entre febrero y marzo de 2026 el IGN registró 576 terremotos entre la Sierra de Grazalema y la costa de Estepona. La mayoría eran tan flojos que nadie los sintió.
Este chisme tiene tres objetivos. Entender qué es un terremoto y por qué ocurren aquí. Aprender cómo funciona un sismógrafo y qué puede y qué no puede medir uno casero. Y practicar la idea central de la serie: medir con honestidad, incluso cuando el resultado es “no se nota nada”.
Es también la vuelta de tuerca de otros chismes de vibración, como el sismógrafo de la mano, que mide temblores con el móvil. Aquí el sensor es externo, mide mucho más tiempo y guarda los datos.
Qué es un terremoto y por qué tiembla en Sierra Bermeja
La Gata hace la primera pregunta: —¿Sabes por qué se mueve la tierra?
Un terremoto es una liberación repentina de energía en la corteza terrestre. Las rocas se van deformando poco a poco, como un palo que se dobla, hasta que se rompen por una falla. Al romperse, parte de la energía se escapa en forma de ondas que viajan por el suelo y hacen temblar todo lo que hay encima.
La Tierra que empuja desde el sur
En el sur de España, la causa es la lenta colisión entre las placas africana y euroasiática. Según el IGN, convergen a unos 5 milímetros al año, en dirección noroeste-sureste. Es muy poco: una uña crece esa distancia en mes y medio. Pero durante millones de años suma mucho.
Ese empuje deforma toda la Cordillera Bética y el mar de Alborán. La sismicidad cercana a Sierra Bermeja es sobre todo superficial, a menos de 40 km de profundidad, con magnitudes bajas o moderadas, y viene de fallas de desgarre (rocas que se deslizan una al lado de la otra).
¿Y la propia sierra?
Aquí conviene corregir una idea: Sierra Bermeja no la crearon terremotos concretos, sino esa misma colisión durante millones de años. Según la Geología de Málaga, sus rocas son peridotitas, rocas del manto terrestre, la capa que hay bajo la corteza.
El modelo que describe esa guía dice que primero la corteza se estiró hasta adelgazarse (hace unos 28 a 25 millones de años) y después las placas de Alborán y África chocaron (hace 25 a 21 millones de años), plegando el manto y empujándolo hasta la superficie. El macizo de Ronda, al que pertenece, es el mayor del mundo, con unos 300 km². Los autores señalan que el proceso sigue siendo un tema controvertido.
Su color rojizo, que da nombre a la sierra, viene de los óxidos de hierro que se forman al alterarse la roca en superficie. Por dentro, la peridotita fresca es verde oscura.
Los terremotos de hoy y la sierra de hoy son, por tanto, dos consecuencias del mismo empuje, en momentos distintos de su historia.
Cómo funciona un sismógrafo con Arduino
Las ondas: P primero, S después
Cuando una falla se rompe, salen dos tipos de ondas que viajan por dentro de la Tierra. Las ondas P (primarias) comprimen y estiran la roca en la dirección en que avanzan, y son las más rápidas. Las ondas S (secundarias) la sacuden de lado a lado, son más lentas y suelen ser las que más se notan.
| Onda P | Onda S | |
|---|---|---|
| Velocidad aproximada en la corteza | unos 6 km/s | unos 3,5 km/s |
| Cuándo llega | primero | después |
| Fuerza | suave | más fuerte |
| Mueve sobre todo | arriba y abajo | de lado a lado |
Las velocidades son valores típicos y cambian según la roca. Lo importante es que la P siempre gana la carrera. Cuanto más lejos está el terremoto, más tiempo pasa entre la llegada de la P y la de la S.
Eso permite una cuenta muy útil. Como regla práctica, la distancia al origen en kilómetros es aproximadamente 8 veces los segundos que pasan entre la P y la S. Si la S llega 3 segundos después de la P, el terremoto ocurrió a unos 24 km. Con tres estaciones, los científicos cruzan esas distancias y localizan el epicentro.
Y así volvemos al gigante: sus pisadas se propagarían por el suelo a varios kilómetros por segundo, mucho más rápido de lo que camina. Un sismógrafo notaría sus pasos antes de verlo. En la realidad no hay gigantes, pero un sismógrafo sí detecta pasos, camiones o máquinas a cierta distancia: las vibraciones viajan por el suelo más rápido que quien las produce.
¿Cómo mide el movimiento nuestro sensor?
Un sismógrafo de verdad usa una masa suspendida que se queda quieta mientras el suelo se mueve. El MPU-6050 es distinto: es un acelerómetro, un sensor diminuto (MEMS) que mide la aceleración del suelo directamente. Los acelérografos del IGN, como los de la red Sílex, usan esa misma tecnología.
Mide en tres ejes (X, Y y Z) y devuelve números enteros. Con la escala de ±2 g, 16384 cuentas son 1 g. Para ver terremotos hablamos de milésimas de g (mg): 1 mg es una milésima de g.
Pero el sensor siempre nota la gravedad: marca 1000 mg en el eje vertical aunque esté quieto. Ese valor no nos interesa. Lo que queremos es la vibración por encima. Por eso el programa resta una media lenta (una constante de tiempo de unos 2 segundos), y lo que queda es solo el movimiento rápido.
¿Cómo decide la página que hay un terremoto?
Hay un detector muy usado en sismología, el STA/LTA (por sus siglas en inglés: promedio a corto plazo entre promedio a largo plazo). Calcula cuánto se agita el suelo en los últimos 0,5 segundos (STA) y lo compara con lo que se agitaba en los últimos 15 segundos (LTA), que es el ruido normal de fondo.
Si el cociente supera un umbral (3,5 por defecto), algo está pasando. Es un detector de cambios: no necesita saber cuánto es mucho, solo cuándo el suelo se agita bastante más de lo habitual. Mientras dura el evento, el LTA se congela para que el propio terremoto no suba el listón.
¿Podemos predecir terremotos?
No. Hoy no existe un método fiable para predecir cuándo, dónde y con qué fuerza ocurrirá un terremoto con días de antelación; los intentos que se han anunciado son controvertidos. Lo que sí se hace es avisar: algunos sistemas de alerta temprana detectan la onda P, que es rápida pero suave, y mandan un aviso antes de que llegue la S. Ese margen es de unos diez segundos, según la misma fuente, y sirve para parar trenes o abrir puertas.
Nuestro sismógrafo no predice ni avisa a tiempo. Detecta que algo ha pasado y lo registra, que es lo primero que hace falta para entender un terremoto.
Qué necesitamos
- Un Arduino Mega 2560 con su cable USB.
- Un sensor MPU-6050 (placa HW-123 o GY-521) con las patillas soldadas.
- Cuatro cables de puente (jumpers).
- Cinta adhesiva fuerte o doble cara, para fijar el sensor.
- Un ordenador con el IDE de Arduino y Python 3 (con Thonny basta, que ya trae
pyserial). - Un móvil en la misma red Wi-Fi que el ordenador.
Conexiones
Son las mismas de los chismes 50 y 52. Conecta con el Arduino desenchufado:
| MPU-6050 | Arduino Mega |
|---|---|
| VCC | 3.3V |
| GND | GND |
| SDA | pin 20 |
| SCL | pin 21 |
Montaje del sismógrafo
Un sismógrafo mide cómo se mueve el suelo, así que el sensor tiene que moverse exactamente igual que el suelo. Si cuelga o puede balancearse, medirá su propio balanceo.
- Pega el sensor con cinta sobre una superficie firme: el suelo, una mesa pesada o una pared de obra. Mejor en una planta baja.
- Colócalo plano, con el eje Z mirando hacia arriba. Es el eje vertical.
- Sujeta también los cables con cinta unos centímetros más allá del sensor, para que no tiren de él.
- Deja el Arduino cerca, sin que roce la mesa. Un cable USB largo va bien.
- Evita ponerlo al lado de la lavadora, la nevera o un ventilador: vibran todo el rato y subirán el ruido de fondo.



El código del Arduino
En el IDE de Arduino: Archivo → Nuevo, pega el código y guárdalo con el nombre chisme53_sismografo_mega. Placa: Arduino Mega or Mega 2560.
Es un programa corto. Despierta el sensor, lo configura en ±2 g y cada 10 milisegundos lee los tres ejes y manda una línea de texto. El filtro interno de 44 Hz tiene una función importante: elimina las vibraciones más rápidas de lo que podemos medir a 100 muestras por segundo, que de otro modo se colarían disfrazadas de otras frecuencias (un fenómeno llamado aliasing que veremos en otro chisme).
// Chisme 53 - El sismógrafo del Lince (parte 1: el Arduino Mega)
// Mide la aceleración del MPU-6050 (acelerómetro) 100 veces por segundo y la manda por el cable USB.
//
// CONEXIONES (las mismas de los chismes 50 y 52):
// MPU-6050 VCC -> 3.3V GND -> GND
// SDA -> pin 20 SCL -> pin 21
//
// QUÉ MANDA: una línea de texto por cada medida, 100 veces por segundo:
// S,tiempo_ms,ax,ay,az
// - tiempo_ms: milisegundos desde que arrancó el Arduino
// - ax, ay, az: la aceleración en "cuentas" del sensor (16384 cuentas = 1 g)
// La página las convierte a milésimas de g (mg).
//
// MONTAJE: el sensor debe quedar FIJO y PLANO, pegado a una mesa, al suelo o a una pared con cinta.
// Un sismógrafo mide cómo se mueve el suelo, así que el sensor no debe poder moverse por su cuenta.
//
// IMPORTANTE: cuando uses chisme53-puente.py, CIERRA el Monitor Serie del IDE de Arduino.
#include <Wire.h>
const uint8_t MPU = 0x68; // dirección del MPU-6050
const unsigned long CADA_US = 10000; // una medida cada 10 ms = 100 por segundo
unsigned long siguiente; // momento de la siguiente medida (microsegundos)
void escribirRegistro(uint8_t registro, uint8_t valor) {
Wire.beginTransmission(MPU);
Wire.write(registro);
Wire.write(valor);
Wire.endTransmission();
}
// Pide al sensor 6 bytes (desde el registro 0x3B): ax, ay, az, cada uno en 2 bytes
bool leerAceleracion(int16_t *a) {
Wire.beginTransmission(MPU);
Wire.write(0x3B);
if (Wire.endTransmission(false) != 0) return false;
if (Wire.requestFrom((int)MPU, 6) != 6) return false;
for (int i = 0; i < 3; i++) {
int16_t alto = Wire.read(); // primero llega el byte alto
int16_t bajo = Wire.read();
a[i] = (alto << 8) | bajo;
}
return true;
}
void setup() {
Serial.begin(115200);
Wire.begin();
escribirRegistro(0x6B, 0x00); // el sensor arranca dormido: lo despertamos
escribirRegistro(0x1C, 0x00); // acelerómetro en +-2 g (16384 cuentas por g)
escribirRegistro(0x1A, 0x03); // filtro interno de ~44 Hz: quita lo que no podemos medir a 100 por segundo
Serial.println("# Sismografo listo: 100 medidas por segundo");
siguiente = micros();
}
void loop() {
if ((long)(micros() - siguiente) < 0) return; // todavía no toca medir
siguiente += CADA_US;
int16_t a[3];
if (!leerAceleracion(a)) {
Serial.println("# error de lectura: revisa el cableado");
return;
}
Serial.print("S,");
Serial.print(millis()); Serial.print(',');
Serial.print(a[0]); Serial.print(',');
Serial.print(a[1]); Serial.print(',');
Serial.println(a[2]);
}
Lee y predice. Con las 100 medidas por segundo, ¿cuál es la frecuencia más alta que podemos ver sin engaños? Pista: hacen falta al menos dos medidas por cada ciclo de la onda.
El puente de Python
Es el mismo puente de los chismes 50 y 51, algo más corto porque aquí solo hay un sentido: del Arduino al navegador. Lee el puerto serie, abre una pequeña web en el ordenador y reparte cada medida a todos los navegadores conectados.
Guárdalo como chisme53-puente.py y, en la misma carpeta, la página del apartado siguiente. Con --simular inventa medidas de un sensor quieto: sirve para probar la página sin Arduino.
# Chisme 53 - El sismógrafo del Lince (parte 2: el puente en el ordenador)
#
# Es el puente de los chismes 50 y 51:
# 1) lee por el cable USB (puerto serie) las medidas del Arduino Mega
# 2) abre una página web en el ordenador, por la red Wi-Fi
# 3) manda cada medida a todos los navegadores que estén mirando (PC y móvil)
#
# COMO USARLO
# - Deja este archivo y chisme53-sismografo.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 chisme53-puente.py
# o con puerto: python chisme53-puente.py --puerto COM5
# o SIN Arduino: python chisme53-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 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__))
PAGINA = "chisme53-sismografo.html"
# 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", "/" + PAGINA):
self.enviar_pagina()
elif self.path == "/datos":
self.enviar_datos()
else:
self.send_error(404)
def enviar_pagina(self):
ruta = os.path.join(CARPETA, PAGINA)
try:
with open(ruta, "rb") as f:
contenido = f.read()
except OSError:
self.send_error(404, "Falta " + PAGINA + " junto a chisme53-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=1000)
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():
"""Medidas inventadas, como las que mandaria el Arduino: 100 por segundo.
El sensor 'esta quieto': 1 g en el eje z y un ruido de unos 2,5 mg (milesimas de g)."""
print("MODO SIMULACION: los datos son inventados.")
CUENTAS_POR_MG = 16384 / 1000.0
t0 = time.time()
i = 0
while True:
t_ms = int(i * 10)
ax = int((20 + random.gauss(0, 2.5)) * CUENTAS_POR_MG)
ay = int((-10 + random.gauss(0, 2.5)) * CUENTAS_POR_MG)
az = int((1000 + random.gauss(0, 2.5)) * CUENTAS_POR_MG)
difundir("S,%d,%d,%d,%d" % (t_ms, ax, ay, az))
i += 1
espera = t0 + i * 0.01 - time.time()
if espera > 0:
time.sleep(espera)
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 chisme53-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()
Para ejecutarlo desde Thonny, usa el intérprete “Local Python 3” y el argumento --puerto COM5 (Ver → Argumentos del programa), cambiando COM5 por el puerto de tu Arduino.
La página del sismógrafo
Es un único archivo HTML. Guárdalo como chisme53-sismografo.html, junto a chisme53-puente.py. Lo servirá el puente por la red local, así que no hace falta publicarlo.
Lo importante está en cinco funciones de JavaScript: procesar() (recibe una medida y quita la gravedad), detectar() (el STA/LTA), aporteSim() (el terremoto inventado con sus ondas P y S), registrar() (guarda las filas del CSV) y dibujar() (pinta las tres trazas).
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>El sismógrafo del Lince</title>
<style>
:root {
--fondo: #f6f8f7;
--tarjeta: #ffffff;
--texto: #1c2b27;
--suave: #5d6f69;
--borde: #d7e0dc;
--x: #0f8f7e;
--y: #3b6fd1;
--z: #c2410c;
--alerta: #d33b2c;
}
* { 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; margin-bottom: 10px; }
#estado.ok { background: #dff3ec; border-color: #9fd4c3; }
#aviso { display: none; padding: 8px 12px; border-radius: 10px; background: #fde7e4;
border: 1px solid #f0aaa3; color: #7a1d12; font-weight: 650; margin-bottom: 8px; }
#aviso.on { display: block; }
canvas { width: 100%; height: 340px; background: #fff; border: 1px solid var(--borde);
border-radius: 12px; display: block; }
.lecturas { display: flex; flex-wrap: wrap; gap: 8px 22px; margin: 10px 2px; font-size: .95rem;
font-variant-numeric: tabular-nums; }
.lecturas b { font-weight: 650; }
.tarjeta { background: var(--tarjeta); border: 1px solid var(--borde); border-radius: 12px;
padding: 12px; margin: 10px 0; }
.tarjeta h2 { font-size: 1rem; margin: 0 0 4px; }
.tarjeta p { margin: 2px 0 8px; font-size: .88rem; color: var(--suave); }
.fila { display: flex; flex-wrap: wrap; gap: 8px 14px; align-items: center; margin: 6px 0; }
button { font: inherit; padding: 7px 14px; border-radius: 8px; border: 1px solid var(--borde);
background: #fff; cursor: pointer; }
button:hover { background: #eef4f1; }
button.grande { background: var(--alerta); border-color: var(--alerta); color: #fff; font-weight: 650; }
button.grabando { background: var(--alerta); border-color: var(--alerta); color: #fff; font-weight: 650; }
select, input[type=range] { font: inherit; }
select { padding: 6px; border-radius: 8px; border: 1px solid var(--borde); }
label { font-size: .9rem; }
table { border-collapse: collapse; width: 100%; font-size: .88rem; font-variant-numeric: tabular-nums; }
th, td { text-align: left; padding: 4px 8px; border-bottom: 1px solid var(--borde); }
details { background: var(--tarjeta); border: 1px solid var(--borde); border-radius: 12px;
padding: 8px 12px; margin: 10px 0; font-size: .92rem; }
summary { cursor: pointer; font-weight: 600; }
.nota { font-size: .86rem; color: var(--suave); }
.leyenda span { display: inline-block; margin-right: 14px; font-size: .85rem; }
.leyenda i { display: inline-block; width: 12px; height: 3px; margin-right: 5px; vertical-align: middle; }
a.archivo { font-size: .88rem; display: block; margin: 2px 0; }
</style>
</head>
<body>
<main>
<h1>El sismógrafo del Lince</h1>
<p class="sub">Mira cómo se mueve el suelo bajo el sensor, busca terremotos y guarda lo que pase.</p>
<span id="estado">Conectando con el puente…</span>
<div id="aviso">⚠ Vibración detectada</div>
<canvas id="traza"></canvas>
<div class="leyenda">
<span><i style="background:var(--x)"></i>X</span>
<span><i style="background:var(--y)"></i>Y</span>
<span><i style="background:var(--z)"></i>Z (vertical)</span>
<span class="nota">Se ven los últimos 30 segundos. Unidad: mg (milésimas de g).</span>
</div>
<div class="lecturas">
<span>Movimiento ahora: <b id="ahora">—</b> mg</span>
<span>Ruido de fondo: <b id="ruido">—</b> mg</span>
<span>STA/LTA: <b id="razon">—</b></span>
<span id="tasa"></span>
</div>
<section class="tarjeta">
<h2>Simular un terremoto</h2>
<p>Añade a lo que mide el sensor un terremoto inventado: primero llega la onda P (rápida, de poca fuerza) y después la S (más lenta y más fuerte). Es una simulación ilustrativa, no un modelo científico.</p>
<div class="fila">
<label>Magnitud: <b id="magTxt">5.0</b>
<input type="range" id="mag" min="2.5" max="7" step="0.1" value="5"></label>
<label>Distancia: <b id="disTxt">30</b> km
<input type="range" id="dis" min="10" max="200" step="5" value="30"></label>
</div>
<div class="fila">
<button class="grande" id="simular">💥 Simular terremoto</button>
<button data-m="4.0" data-d="20" class="pre">Como el de febrero de 2026 (M4,0 a 20 km)</button>
<button data-m="5.0" data-d="15" class="pre">Moderado y cercano (M5,0 a 15 km)</button>
<button data-m="6.5" data-d="30" class="pre">Fuerte (M6,5 a 30 km)</button>
</div>
<p id="infoSim" class="nota"></p>
</section>
<section class="tarjeta">
<h2>Detector y escala</h2>
<p>El detector compara la agitación de los últimos 0,5 s (STA) con la de los últimos 15 s (LTA). Si la proporción supera el umbral, dice que hay un terremoto (o un golpe).</p>
<div class="fila">
<label>Sensibilidad
<select id="sens">
<option value="2.5">Alta (salta con poco)</option>
<option value="3.5" selected>Normal</option>
<option value="5">Baja</option>
</select></label>
<label>Escala vertical
<select id="escala">
<option value="0">Automática</option>
<option value="5">±5 mg</option>
<option value="20">±20 mg</option>
<option value="100">±100 mg</option>
<option value="500">±500 mg</option>
</select></label>
<label><input type="checkbox" id="autoguardar" checked> Guardar un registro cuando haya un evento (10 s antes y 30 s después)</label>
</div>
</section>
<section class="tarjeta">
<h2>Grabación (CSV)</h2>
<p>Graba todas las medidas, sin parar, hasta que pulses de nuevo. Se descarga un archivo CSV que puedes abrir en una hoja de cálculo.</p>
<div class="fila">
<button id="grabar">● Grabar</button>
<span id="filas" class="nota"></span>
</div>
<div id="archivos"></div>
</section>
<section class="tarjeta">
<h2>Eventos detectados</h2>
<table>
<thead><tr><th>Hora</th><th>Pico (mg)</th><th>Duración (s)</th><th>STA/LTA máx.</th><th>Origen</th></tr></thead>
<tbody id="eventos"><tr><td colspan="5" class="nota">Todavía ninguno.</td></tr></tbody>
</table>
<div class="fila"><button id="bajarEventos">Descargar eventos (CSV)</button></div>
</section>
<details>
<summary>Montaje y consejos</summary>
<p><b>Sensor:</b> pegado con cinta, plano y fijo, sobre una mesa, el suelo o una pared. Si se puede mover solo, no mide el suelo, mide el sensor.</p>
<p><b>Probar sin terremoto:</b> da un golpe en la mesa, salta cerca o camina fuerte. Cada uno tiene una "firma" distinta.</p>
<p><b>Ruido de fondo:</b> es lo que marca el sensor aunque todo esté quieto. Un terremoto menor que ese ruido no se puede ver con este sensor.</p>
<p><b>Sin Arduino:</b> con <code>--simular</code> el puente inventa medidas de un sensor quieto; el botón de terremoto funciona igual.</p>
</details>
<p class="nota">Mide la aceleración del suelo 100 veces por segundo con un MPU-6050. Es un sismógrafo de juguete: sirve para aprender, no para vigilar terremotos de verdad.</p>
</main>
<script>
const FS = 100; // medidas por segundo (lo decide el Arduino)
const VENTANA = 30 * FS; // 30 segundos de gráfica
const $ = id => document.getElementById(id);
// ---- Estado del procesado ----
let n = 0; // medidas recibidas en total
let t0 = null; // tiempo (ms) de la primera medida
let tSeg = 0; // tiempo de la última medida, en segundos
const hx = new Float32Array(VENTANA), hy = new Float32Array(VENTANA), hz = new Float32Array(VENTANA);
let base = null; // línea base de cada eje (la gravedad y la inclinación)
let sta = 0, lta = 0; // agitación a corto y a largo plazo
let razon = 0;
let hayDatos = false, ultimaLlegada = 0, contador = 0;
let marcas = []; // líneas verticales sobre la gráfica: {n, texto, color}
// Evento en curso
let evento = null; // {n0, t0, pico, razonMax, sim}
const eventos = []; // eventos terminados
// Simulación
let sim = null; // {t0, mag, dis, dtSP, A, tau, fase}
// Grabación
let grabando = false, filasGrab = [], tGrab0 = 0;
const previo = []; // últimos 10 s de medidas, para los registros de eventos
let registroEvento = null; // {filas, hasta}
let numRegistro = 0;
function estado(texto, ok) { $("estado").textContent = texto; $("estado").className = ok ? "ok" : ""; }
// ---- Terremoto inventado: ondas P y S ----
function crearSimulacion(mag, dis) {
const dtSP = dis / 8.4; // la S llega unos 1,2 s después de la P por cada 10 km
const A = 3 * Math.pow(10, 0.6 * (mag - 4)) * Math.pow(20 / dis, 1.2); // pico de la onda S en mg (ilustrativo)
const tau = 2 + (mag - 3) * 1.5; // cuánto tarda en apagarse
return { t0: tSeg, mag, dis, dtSP, A, tau,
f1: Math.random() * 6, f2: Math.random() * 6, f3: Math.random() * 6 };
}
function aporteSim(t) { // devuelve [dx, dy, dz] en mg
if (!sim) return [0, 0, 0];
const s = t - sim.t0;
if (s > sim.dtSP + sim.tau * 5 + 2) { sim = null; return [0, 0, 0]; }
let p = 0, o = 0;
if (s >= 0) { // onda P: rápida (6 Hz) y poco intensa
p = 0.3 * sim.A * (1 - Math.exp(-s / 0.2)) * Math.exp(-s / 1.0) * Math.sin(2 * Math.PI * 6 * s + sim.f1);
}
const u = s - sim.dtSP;
if (u >= 0) { // onda S: más lenta (2,5 a 3,7 Hz) y mucho más fuerte
const env = (1 - Math.exp(-u / 0.4)) * Math.exp(-u / sim.tau);
o = sim.A * env * (0.7 * Math.sin(2 * Math.PI * 2.5 * u + sim.f2) + 0.5 * Math.sin(2 * Math.PI * 3.7 * u + sim.f3));
o += 0.3 * sim.A * Math.exp(-u / (2 * sim.tau)) * Math.sin(2 * Math.PI * 1.5 * u); // cola final
}
// La P mueve sobre todo en vertical y la S sobre todo en horizontal
return [0.3 * p + 1.0 * o, 0.3 * p + 0.8 * o, 1.0 * p + 0.4 * o];
}
// ---- Recibir una medida: "S,tiempo_ms,ax,ay,az" (aceleración en cuentas; 16384 = 1 g) ----
function procesar(linea) {
if (linea.charAt(0) === "#") { estado(linea.slice(1).trim(), false); return; }
if (linea.slice(0, 2) !== "S,") return;
const p = linea.split(",");
if (p.length !== 5) return;
const t = Number(p[1]);
const cuentas = [Number(p[2]), Number(p[3]), Number(p[4])];
if (isNaN(t) || cuentas.some(isNaN)) return; // línea rota: la ignoramos
ultimaLlegada = performance.now();
hayDatos = true;
contador++;
if (t0 === null) t0 = t;
tSeg = (t - t0) / 1000;
let mg = cuentas.map(c => c * 1000 / 16384); // de cuentas a milésimas de g
const extra = aporteSim(tSeg);
const esSim = sim !== null ? 1 : 0;
mg = [mg[0] + extra[0], mg[1] + extra[1], mg[2] + extra[2]];
// Quitamos la parte lenta (la gravedad): lo que queda es la vibración
if (base === null) base = mg.slice();
const alfa = 0.005; // aprox. una constante de tiempo de 2 s
for (let k = 0; k < 3; k++) base[k] += (mg[k] - base[k]) * alfa;
const v = [mg[0] - base[0], mg[1] - base[1], mg[2] - base[2]];
const i = n % VENTANA;
hx[i] = v[0]; hy[i] = v[1]; hz[i] = v[2];
n++;
detectar(Math.hypot(v[0], v[1], v[2]), esSim);
registrar(tSeg, mg, esSim);
}
// ---- Detector STA/LTA ----
function detectar(m, esSim) {
sta += (m - sta) / (0.5 * FS);
const aprendiendo = n < 15 * FS;
if (aprendiendo) {
lta += (m - lta) / Math.max(1, n); // al principio, media simple
} else if (!evento) {
lta += (m - lta) / (15 * FS); // durante un evento no dejamos que el ruido "suba"
}
razon = lta > 0 ? sta / lta : 0;
const umbral = Number($("sens").value);
if (!aprendiendo) {
if (!evento && razon >= umbral && sta > 6) {
evento = { n0: n, t0: tSeg, hora: new Date(), pico: m, razonMax: razon, sim: esSim };
marcas.push({ n, texto: "Evento", color: "#d33b2c" });
if ($("autoguardar").checked && !registroEvento) {
registroEvento = { filas: previo.slice(), hasta: tSeg + 30, nombre: "evento" };
}
} else if (evento) {
evento.pico = Math.max(evento.pico, m);
evento.razonMax = Math.max(evento.razonMax, razon);
if (esSim) evento.sim = 1;
if (razon < 1.5) { cerrarEvento(); }
}
}
$("aviso").classList.toggle("on", !!evento);
$("ahora").textContent = m.toFixed(1);
$("razon").textContent = aprendiendo ? "aprendiendo…" : razon.toFixed(1);
$("ruido").textContent = aprendiendo ? "…" : lta.toFixed(1);
}
function cerrarEvento() {
evento.dur = tSeg - evento.t0;
eventos.push(evento);
evento = null;
pintarEventos();
}
function pintarEventos() {
const cuerpo = $("eventos");
cuerpo.innerHTML = "";
if (!eventos.length) {
cuerpo.innerHTML = '<tr><td colspan="5" class="nota">Todavía ninguno.</td></tr>';
return;
}
for (const e of eventos.slice().reverse()) {
const tr = document.createElement("tr");
tr.innerHTML = "<td>" + e.hora.toLocaleTimeString() + "</td><td>" + e.pico.toFixed(1) +
"</td><td>" + e.dur.toFixed(1) + "</td><td>" + e.razonMax.toFixed(1) + "</td><td>" +
(e.sim ? "simulado" : "real") + "</td>";
cuerpo.appendChild(tr);
}
}
// ---- Grabación y CSV ----
function registrar(t, mg, esSim) {
const fila = [t.toFixed(3), mg[0].toFixed(2), mg[1].toFixed(2), mg[2].toFixed(2), esSim];
previo.push(fila);
if (previo.length > 10 * FS) previo.shift();
if (grabando) {
filasGrab.push(fila);
if (filasGrab.length % 50 === 0) $("filas").textContent = filasGrab.length + " filas (" + (filasGrab.length / FS).toFixed(0) + " s)";
if (filasGrab.length >= 360000) pararGrabacion(); // tope: una hora
}
if (registroEvento) {
registroEvento.filas.push(fila);
if (t >= registroEvento.hasta) {
numRegistro++;
ofrecerCSV(registroEvento.filas, "chisme53_evento" + numRegistro + ".csv", false);
registroEvento = null;
}
}
}
function csvTexto(filas) {
return "tiempo_s,ax_mg,ay_mg,az_mg,simulado\n" + filas.map(f => f.join(",")).join("\n") + "\n";
}
function ofrecerCSV(filas, nombre, descargar) {
const url = URL.createObjectURL(new Blob([csvTexto(filas)], { type: "text/csv" }));
const a = document.createElement("a");
a.className = "archivo"; a.href = url; a.download = nombre;
a.textContent = "⬇ " + nombre + " (" + filas.length + " filas)";
$("archivos").prepend(a);
if (descargar) a.click();
}
function pararGrabacion() {
grabando = false;
$("grabar").textContent = "● Grabar";
$("grabar").classList.remove("grabando");
if (filasGrab.length) {
const marca = new Date().toISOString().slice(0, 19).replace(/[-:T]/g, "");
ofrecerCSV(filasGrab, "chisme53_registro_" + marca + ".csv", true);
}
filasGrab = [];
$("filas").textContent = "";
}
$("grabar").addEventListener("click", () => {
if (grabando) { pararGrabacion(); return; }
if (!hayDatos) { estado("Todavía no llegan datos: no hay nada que grabar.", false); return; }
filasGrab = []; grabando = true;
$("grabar").textContent = "■ Parar y descargar";
$("grabar").classList.add("grabando");
$("filas").textContent = "grabando…";
});
$("bajarEventos").addEventListener("click", () => {
const txt = "hora,pico_mg,duracion_s,sta_lta_max,origen\n" + eventos.map(e =>
[e.hora.toISOString(), e.pico.toFixed(2), e.dur.toFixed(2), e.razonMax.toFixed(2), e.sim ? "simulado" : "real"].join(",")).join("\n") + "\n";
const a = document.createElement("a");
a.href = URL.createObjectURL(new Blob([txt], { type: "text/csv" }));
a.download = "chisme53_eventos.csv";
a.click();
});
// ---- Controles de la simulación ----
$("mag").addEventListener("input", () => { $("magTxt").textContent = Number($("mag").value).toFixed(1); });
$("dis").addEventListener("input", () => { $("disTxt").textContent = $("dis").value; });
document.querySelectorAll("button.pre").forEach(b => b.addEventListener("click", () => {
$("mag").value = b.dataset.m; $("dis").value = b.dataset.d;
$("magTxt").textContent = Number(b.dataset.m).toFixed(1); $("disTxt").textContent = b.dataset.d;
lanzarSimulacion();
}));
$("simular").addEventListener("click", lanzarSimulacion);
function lanzarSimulacion() {
if (!hayDatos) { estado("Todavía no llegan datos: arranca el puente primero.", false); return; }
sim = crearSimulacion(Number($("mag").value), Number($("dis").value));
marcas.push({ n, texto: "P", color: "#555" });
marcas.push({ n: n + Math.round(sim.dtSP * FS), texto: "S", color: "#555" });
$("infoSim").textContent = "Terremoto simulado de magnitud " + sim.mag.toFixed(1) + " a " + sim.dis +
" km. La onda S llega " + sim.dtSP.toFixed(1) + " s después de la P. Pico de la onda S: unos " +
sim.A.toFixed(1) + " mg. Compáralo con el ruido de fondo: si es parecido, el sensor no lo distingue.";
}
// ---- Dibujar ----
const lienzo = $("traza");
const ctx = lienzo.getContext("2d");
let escalaAuto = 10;
function dibujar() {
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);
ctx.font = "11.5px system-ui, sans-serif";
const izq = 54, der = 10, arr = 8, aba = 22;
const ancho = w - izq - der;
const franja = (h - arr - aba) / 3;
if (n < 2) {
ctx.fillStyle = "#5d6f69";
ctx.fillText("Esperando medidas del Arduino…", izq, h / 2);
return;
}
const total = Math.min(n, VENTANA);
const primero = n - total;
// Escala vertical
let esc = Number($("escala").value);
if (esc === 0) {
let mx = 0;
for (let i = 0; i < total; i += 3) {
const j = (primero + i) % VENTANA;
mx = Math.max(mx, Math.abs(hx[j]), Math.abs(hy[j]), Math.abs(hz[j]));
}
escalaAuto += (Math.max(8, mx * 1.15) - escalaAuto) * 0.1;
esc = escalaAuto;
}
const ejes = [[hx, "#0f8f7e", "X"], [hy, "#3b6fd1", "Y"], [hz, "#c2410c", "Z"]];
ejes.forEach(([datos, color, nombre], k) => {
const cy = arr + franja * (k + 0.5);
ctx.strokeStyle = "#e3eae7"; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(izq, cy); ctx.lineTo(w - der, cy); ctx.stroke();
ctx.strokeStyle = "#eef2f0";
ctx.beginPath(); ctx.moveTo(izq, arr + franja * (k + 1)); ctx.lineTo(w - der, arr + franja * (k + 1)); ctx.stroke();
ctx.fillStyle = "#5d6f69";
ctx.fillText(nombre, 8, cy + 4);
ctx.fillText("±" + esc.toFixed(esc < 20 ? 1 : 0), 22, cy - franja / 2 + 12);
ctx.strokeStyle = color; ctx.lineWidth = 1.4;
ctx.beginPath();
const paso = Math.max(1, Math.floor(total / (ancho * 1.5)));
for (let i = 0; i < total; i += paso) {
const j = (primero + i) % VENTANA;
const x = izq + (i + (VENTANA - total)) / VENTANA * ancho;
const y = Math.max(cy - franja / 2, Math.min(cy + franja / 2, cy - datos[j] / esc * (franja / 2)));
i ? ctx.lineTo(x, y) : ctx.moveTo(x, y);
}
ctx.stroke();
});
// Marcas (evento, P, S) y segundos
marcas = marcas.filter(m => m.n > n - VENTANA);
for (const m of marcas) {
if (m.n > n) continue; // la S aún no ha llegado: no se dibuja todavía
const x = izq + (m.n - primero + (VENTANA - total)) / VENTANA * ancho;
ctx.strokeStyle = m.color; ctx.lineWidth = 1.5; ctx.setLineDash([4, 3]);
ctx.beginPath(); ctx.moveTo(x, arr); ctx.lineTo(x, h - aba); ctx.stroke(); ctx.setLineDash([]);
ctx.fillStyle = m.color; ctx.fillText(m.texto, x + 3, arr + 11);
}
ctx.fillStyle = "#5d6f69";
for (let s = 0; s <= 30; s += 5) {
const x = izq + (30 - s) / 30 * ancho;
ctx.fillText(s === 0 ? "ahora" : "-" + s + " s", x - (s === 0 ? 24 : 12), h - 6);
}
}
function fotograma() {
dibujar();
if (hayDatos && performance.now() - ultimaLlegada > 3000) {
estado("Conectado, pero no llegan datos del Arduino (¿cable? ¿Monitor Serie abierto?)", false);
hayDatos = false;
}
requestAnimationFrame(fotograma);
}
requestAnimationFrame(fotograma);
setInterval(() => {
$("tasa").textContent = contador ? contador + " medidas/s" : "";
if (contador && $("estado").className !== "ok") estado("Recibiendo datos del Arduino", true);
contador = 0;
}, 1000);
// ---- Conexión con chisme53-puente.py ----
const origen = new EventSource("/datos");
origen.onopen = () => estado("Conectado al puente. Esperando datos…", false);
origen.onerror = () => estado("Sin conexión con el puente (¿sigue abierto chisme53-puente.py?)", false);
origen.onmessage = e => procesar(e.data);
</script>
</body>
</html>
Cuando todo esté en marcha, abre en el móvil la dirección que escribe el puente en la consola.
Cómo probarlo
La Gata no quiere sorpresas, así que probamos en tres pasos, de menos a más.
Prueba 1: sin sensor, solo con el simulador
Ejecuta el puente con python chisme53-puente.py --simular y abre la dirección que te muestra, en el ordenador o en el móvil (misma wifi).
- Verás tres trazas casi planas: es el ruido de un sensor en reposo. Espera unos 15 segundos: el detector está aprendiendo cuál es el ruido normal.
- Pulsa 💥 Simular terremoto con el ajuste «Fuerte (M6,5 a 30 km)». Primero aparece un temblor suave (ondas P), unos segundos después uno mucho mayor (ondas S).
- El evento queda marcado y aparece en la tabla con su hora, su pico y la diferencia S−P.
- Prueba ahora «Como el de febrero de 2026 (M4,0 a 20 km)». Con un sensor casero probablemente no se detecte, y eso es justo lo que queremos que el alumnado descubra.
Prueba 2: con el MPU-6050 de verdad
- Deja el sensor plano y fijo sobre una superficie firme (mejor una mesa estable que un cartón sobre una esponja).
- Sube el programa a la Mega y cierra el Monitor Serie del IDE: si sigue abierto, Python no podrá usar el puerto.
- Ejecuta
python chisme53-puente.py --puerto COM5(cambia COM5 por el tuyo) y abre la página. - Espera los 15 segundos de aprendizaje y mira el ruido de fondo. Con un MPU-6050 es normal ver unos pocos mg.
- Da un golpecito en la mesa, camina cerca o salta un poco. El detector debería saltar con esos «terremotos domésticos».
Prueba 3: grabar datos
Pulsa el botón de grabar, deja pasar un par de minutos con algo de movimiento (puertas, pasos, una silla) y detén la grabación. Descarga el CSV y ábrelo en una hoja de cálculo: tendrás tiempo, ax, ay, az y una columna que indica si el dato era simulado.
Si algo no va, lo más habitual es: puerto ocupado por el Monitor Serie, puerto COM equivocado, SDA y SCL cambiados o pyserial sin instalar (pip install pyserial).
Qué estamos midiendo realmente
Aquí está la parte más honesta del chisme: medir no significa medir bien, y un sismógrafo casero lo demuestra mejor que ningún otro.
Lo que mide el sensor
El MPU-6050 mide aceleración en tres ejes, en cuentas del conversor (16384 por g con el rango de ±2 g). Nuestro programa la envía tal cual y es la página quien la convierte a miligramos de g (mg) y le resta la componente lenta (la gravedad y la inclinación), para quedarse solo con lo que vibra.
Lo que vemos es, por tanto, una vibración del sensor, no «el terremoto». Si el sensor está sobre una mesa que cruje, mide la mesa.
El ruido de fondo
Con el sensor en reposo, la traza no es una línea perfecta: oscila unos 4 o 5 mg. Es el ruido electrónico del chip más las vibraciones del entorno. Ese ruido es el suelo de nuestra medida: cualquier cosa por debajo es invisible.
Por qué no veremos el terremoto de febrero
Según los datos publicados por el IGN para la serie de febrero de 2026, los terremotos de magnitud 4 se registraron en la zona con aceleraciones del orden de 3 mg a unos 15–20 km. Es menos que el ruido de nuestro sensor. Los sismógrafos profesionales y los acelerógrafos del IGN usan sensores mucho más silenciosos, instalados sobre roca y lejos de la gente.
Conclusión: con este montaje detectaremos movimientos fuertes y cercanos, o el de un salto en el aula, pero no un M4 a 20 km. Y saberlo es parte de la ciencia.
Dato frente a medida
Un número en pantalla es un dato. Para convertirlo en una medida hay que saber su unidad, su incertidumbre y cómo se obtuvo. Nuestro «pico: 81 mg» es un dato fiable de lo que vibró el sensor, pero no es la aceleración del suelo ni la magnitud del terremoto.
El simulador es ilustrativo
Las amplitudes del botón «Simular terremoto» salen de una fórmula sencilla (más magnitud, más amplitud; más distancia, menos) con ondas inventadas. Sirve para ver cómo se ven las ondas P y S y cómo reacciona el detector, no para predecir qué registraría un terremoto real. Por eso el CSV lleva la columna simulado: nunca hay que mezclar datos reales con inventados.
Falsas alarmas y límites del detector
El STA/LTA solo compara «ahora» con «el rato de antes». No sabe qué es un terremoto: un portazo, un paso fuerte o un tractor cerca disparan el detector igual. Es correlación con vibración, no causalidad con un sismo. Las estaciones reales lo resuelven cruzando datos de varios sensores.
Qué no podemos concluir
Que no salte el detector no significa que no hubiera terremoto, y que salte no significa que lo hubiera. Tampoco podemos predecir terremotos con este aparato: solo observamos lo que ya ha empezado. Además, a 100 medidas por segundo no podemos ver vibraciones mucho más rápidas que 50 Hz (es el límite de Nyquist) y el filtro interno del sensor ya las atena.
Y tus datos
Todo ocurre en tu red local: el sensor habla con la Mega, la Mega con Python y Python con tu navegador. No se envía nada a internet ni se guardan datos personales; el CSV solo se crea si lo descargas tú.
Para el aula
1. La regla de la distancia. Con el simulador, anota la diferencia S−P de varios terremotos a distintas distancias. Calcula la distancia con la regla de ~8 km por segundo y comprueba cuánto se equivoca. ¿Por qué no es exacta?
2. Medir el ruido. Deja el sensor quieto 5 minutos grabando. Calcula en la hoja de cálculo la media y la desviación típica de cada eje. ¿Cuánto vale el ruido en mg? ¿Qué eje es más ruidoso?
3. Cambiar una variable. Mide el ruido sobre la mesa, sobre el suelo y sobre una esponja. ¿Qué cambia? ¿Por qué las estaciones reales se instalan sobre roca?
4. Comparar con el IGN. Busca en el catálogo del IGN un terremoto reciente y su magnitud. ¿Habría sido detectable con nuestro sensor? Razona con el ruido que has medido.
5. Afinar el detector. Cambia la sensibilidad (2,5 / 3,5 / 5) y cuenta falsas alarmas durante 10 minutos de clase normal. ¿Cuál es el mejor compromiso entre no perderse un terremoto y no dar falsas alarmas?
6. Hipótesis y experimento. «¿Se nota el patio a la hora del recreo?» Formula la hipótesis, graba datos con y sin recreo y diseña cómo comparar los resultados.
7. El gigante. Si un gigante camina a 5 km/h y sus pasos sacuden el suelo, ¿a qué distancia lo detectaríamos? ¿Cuánto tiempo de aviso tendríamos con las ondas S y con las ondas P? Es el mismo cálculo que un sistema de alerta real.
Retos
Reto 1. Haz que funcione. Monta el sensor, lanza el puente y consigue ver la traza en el móvil.
Reto 2. Modifica algo. Cambia el factor de sensibilidad por defecto o los colores de las trazas en la página.
Reto 3. Añade un sensor. Pon un segundo MPU-6050 (dirección 0x69, cable AD0 a 3,3 V) y compara dos puntos de la mesa.
Reto 4. Guarda los datos. Graba una mañana completa de clase en CSV y localiza los momentos de más vibración.
Reto 5. Representa los datos. Dibuja en una hoja de cálculo la amplitud en función de la hora y busca patrones (recreos, cambios de clase).
Reto 6. Automatiza el proceso. Haz que el puente guarde cada evento automáticamente en una carpeta, con la fecha en el nombre.
Reto 7. Haz que otro dispositivo participe. Con dos Arduino o un Arduino y el móvil, mide el mismo golpe en dos sitios y calcula el desfase entre ambos. Es el principio de la triangulación.
Reto 8. Conviértelo en investigación. Deja el sensor una semana, compara los eventos detectados con el catálogo del IGN y escribe un informe: ¿qué vimos?, ¿qué nos perdimos?, ¿por qué? Es ciencia ciudadana, con los límites bien dichos.
Qué hemos aprendido
Hemos aprendido que un terremoto son ondas que viajan por la Tierra, que las P llegan antes que las S y que esa diferencia permite calcular a qué distancia ha ocurrido. Que un acelerómetro como el MPU-6050 mide aceleración, que el STA/LTA compara lo que pasa ahora con lo de antes y que un detector necesita conocer su propio ruido.
Y, sobre todo, hemos aprendido a ser honestos con la medida: nuestro sismógrafo no verá un M4 lejano, no predice nada, y aun así nos ha enseñado cómo piensan los que sí lo hacen.
El Lince apagó la pantalla y apoyó la oreja en la mesa, por costumbre.
—¿Sigues esperando al gigante? —preguntó la Gata.
—Ya no tengo que escuchar el suelo con la oreja. Tengo un aparato que lo hace mejor.
—Y que avisaría… ¿cuánto? ¿Diez segundos?
—Los suficientes para que tú te subas a un árbol y yo prepare el café.
—Qué valiente.
Para seguir investigando
Si te ha gustado ver cómo tiembla el suelo, estos chismes te interesan:
- Chismes del Lince 11: el sismógrafo de la mano, el mismo tipo de sensor, pero con el móvil y el temblor de la mano.
- Chismes del Lince 20: el nivel de burbuja, para entender cómo un sensor distingue la inclinación (la parte lenta que aquí restamos).
- Chismes del Lince 23: estación del tiempo con micro:bit, otra forma de registrar datos del entorno.
Y para ir a las fuentes: la serie sísmica de febrero de 2026 en el IGN, el informe sobre la sismotectónica de la zona, la guía geológica de Málaga y el artículo de Xataka sobre predecir terremotos, que ya hemos enlazado en las secciones anteriores.
Etiqueta:acelerómetro, Arduino Mega, ciencia ciudadana, CSV, IGN, MPU-6050, python, sierra bermeja, sismógrafo, STA/LTA, terremotos



