Um dashboard quiosque com Raspberry Pi transforma uma placa barata e qualquer monitor HDMI em um painel de parede que mostra valores de sensores ao vivo, sem teclado, mouse ou login. Neste tutorial você vai escrever um dashboard HTML em um único arquivo que assina o MQTT via WebSocket seguro, servi-lo a partir do Pi e configurar o Chromium para abrir em tela cheia no boot, com o desligamento de tela desativado. O resultado sobrevive a quedas de energia e não exige manutenção além de uma atualização ocasional do sistema, seja no escritório, na recepção ou na linha de produção de uma planta.
Como o quiosque recebe os dados
O navegador no Pi se conecta diretamente ao broker MQTT via WebSocket. Não há servidor de backend, banco de dados ou polling. Quando um sensor publica, o broker empurra a mensagem para todos os assinantes, inclusive a página do quiosque, e o valor na tela muda em instantes.
Esse design depende de o broker oferecer MQTT over WebSocket, porque os navegadores não conseguem abrir sockets TCP brutos. O servidor MQTT público em broker.connectmq.com escuta na porta 8084 com TLS (wss://, caminho /mqtt) e na 8083 sem TLS. Use sempre a porta segura: o Chromium bloqueia conexões WebSocket inseguras a partir de páginas servidas por HTTPS, e a criptografia não tem custo perceptível aqui. O guia de MQTT over WebSocket explica os detalhes do protocolo.
As retained messages fazem o quiosque parecer instantâneo. Se os sensores publicarem seu último estado com a flag retain, o broker entrega esses valores à página no momento em que ela assina, então a tela fica completamente preenchida um segundo depois do boot, em vez de esperar pela próxima leitura.
| Peça | O que faz | Onde roda |
|---|---|---|
| Sensores | Publicam JSON, de preferência retained | Em qualquer lugar com internet |
| Broker MQTT | Roteia mensagens, guarda valores retained | Nuvem |
dashboard.html |
Assina via WSS, renderiza os blocos | Chromium no Pi |
| Servidor web local | Serve o arquivo HTML | Pi, porta 8080 |
| Chromium em quiosque | Navegador em tela cheia, sem interface | Sessão de desktop do Pi |
O que você precisa
- Raspberry Pi 4 ou 5 (um Pi 3B+ funciona para páginas simples) com Raspberry Pi OS with desktop, Bookworm.
- Um monitor ou TV via HDMI, ou o display touch oficial.
- Wi-Fi ou Ethernet com acesso de saída à porta 8084.
- Algo publicando dados MQTT. Para começar rápido, o publicador de estatísticas do sistema do Raspberry Pi gera valores reais a partir de qualquer Pi.
As imagens Bookworm recentes rodam um desktop Wayland (labwc nas versões atuais, Wayfire nas builds Bookworm mais antigas). Os passos de inicialização automática abaixo cobrem o labwc e indicam a alternativa.
Passo 1: escreva a página do dashboard
Crie uma pasta e a 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
// Private broker: add username: "YOUR_USERNAME", password: "YOUR_PASSWORD"
// and use 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) { /* plain string payload */ }
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>
A página faz três coisas que importam em uma tela sem supervisão. Ela mostra um indicador de conexão, para que qualquer pessoa que passe saiba se os números estão ao vivo. Ela deixa em cinza os blocos que não foram atualizados em cinco minutos, o que impede que um sensor morto exiba silenciosamente um valor antigo como se fosse atual. E ela se reconecta sozinha a cada cinco segundos depois de uma queda de rede.
Se preferir não carregar o MQTT.js de uma CDN a cada boot, baixe o arquivo uma vez para ~/kiosk/ e mude o src para mqtt.min.js. O quiosque então só precisa de internet para a conexão com o broker.
Passo 2: sirva a página localmente
O Chromium consegue abrir URLs file://, mas servir por HTTP a partir do Pi evita restrições estranhas do navegador. O servidor embutido do Python é suficiente. Crie um serviço do systemd para que ele suba no boot:
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
Substitua pi pelo seu nome de usuário, se for diferente. Fazer o bind em 127.0.0.1 mantém a página acessível apenas no próprio Pi.
Passo 3: inicie o Chromium no modo quiosque do Raspberry Pi
Primeiro garanta que o Pi inicie direto no desktop com login automático: execute sudo raspi-config, abra System Options, Boot / Auto Login e escolha Desktop Autologin. Aproveite e abra Display Options, Screen Blanking para desativá-lo.
Crie um pequeno script de inicialização:
nano ~/kiosk/start-kiosk.sh
#!/bin/bash
# Aguarda o servidor web local e a rede
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
No labwc, adicione o script ao arquivo de autostart do compositor:
mkdir -p ~/.config/labwc
echo "/home/pi/kiosk/start-kiosk.sh &" >> ~/.config/labwc/autostart
Em uma build Bookworm mais antiga que roda Wayfire, adicione isto a ~/.config/wayfire.ini:
[autostart]
kiosk = /home/pi/kiosk/start-kiosk.sh
Se você voltou o Pi para X11 no raspi-config, remova a flag --ozone-platform=wayland e use ~/.config/autostart/kiosk.desktop com uma linha Exec= apontando para o script. Reinicie e o dashboard deve ocupar a tela inteira.
Blindando um dashboard quiosque com Raspberry Pi sem supervisão
Um quiosque roda por meses sem ninguém olhar para ele, então planeje-se para as falhas sem graça:
- Travamentos. A flag
--disable-session-crashed-bubbleimpede a barra de "restaurar páginas" depois de uma queda de energia. Para mais segurança, coloque oexecdentro de um loop para que o Chromium reinicie se for encerrado. - Crescimento de memória. Abas de navegador abertas por muito tempo podem crescer. Um reboot noturno via
sudo crontab -ecom0 4 * * * /sbin/rebooté uma solução grosseira, mas confiável. - Desgaste do cartão SD. O Chromium grava um perfil o tempo todo. Considere ativar o overlay file system no raspi-config quando a configuração estiver finalizada, o que deixa o sistema de arquivos raiz somente leitura.
- Marcação de tela (burn-in). Layouts estáticos em telas OLED ou de plasma podem deixar imagens fantasmas. Use fundos escuros e desligue o display à noite com
wlr-randr --output HDMI-A-1 --offa partir de um script agendado. - Credenciais na tela. Tudo o que está no código-fonte da página fica visível para quem tem acesso físico. Use um cliente dedicado, com credenciais próprias, para o quiosque.
Produção: um broker privado para o quiosque
O servidor público é ideal durante a montagem, mas qualquer pessoa pode ler e escrever nos topics dele, então um painel de parede de verdade deve assinar o seu próprio broker. Um broker MQTT gerenciado como o ConnectMQ oferece um broker privado com WebSocket e WebSocket seguro ativados de fábrica. No portal você cria um broker e recebe usuário, senha e client ID; coloque-os nas opções do mqtt.connect e use exatamente o client ID emitido, porque o ConnectMQ vincula os client IDs às credenciais. Como a página usa uma única conexão, ela cabe com folga no plano Serverless gratuito ao lado dos seus sensores.
Antes de ligar tudo ao quiosque, o cliente MQTT no navegador e o explorador de topics ao vivo do portal ConnectMQ são úteis para verificar se os sensores publicam os topics e campos JSON que os seus blocos esperam.
Solução de problemas
| Sintoma | Causa | Solução |
|---|---|---|
| O status fica em "offline" | Porta 8084 bloqueada ou caminho errado | Use wss://...:8084/mqtt e verifique o firewall |
Os blocos mostram -- para sempre |
Topic ou campo não correspondem | Assine com mosquitto_sub -v e compare |
| Os valores só aparecem depois de minutos | Os sensores não usam retain | Publique o último valor com a flag retain |
| Tela preta depois do boot | Arquivo de autostart não lido | Verifique qual compositor roda com echo $XDG_SESSION_DESKTOP |
| A tela desliga após 10 minutos | Screen blanking ativado | Desative no raspi-config, Display Options |
Comece com o ConnectMQ
Dê ao seu quiosque um feed privado e protegido por TLS: crie um broker em app.connectmq.com no plano Serverless gratuito, sem cartão de crédito, e cole as credenciais no dashboard.html. Para montar uma versão touchscreen que também envia comandos, continue com o tutorial de display MQTT com Raspberry Pi, ou volte ao básico com o tutorial MQTT com Raspberry Pi.
Perguntas frequentes
Por que meu quiosque precisa de MQTT over WebSocket em vez da porta 1883?
Os navegadores só abrem conexões HTTP e WebSocket, não sockets TCP brutos, então uma página web não consegue usar a porta 1883 diretamente. O WebSocket seguro na porta 8084 é a forma padrão de assinar a partir do navegador.
Posso usar o Grafana em vez de uma página HTML própria?
Sim, abra a URL de um dashboard do Grafana no modo quiosque da mesma forma. Uma página própria é mais leve e atualiza instantaneamente, enquanto o Grafana é melhor quando você precisa de histórico e gráficos.
Como saio do modo quiosque do Chromium?
Conecte um teclado e pressione Alt+F4, ou acesse o Pi por SSH e execute pkill chromium. Remova a linha de autostart se quiser o desktop normal de volta.
O quiosque continua funcionando se a internet cair?
A página continua na tela, se marca como reconectando e deixa os blocos em cinza conforme ficam desatualizados. Quando a conexão volta, o MQTT.js se reconecta e os valores retained atualizam a tela.



