Raspberry Pi

Dashboard en modo kiosko con Raspberry Pi para datos MQTT en vivo por WebSocket

Convierte una Raspberry Pi y cualquier pantalla HDMI en un dashboard de pared. Una sola página HTML se suscribe a MQTT por WebSocket seguro y Chromium arranca a pantalla completa en modo kiosko en cada inicio.

Pantalla de pared controlada por un dashboard Raspberry Pi en modo kiosko que muestra en vivo la temperatura y el estado de las máquinas por MQTT
Pruébalo en tu propio broker MQTTConsigue un broker MQTT privado y gestionado con el plan Free y empieza con ejemplos de código listos para usar.
Pruébalo gratis Ejemplos de código Sin tarjeta

Un dashboard Raspberry Pi en modo kiosko convierte una placa económica y cualquier monitor HDMI en una pantalla de pared que muestra valores de sensores en vivo sin teclado, ratón ni inicio de sesión. En este tutorial escribirás un dashboard HTML de un solo archivo que se suscribe a MQTT por WebSocket seguro, lo servirás desde la Pi y configurarás Chromium para que arranque a pantalla completa al encender, con el apagado de pantalla desactivado. El resultado sobrevive a los cortes de luz y no necesita más mantenimiento que alguna actualización ocasional del sistema operativo.

Cómo obtiene el kiosko sus datos

El navegador de la Pi se conecta directamente al broker MQTT por WebSocket. No hay servidor backend, ni base de datos, ni polling. Cuando un sensor publica, el broker envía el mensaje a todos los suscriptores, incluida la página del kiosko, y el valor en pantalla cambia en cuestión de instantes.

Este diseño depende de que el broker ofrezca MQTT sobre WebSocket, porque los navegadores no pueden abrir sockets TCP directos. El servidor MQTT público gratuito broker.connectmq.com escucha en el puerto 8084 con TLS (wss://, ruta /mqtt) y en el 8083 sin TLS. Usa siempre el puerto seguro: Chromium bloquea las conexiones WebSocket inseguras desde páginas servidas por HTTPS, y el cifrado aquí no tiene un coste apreciable. La guía de MQTT sobre WebSocket explica los detalles del protocolo.

Los retained messages hacen que el kiosko parezca instantáneo. Si los sensores publican su último estado con el flag retain, el broker entrega esos valores a la página en cuanto se suscribe, de modo que la pantalla está completa un segundo después del arranque en lugar de esperar a la siguiente lectura.

Pieza Qué hace Dónde se ejecuta
Sensores Publican JSON, idealmente retained En cualquier lugar con internet
Broker MQTT Enruta mensajes, guarda valores retained Nube
dashboard.html Se suscribe por WSS, dibuja los paneles Chromium en la Pi
Servidor web local Sirve el archivo HTML Pi, puerto 8080
Chromium en kiosko Navegador a pantalla completa, sin interfaz Sesión de escritorio de la Pi

Qué necesitas

  • Raspberry Pi 4 o 5 (una Pi 3B+ sirve para páginas sencillas) con Raspberry Pi OS with desktop, Bookworm.
  • Un monitor o televisor por HDMI, o la pantalla táctil oficial.
  • Wi-Fi o Ethernet con acceso saliente al puerto 8084.
  • Algo que publique datos MQTT. Para empezar rápido, el publicador de estadísticas del sistema de la Raspberry Pi genera valores reales desde cualquier Pi.

Las imágenes recientes de Bookworm usan un escritorio Wayland (labwc en las versiones actuales, Wayfire en compilaciones anteriores de Bookworm). Los pasos de inicio automático de abajo cubren labwc e indican la alternativa.

Paso 1: escribe la página del dashboard

Crea una carpeta y la página:

mkdir -p ~/kiosk && nano ~/kiosk/dashboard.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Site Dashboard</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
  :root { color-scheme: dark; }
  body { margin: 0; font-family: system-ui, sans-serif; background: #0f1419; color: #e6e6e6; }
  header { display: flex; justify-content: space-between; padding: 16px 28px; font-size: 22px; }
  #status.ok { color: #4caf50; } #status.bad { color: #f44336; }
  main { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; padding: 0 28px 28px; }
  .tile { background: #1b2229; border-radius: 12px; padding: 24px; }
  .label { font-size: 20px; opacity: .7; }
  .value { font-size: 72px; font-weight: 600; margin-top: 8px; }
  .age { font-size: 16px; opacity: .5; margin-top: 6px; }
  .stale .value { opacity: .35; }
</style>
</head>
<body>
<header><span>Site Dashboard</span><span id="clock"></span><span id="status" class="bad">offline</span></header>
<main id="tiles"></main>
<script src="https://unpkg.com/mqtt@5.10.1/dist/mqtt.min.js"></script>
<script>
  const PREFIX = "connectmq/demo/c81f3d";
  const TILES = [
    { topic: PREFIX + "/office/climate",  field: "t",    label: "Office temperature", unit: " °C" },
    { topic: PREFIX + "/office/climate",  field: "rh",   label: "Office humidity",    unit: " %"  },
    { topic: PREFIX + "/pi01/stats",      field: "cpu_temp", label: "Gateway CPU",    unit: " °C" },
    { topic: PREFIX + "/line1/state",     field: null,   label: "Line 1",             unit: ""    }
  ];
  const STALE_MS = 5 * 60 * 1000;

  const tilesEl = document.getElementById("tiles");
  const lastSeen = new Map();
  TILES.forEach((t, i) => {
    const el = document.createElement("div");
    el.className = "tile stale"; el.id = "tile" + i;
    el.innerHTML = '<div class="label"></div><div class="value">--</div><div class="age">waiting</div>';
    el.querySelector(".label").textContent = t.label;
    tilesEl.appendChild(el);
  });

  const client = mqtt.connect("wss://broker.connectmq.com:8084/mqtt", {
    clientId: "kiosk-c81f3d-" + Math.random().toString(16).slice(2, 8),
    protocolVersion: 5,
    clean: true,
    reconnectPeriod: 5000,
    connectTimeout: 10000
    // Broker privado: añade username: "YOUR_USERNAME", password: "YOUR_PASSWORD"
    // y usa clientId: "YOUR_CLIENT_ID"
  });

  const statusEl = document.getElementById("status");
  client.on("connect", () => {
    statusEl.textContent = "live"; statusEl.className = "ok";
    const topics = [...new Set(TILES.map(t => t.topic))];
    client.subscribe(topics, { qos: 1 });
  });
  client.on("close", () => { statusEl.textContent = "reconnecting"; statusEl.className = "bad"; });

  client.on("message", (topic, payload) => {
    const text = payload.toString();
    let data = null;
    try { data = JSON.parse(text); } catch (e) { /* payload de texto plano */ }
    TILES.forEach((t, i) => {
      if (t.topic !== topic) return;
      let v = t.field ? (data ? data[t.field] : undefined) : text;
      if (v === undefined || v === null) return;
      if (typeof v === "number") v = v.toFixed(1);
      const el = document.getElementById("tile" + i);
      el.querySelector(".value").textContent = v + t.unit;
      el.classList.remove("stale");
      lastSeen.set(i, Date.now());
    });
  });

  setInterval(() => {
    document.getElementById("clock").textContent = new Date().toLocaleTimeString();
    TILES.forEach((t, i) => {
      const seen = lastSeen.get(i);
      const el = document.getElementById("tile" + i);
      if (!seen) return;
      const age = Math.round((Date.now() - seen) / 1000);
      el.querySelector(".age").textContent = age < 60 ? "updated " + age + " s ago" : "updated " + Math.round(age / 60) + " min ago";
      el.classList.toggle("stale", Date.now() - seen > STALE_MS);
    });
  }, 1000);
</script>
</body>
</html>

La página hace tres cosas importantes en una pantalla desatendida. Muestra un indicador de conexión para que cualquiera que pase sepa si los números están en vivo. Atenúa los paneles que no se han actualizado en cinco minutos, lo que evita que un sensor caído muestre en silencio un valor antiguo como si fuera actual. Y se reconecta sola cada cinco segundos tras una caída de red.

Si prefieres no cargar MQTT.js desde un CDN en cada arranque, descarga el archivo una vez en ~/kiosk/ y cambia el src a mqtt.min.js. Así el kiosko solo necesita internet para la propia conexión con el broker.

Si más adelante quieres ir más allá de los paneles numéricos, los mismos datos MQTT pueden alimentar un gemelo digital 3D de la planta o del edificio que se muestre en la misma pantalla.

Paso 2: sirve la página en local

Chromium puede abrir URLs file://, pero servir la página por HTTP desde la Pi evita restricciones extrañas del navegador. El servidor integrado de Python es suficiente. Crea un servicio systemd para que arranque al iniciar:

sudo tee /etc/systemd/system/kiosk-web.service >/dev/null <<'EOF'
[Unit]
Description=Kiosk dashboard web server
After=network-online.target
Wants=network-online.target

[Service]
User=pi
WorkingDirectory=/home/pi/kiosk
ExecStart=/usr/bin/python3 -m http.server 8080 --bind 127.0.0.1
Restart=always

[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now kiosk-web

Sustituye pi por tu nombre de usuario si es distinto. Vincular a 127.0.0.1 mantiene la página accesible solo desde la propia Pi.

Paso 3: inicia Chromium en modo kiosko en la Raspberry Pi

Primero asegúrate de que la Pi arranca directamente en el escritorio con inicio de sesión automático: ejecuta sudo raspi-config, abre System Options, Boot / Auto Login y elige Desktop Autologin. Ya que estás ahí, abre Display Options, Screen Blanking y desactívalo.

Crea un pequeño script de arranque:

nano ~/kiosk/start-kiosk.sh
#!/bin/bash
# Esperar al servidor web local y a la red
for i in $(seq 1 30); do
  curl -s -o /dev/null http://127.0.0.1:8080/dashboard.html && break
  sleep 2
done

BROWSER=$(command -v chromium-browser || command -v chromium)
exec "$BROWSER" \
  --kiosk \
  --noerrdialogs \
  --disable-infobars \
  --disable-session-crashed-bubble \
  --check-for-update-interval=31536000 \
  --ozone-platform=wayland \
  http://127.0.0.1:8080/dashboard.html
chmod +x ~/kiosk/start-kiosk.sh

En labwc, añade el script al archivo de inicio automático del compositor:

mkdir -p ~/.config/labwc
echo "/home/pi/kiosk/start-kiosk.sh &" >> ~/.config/labwc/autostart

En una compilación antigua de Bookworm con Wayfire, añade esto a ~/.config/wayfire.ini en su lugar:

[autostart]
kiosk = /home/pi/kiosk/start-kiosk.sh

Si volviste a X11 en raspi-config, elimina el flag --ozone-platform=wayland y usa ~/.config/autostart/kiosk.desktop con una línea Exec= que apunte al script. Reinicia y el dashboard debería ocupar toda la pantalla.

Cómo blindar un dashboard Raspberry Pi en modo kiosko desatendido

Un kiosko funciona durante meses sin que nadie lo mire, así que prevé los fallos aburridos:

  1. Cierres inesperados. El flag --disable-session-crashed-bubble evita la barra de "restaurar páginas" tras un corte de luz. Para mayor seguridad, envuelve el exec en un bucle para que Chromium se reinicie si se cierra.
  2. Crecimiento de memoria. Las pestañas que llevan mucho tiempo abiertas pueden crecer. Un reinicio nocturno con sudo crontab -e y 0 4 * * * /sbin/reboot es una solución tosca pero fiable.
  3. Desgaste de la tarjeta SD. Chromium escribe su perfil constantemente. Plantéate activar el sistema de archivos overlay en raspi-config cuando la configuración sea definitiva; así el sistema de archivos raíz queda en solo lectura.
  4. Quemado de pantalla. Los diseños estáticos en pantallas OLED o de plasma pueden dejar imágenes fantasma. Usa fondos oscuros y apaga la pantalla por la noche con wlr-randr --output HDMI-A-1 --off desde un script programado.
  5. Credenciales en pantalla. Todo lo que haya en el código fuente de la página es visible para cualquiera con acceso físico. Usa un cliente dedicado con sus propias credenciales para el kiosko.

Producción: un broker privado para el kiosko

El servidor público es ideal mientras desarrollas, pero cualquiera puede leer y escribir en sus topics, así que una pantalla de pared real debería suscribirse a tu propio broker. Un broker MQTT gestionado como ConnectMQ te ofrece un broker privado con WebSocket y WebSocket seguro activados de serie. En el portal creas un broker y recibes un nombre de usuario, una contraseña y un client ID; colócalos en las opciones de mqtt.connect y usa el client ID emitido tal cual, porque ConnectMQ vincula los client IDs a las credenciales. Como la página usa una sola conexión, cabe sin problemas en el plan gratuito Serverless junto con tus sensores.

Antes de conectarlo todo al kiosko, el cliente MQTT en el navegador y el explorador de topics en vivo del portal de ConnectMQ resultan muy útiles para comprobar que los sensores publican los topics y campos JSON que esperan tus paneles.

Solución de problemas

Síntoma Causa Solución
El estado se queda en "offline" Puerto 8084 bloqueado o ruta incorrecta Usa wss://...:8084/mqtt y revisa el firewall
Los paneles muestran -- indefinidamente Topic o campo no coinciden Suscríbete con mosquitto_sub -v y compara
Los valores tardan minutos en aparecer Los sensores no usan retain Publica el último valor con el flag retain
Pantalla negra tras el arranque No se lee el archivo de inicio automático Comprueba qué compositor se ejecuta con echo $XDG_SESSION_DESKTOP
La pantalla se apaga a los 10 minutos Apagado de pantalla activado Desactívalo en raspi-config, Display Options

Empieza con ConnectMQ

Dale a tu kiosko un flujo privado y protegido con TLS: crea un broker en app.connectmq.com con el plan gratuito Serverless, sin tarjeta de crédito, y pega las credenciales en dashboard.html. Para construir una versión táctil que también envíe comandos, continúa con el tutorial de pantalla MQTT con Raspberry Pi, o vuelve a lo básico con el tutorial de MQTT con Raspberry Pi.

Preguntas frecuentes

¿Por qué mi kiosko necesita MQTT sobre WebSocket en lugar del puerto 1883?

Los navegadores solo pueden abrir conexiones HTTP y WebSocket, no sockets TCP directos, así que una página web no puede usar el puerto 1883. El WebSocket seguro en el puerto 8084 es la forma estándar de suscribirse desde el navegador.

¿Puedo usar Grafana en lugar de una página HTML propia?

Sí, abre la URL de un dashboard de Grafana en modo kiosko de la misma forma. Una página propia es más ligera y se actualiza al instante, mientras que Grafana es mejor cuando necesitas histórico y gráficas.

¿Cómo salgo del modo kiosko de Chromium?

Conecta un teclado y pulsa Alt+F4, o entra por SSH en la Pi y ejecuta pkill chromium. Elimina la línea de inicio automático si quieres recuperar el escritorio normal.

¿Sigue funcionando el kiosko si se cae internet?

La página permanece en pantalla, se marca como reconectando y atenúa los paneles a medida que quedan desactualizados. Cuando vuelve la conexión, MQTT.js se reconecta y los valores retained refrescan la pantalla.

Artículos relacionados

Fiabilidad de la Raspberry Pi: cómo usar una Pi 24/7 como gateway IoT

Una guía práctica para mantener un gateway MQTT con Raspberry Pi funcionando durante meses: desgaste del almacenamiento, sistemas de archivos de solo lectura, alimentación, refrigeración, watchdogs de hardware y software y monitorización remota de la salud.

9 min de lectura

Pico W vs Pico 2 W para MQTT: rendimiento, memoria y TLS de RP2040 frente a RP2350

La Pico 2 W mantiene el formato y el chip Wi-Fi de la Pico W, pero sustituye el RP2040 por el RP2350, más rápido. Esta comparativa analiza qué significa para los dispositivos MQTT: CPU, memoria, TLS, funciones de seguridad, soporte de software y qué placa comprar para cada proyecto.

9 min de lectura

Registro de accesos Raspberry Pi RFID MQTT con el lector RC522

Conecta un lector RFID RC522 a una Raspberry Pi, lee etiquetas en Python, compáralas con una lista blanca local y publica cada intento de acceso como un evento MQTT que pueden consumir registradores, dashboards y herramientas de alertas.

8 min de lectura

Todos los artículos →

Tu broker MQTT privado, a unos pocos clics

Crea una cuenta, obtén las credenciales de tu broker y conecta tu primer dispositivo, sin servidores que instalar, parchear ni escalar.