Un dashboard IoT in tempo reale non è semplicemente una pagina web con grafici. È un sistema completo che coinvolge ingestione dei dati, elaborazione, archiviazione, trasmissione e rendering, tutto ottimizzato affinché i dati arrivino dal sensore all’occhio dell’utente in meno di un secondo.
In questa guida ti spiego come costruirne uno da zero, basandomi sull’esperienza reale di averli costruiti per clienti come Spherag (monitoraggio agricolo 24/7), InfoAdex (55M+ record pubblicitari) e Orquest (gestione della forza lavoro).
Requisiti di un dashboard IoT reale
Prima di scegliere le tecnologie, definisci di cosa ha bisogno il tuo dashboard:
Funzionali
- Mostrare dati aggiornati in tempo reale (< 1 secondo di latenza)
- Visualizzare serie temporali con zoom e navigazione
- Alert visivi quando un valore esce dal range
- Più fonti di dati in una vista unificata
- Filtri per dispositivo, posizione, intervallo temporale
- Esportazione dati (CSV, PDF)
Non funzionali
- Funzionare 24/7 senza degrado delle prestazioni
- Supportare centinaia di widget che si aggiornano simultaneamente
- Renderizzare migliaia di punti dati senza bloccare la UI
- Riconnessione automatica se si perde la connessione
- Responsive: funzionare su schermi di controllo e mobile
- Tempo di caricamento iniziale < 3 secondi
Stack tecnico consigliato
Dopo aver iterato su molteplici progetti, questo è lo stack che consigliamo per un dashboard IoT in tempo reale:
Frontend
| Tecnologia | Per cosa |
|---|---|
| React 18+ | Framework UI con Concurrent Features |
| Recharts o Visx | Grafici di serie temporali performanti |
| D3.js | Visualizzazioni custom complesse |
| TanStack Query | Gestione stato server + cache |
| Socket.IO o WebSocket nativo | Dati in tempo reale |
| Tailwind CSS | Stili rapidi e coerenti |
| Zustand | Stato globale leggero |
Backend
| Tecnologia | Per cosa |
|---|---|
| Node.js + Fastify | API REST + WebSocket server |
| Redis | Cache + pub/sub per broadcast |
| TimescaleDB | Database di serie temporali |
| PostgreSQL | Metadati, configurazioni, utenti |
| Broker MQTT (EMQX/Mosquitto) | Ricezione dati dai dispositivi |
Infrastruttura
| Tecnologia | Per cosa |
|---|---|
| Docker + Docker Compose | Sviluppo locale e deploy |
| Kubernetes | Produzione scalabile |
| Nginx | Reverse proxy + WebSocket upgrade |
| Prometheus + Grafana | Monitoraggio del dashboard stesso |
Architettura dei dati
Flusso dati end-to-end
Dispositivo IoT
↓ (MQTT)
Broker MQTT
↓ (sottoscrizione)
Servizio di ingestione (Node.js)
↓ ↓
TimescaleDB Redis pub/sub
(archiviazione) (tempo reale)
↓ ↓
API REST WebSocket server
↓ ↓
Dashboard (dati storici + dati in tempo reale)
Separare i dati storici dai dati in tempo reale
Questo è il pattern chiave. Il dashboard ha bisogno di due flussi di dati diversi:
- Dati storici: Quando l’utente carica la pagina o cambia l’intervallo temporale, vengono interrogati i dati archiviati in TimescaleDB via API REST
- Dati in tempo reale: Una volta caricata la vista, i nuovi dati arrivano via WebSocket e vengono aggiunti al grafico senza ricaricamento
Questo permette al dashboard di mostrare mesi di storico con zoom efficiente E aggiornarsi in tempo reale con i nuovi dati.
Passo 1: Ingestione dati MQTT
Il primo componente è un servizio che si iscrive al broker MQTT e processa i messaggi in arrivo.
// ingestion-service.js (Node.js)
import mqtt from 'mqtt';
import { pool } from './db.js';
import { redis } from './redis.js';
const client = mqtt.connect(process.env.MQTT_BROKER_URL, {
username: process.env.MQTT_USER,
password: process.env.MQTT_PASS,
clean: false,
clientId: 'dashboard-ingestion-01'
});
client.subscribe('devices/+/telemetry', { qos: 1 });
client.on('message', async (topic, payload) => {
const deviceId = topic.split('/')[1];
const data = JSON.parse(payload.toString());
// 1. Archiviare in TimescaleDB
await pool.query(
`INSERT INTO telemetry (device_id, timestamp, temperature, humidity, battery)
VALUES ($1, NOW(), $2, $3, $4)`,
[deviceId, data.temperature, data.humidity, data.battery]
);
// 2. Pubblicare su Redis per broadcast WebSocket
await redis.publish('realtime:telemetry', JSON.stringify({
deviceId,
timestamp: Date.now(),
...data
}));
// 3. Valutare le regole di alert
await evaluateAlerts(deviceId, data);
});
Questo servizio fa tre cose con ogni messaggio: lo archivia per le query storiche, lo pubblica su Redis affinché il WebSocket server lo trasmetta ai dashboard connessi, e valuta se deve attivare un alert.
Passo 2: WebSocket server
Il WebSocket server mantiene connessioni persistenti con i dashboard e invia loro i nuovi dati in tempo reale.
// websocket-server.js
import { WebSocketServer } from 'ws';
import { redis } from './redis.js';
const wss = new WebSocketServer({ port: 8080 });
const subscriber = redis.duplicate();
// Iscriversi al canale Redis
await subscriber.subscribe('realtime:telemetry');
subscriber.on('message', (channel, message) => {
const data = JSON.parse(message);
// Broadcast a tutti i client connessi
// (in produzione, filtrare per le sottoscrizioni di ogni client)
wss.clients.forEach(client => {
if (client.readyState === 1) {
client.send(message);
}
});
});
wss.on('connection', (ws, req) => {
// Autenticazione
const token = new URL(req.url, 'http://localhost').searchParams.get('token');
if (!verifyToken(token)) {
ws.close(4001, 'Unauthorized');
return;
}
// Heartbeat per rilevare connessioni morte
ws.isAlive = true;
ws.on('pong', () => { ws.isAlive = true; });
});
// Ping periodico per rilevare le disconnessioni
setInterval(() => {
wss.clients.forEach(ws => {
if (!ws.isAlive) return ws.terminate();
ws.isAlive = false;
ws.ping();
});
}, 30000);
WebSocket vs Server-Sent Events (SSE)
- WebSocket: Bidirezionale. Usalo se il dashboard invia comandi al server (filtri dinamici, controllo dispositivi)
- SSE: Unidirezionale server-client. Più semplice, funziona meglio con proxy HTTP. Usalo se il dashboard riceve solo dati
Per la maggior parte dei dashboard IoT, SSE è sufficiente e più semplice da implementare. WebSocket è necessario quando c’è frequente interazione bidirezionale.
Passo 3: API REST per dati storici
Il dashboard ha bisogno di un’API per caricare i dati storici quando l’utente cambia l’intervallo temporale o applica filtri.
// api.js (Fastify)
app.get('/api/telemetry/:deviceId', async (req, reply) => {
const { deviceId } = req.params;
const { from, to, interval } = req.query;
// Downsampling automatico secondo l'intervallo temporale
const bucket = calculateBucket(from, to);
const result = await pool.query(`
SELECT
time_bucket($1, timestamp) AS time,
AVG(temperature) as temperature,
AVG(humidity) as humidity,
MIN(battery) as battery
FROM telemetry
WHERE device_id = $2
AND timestamp BETWEEN $3 AND $4
GROUP BY time
ORDER BY time ASC
`, [bucket, deviceId, from, to]);
return result.rows;
});
function calculateBucket(from, to) {
const diffHours = (new Date(to) - new Date(from)) / 3600000;
if (diffHours <= 1) return '1 minute';
if (diffHours <= 24) return '5 minutes';
if (diffHours <= 168) return '1 hour';
if (diffHours <= 720) return '6 hours';
return '1 day';
}
Il trucco del downsampling è fondamentale: quando l’utente vede un anno di dati, non ha bisogno di 525.600 punti (uno al minuto). Ha bisogno di ~365 punti (uno al giorno). TimescaleDB ha time_bucket nativo che fa questo in modo efficiente.
Passo 4: Frontend React
Hook per dati in tempo reale
// useRealtimeData.js
import { useEffect, useCallback, useRef } from 'react';
import { useQueryClient } from '@tanstack/react-query';
export function useRealtimeData(deviceId) {
const queryClient = useQueryClient();
const wsRef = useRef(null);
useEffect(() => {
const ws = new WebSocket(
`${WS_URL}?token=${getToken()}&device=${deviceId}`
);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
// Aggiornare la cache di TanStack Query
queryClient.setQueryData(
['telemetry', deviceId],
(old) => old ? [...old.slice(-999), data] : [data]
);
};
ws.onclose = () => {
// Riconnessione automatica con backoff esponenziale
setTimeout(() => reconnect(), getBackoff());
};
wsRef.current = ws;
return () => ws.close();
}, [deviceId]);
}
Componente grafico con dati in tempo reale
// TimeSeriesChart.jsx
import { useQuery } from '@tanstack/react-query';
import { LineChart, Line, XAxis, YAxis, Tooltip } from 'recharts';
import { useRealtimeData } from './useRealtimeData';
function TimeSeriesChart({ deviceId, metric, from, to }) {
// Dati storici
const { data: historicalData } = useQuery({
queryKey: ['telemetry', deviceId, from, to],
queryFn: () => fetchTelemetry(deviceId, from, to),
});
// Dati in tempo reale (vengono aggiunti alla fine)
useRealtimeData(deviceId);
return (
<LineChart data={historicalData} width={800} height={300}>
<XAxis dataKey="time" />
<YAxis />
<Tooltip />
<Line
type="monotone"
dataKey={metric}
stroke="#5dd3b3"
dot={false}
isAnimationActive={false}
/>
</LineChart>
);
}
Nota isAnimationActive={false}: nei dashboard con aggiornamenti frequenti, le animazioni di transizione bloccano il render e degradano le prestazioni.
Passo 5: Sistema di alert
// alerts.js
async function evaluateAlerts(deviceId, data) {
const rules = await getAlertRules(deviceId);
for (const rule of rules) {
const value = data[rule.metric];
let triggered = false;
switch (rule.condition) {
case 'above': triggered = value > rule.threshold; break;
case 'below': triggered = value < rule.threshold; break;
case 'equals': triggered = value === rule.threshold; break;
}
if (triggered) {
await createAlert({
deviceId,
ruleId: rule.id,
value,
threshold: rule.threshold,
severity: rule.severity,
timestamp: Date.now()
});
// Notificare via i canali configurati
await notify(rule.channels, {
device: deviceId,
metric: rule.metric,
value,
threshold: rule.threshold
});
}
}
}
Ottimizzazioni di performance
1. Throttling degli aggiornamenti
Se un dispositivo invia dati ogni 100ms, non hai bisogno di aggiornare il dashboard 10 volte al secondo. Raggruppa gli aggiornamenti:
// Nel WebSocket server
const buffer = new Map();
subscriber.on('message', (channel, message) => {
const data = JSON.parse(message);
buffer.set(data.deviceId, data);
});
// Flush ogni 200ms
setInterval(() => {
if (buffer.size > 0) {
const batch = Array.from(buffer.values());
broadcast(JSON.stringify({ type: 'batch', data: batch }));
buffer.clear();
}
}, 200);
2. Virtualizzazione delle liste
Se hai 1.000 dispositivi, non renderizzare 1.000 righe. Usa la virtualizzazione (react-window o react-virtual) per renderizzare solo quelle visibili.
3. Web Workers per elaborazione pesante
Se devi calcolare statistiche o rilevare anomalie nel frontend, fallo in un Web Worker per non bloccare il thread principale.
4. Canvas invece di SVG per molti punti
Recharts usa SVG di default, che si degrada con più di 5.000 punti. Per serie temporali ad alta risoluzione, usa una libreria basata su Canvas (come uPlot o lightweight-charts).
Deploy e operazioni
Docker Compose per lo sviluppo
services:
mqtt:
image: emqx/emqx:latest
ports: ["1883:1883", "8083:8083"]
timescaledb:
image: timescale/timescaledb:latest-pg16
environment:
POSTGRES_PASSWORD: dev
ports: ["5432:5432"]
redis:
image: redis:7-alpine
ports: ["6379:6379"]
ingestion:
build: ./services/ingestion
depends_on: [mqtt, timescaledb, redis]
api:
build: ./services/api
ports: ["3000:3000"]
depends_on: [timescaledb, redis]
dashboard:
build: ./frontend
ports: ["5173:5173"]
Monitoraggio del dashboard stesso
Usa Prometheus per monitorare:
- Numero di connessioni WebSocket attive
- Latenza di scrittura in TimescaleDB
- Messaggi MQTT processati al secondo
- Tempo di risposta dell’API
- Uso della memoria del WebSocket server
Alternative: Grafana vs Custom
Usa Grafana se:
- Il dashboard è per uso interno tecnico
- TimescaleDB/InfluxDB è la tua fonte di dati principale
- Non hai bisogno di UX personalizzata
- Vuoi essere operativo in giorni, non settimane
Costruisci custom se:
- Il dashboard è per utenti finali
- Hai bisogno di branding e UX specifica
- Hai interazioni complesse (comandi a dispositivi, workflow)
- Hai bisogno di funzionalità non supportate da Grafana
In molti progetti, la soluzione migliore è entrambe: Grafana per il team ops e un dashboard custom per gli utenti di business.
Conclusione
Costruire un dashboard IoT in tempo reale non è banale, ma con lo stack corretto e i pattern adeguati (separazione storico/tempo reale, downsampling, throttling), puoi avere un sistema robusto e performante.
I punti chiave:
- Separa i dati storici (API REST + TimescaleDB) dai dati in tempo reale (WebSocket + Redis pub/sub)
- Implementa il downsampling automatico secondo l’intervallo temporale
- Usa il throttling per non saturare il frontend con aggiornamenti
- Progetta il sistema di alert come componente indipendente
- Monitora il dashboard stesso come qualsiasi sistema in produzione
Se hai bisogno di aiuto per costruire il tuo dashboard in tempo reale o la tua piattaforma IoT industriale, in Soamee lo facciamo da anni. Prenota una consulenza gratuita e vediamo il tuo caso.