Introduction
Un Digital Twin (jumeau numérique) est une représentation virtuelle fidèle d'un objet physique, mise à jour en temps réel grâce à des données de capteurs. Dans l'IoT, il permet de simuler, prédire et optimiser le comportement d'équipements sans risque sur le monde réel. Par exemple, un Digital Twin d'un capteur de température industriel mirror ses variations, alerte sur les anomalies et teste des scénarios virtuels.
Ce tutoriel beginner vous guide pour créer un Digital Twin basique d'un capteur de température avec Node.js, Express pour l'API et Socket.io pour les mises à jour temps réel. Imaginez un thermostat physique : son twin affiche la température actuelle, simule des fluctuations et permet un contrôle virtuel. À la fin, vous aurez un prototype fonctionnel, scalable vers des cas réels comme l'industrie 4.0. Pourquoi c'est crucial en 2026 ? Les Digital Twins réduisent les coûts de maintenance de 20-30% selon Gartner, et ce setup est idéal pour portfolios ou prototypes rapides. (142 mots)
Prérequis
- Node.js 20+ installé (téléchargez ici)
- Éditeur de code comme VS Code
- Connaissances basiques en JavaScript (variables, fonctions, async)
- Terminal (cmd ou bash)
- Pas d'expérience IoT requise
Initialisation du projet
mkdir digital-twin-app
cd digital-twin-app
npm init -y
npm install express socket.io cors
npm install -D nodemonCette commande crée un dossier projet, initialise npm et installe les dépendances essentielles : Express pour le serveur HTTP, Socket.io pour le temps réel bidirectionnel, CORS pour les requêtes cross-origin. Nodemon est un dev tool qui redémarre auto le serveur lors des changements de code.
Configuration package.json
{
"name": "digital-twin-app",
"version": "1.0.0",
"description": "Digital Twin simple avec Node.js",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
},
"dependencies": {
"express": "^4.19.2",
"socket.io": "^4.7.5",
"cors": "^2.8.5"
},
"devDependencies": {
"nodemon": "^3.1.0"
}
}Ce package.json complet définit les scripts : npm run dev pour développement avec hot-reload. Il liste précisément les versions stables 2026-compatibles. Copiez-collez pour un setup immédiat, évitant les conflits de versions.
Comprendre le flux du Digital Twin
Le twin physique est simulé par un loop qui génère des données de température aléatoires (18-35°C). Le twin digital les reçoit via Socket.io, les stocke et les visualise. Analogie : comme un miroir qui reflète vos mouvements en live. L'API REST permet des queries (GET /status), tandis que WebSockets push les updates toutes les 2s pour fluidité.
Serveur principal avec simulation
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const cors = require('cors');
const app = express();
const server = http.createServer(app);
const io = socketIo(server, {
cors: { origin: "*" }
});
app.use(cors());
app.use(express.static('public'));
// État du Digital Twin
let twinState = {
temperature: 20,
humidity: 50,
status: 'nominal',
timestamp: new Date().toISOString()
};
// Simulation physique (loop toutes les 2s)
setInterval(() => {
twinState.temperature = Math.max(18, Math.min(35, twinState.temperature + (Math.random() - 0.5) * 2));
twinState.humidity = Math.max(30, Math.min(80, twinState.humidity + (Math.random() - 0.5) * 5));
twinState.status = twinState.temperature > 30 ? 'alerte' : 'nominal';
twinState.timestamp = new Date().toISOString();
io.emit('update', twinState); // Broadcast au twin digital
}, 2000);
// API REST
app.get('/api/status', (req, res) => {
res.json(twinState);
});
io.on('connection', (socket) => {
console.log('Client connecté');
socket.emit('update', twinState); // Sync initial
});
server.listen(3000, () => {
console.log('Serveur sur http://localhost:3000');
});Ce serveur Express+Socket.io simule le 'physique' via setInterval : température fluctue réalistement (±1°C aléatoire). Broadcast via io.emit met à jour tous les clients. GET /api/status fournit un snapshot synchrone. Piège : sans CORS, le frontend bloque ; ici configuré pour * en dev.
Interface client HTML
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Digital Twin - Capteur Température</title>
<style>
body { font-family: Arial; max-width: 600px; margin: 50px auto; }
.gauge { width: 200px; height: 100px; border: 5px solid #ddd; border-radius: 50px; margin: 20px 0; display: flex; align-items: center; justify-content: center; font-size: 24px; }
.alert { background: #ff6b6b; color: white; }
#log { height: 200px; overflow-y: scroll; border: 1px solid #ccc; padding: 10px; }
</style>
</head>
<body>
<h1>Digital Twin : Capteur IoT</h1>
<div>Temperature: <div id="temp" class="gauge">20°C</div></div>
<div>Humidité: <span id="humidity">50%</span></div>
<div>Statut: <span id="status">nominal</span></div>
<div>Dernier update: <span id="timestamp"></span></div>
<button onclick="fetchStatus()">Refresh API</button>
<div id="log"></div>
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
socket.on('update', (data) => {
document.getElementById('temp').textContent = data.temperature.toFixed(1) + '°C';
document.getElementById('humidity').textContent = data.humidity + '%';
document.getElementById('status').textContent = data.status;
document.getElementById('timestamp').textContent = new Date(data.timestamp).toLocaleString('fr-FR');
document.getElementById('temp').className = data.status === 'alerte' ? 'gauge alert' : 'gauge';
log('Update reçu: ' + JSON.stringify(data));
});
function fetchStatus() {
fetch('/api/status').then(res => res.json()).then(data => {
socket.emit('update', data); // Trigger visu
log('API fetch: ' + JSON.stringify(data));
});
}
function log(msg) {
const logDiv = document.getElementById('log');
logDiv.innerHTML += '<p>' + new Date().toLocaleTimeString() + ': ' + msg + '</p>';
logDiv.scrollTop = logDiv.scrollHeight;
}
</script>
</body>
</html>Ce HTML autonome intègre Socket.io client-side pour live updates. Gauges CSS visuelles changent de couleur en alerte (>30°C). Bouton teste l'API REST. Log console-like pour debug. Créez le dossier 'public/' avant ; sinon 404 sur static files.
Lancement et test
Démarrez avec npm run dev. Ouvrez http://localhost:3000. Observez les updates auto toutes 2s, testez le bouton Refresh. Le twin 'physique' (serveur) génère, le 'digital' (browser) reflète parfaitement. Analogie : un coach virtuel qui suit vos runs en live.
Ajout de contrôle bidirectionnel
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const cors = require('cors');
const app = express();
const server = http.createServer(app);
const io = socketIo(server, {
cors: { origin: "*" }
});
app.use(cors());
app.use(express.static('public'));
app.use(express.json());
let twinState = {
temperature: 20,
humidity: 50,
setpoint: 22,
status: 'nominal',
timestamp: new Date().toISOString()
};
setInterval(() => {
// Simulation influencée par setpoint
const delta = twinState.setpoint - twinState.temperature;
twinState.temperature += (Math.random() - 0.5) + (delta > 0 ? 0.1 : -0.1);
twinState.temperature = Math.max(18, Math.min(35, twinState.temperature));
twinState.humidity = Math.max(30, Math.min(80, twinState.humidity + (Math.random() - 0.5) * 3));
twinState.status = twinState.temperature > 30 ? 'alerte' : 'nominal';
twinState.timestamp = new Date().toISOString();
io.emit('update', twinState);
}, 2000);
app.get('/api/status', (req, res) => res.json(twinState));
app.post('/api/setpoint', (req, res) => {
twinState.setpoint = Math.max(18, Math.min(28, parseFloat(req.body.setpoint)));
res.json({ success: true, setpoint: twinState.setpoint });
});
io.on('connection', (socket) => {
socket.emit('update', twinState);
socket.on('setSetpoint', (setpoint) => {
twinState.setpoint = Math.max(18, Math.min(28, setpoint));
socket.emit('update', twinState);
});
});
server.listen(3000, () => {
console.log('Serveur sur http://localhost:3000');
});Amélioration : ajoute POST /api/setpoint et socket 'setSetpoint' pour contrôle du twin (setpoint influence simulation). Le loop réagit au setpoint comme un vrai thermostat. Sécurisé avec bornes 18-28°C. Remplacez server.js pour bidirectionnalité.
Client avec contrôles interactifs
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Digital Twin - Capteur Température</title>
<style>
body { font-family: Arial; max-width: 600px; margin: 50px auto; }
.gauge { width: 200px; height: 100px; border: 5px solid #ddd; border-radius: 50px; margin: 20px 0; display: flex; align-items: center; justify-content: center; font-size: 24px; }
.alert { background: #ff6b6b; color: white; }
#log { height: 200px; overflow-y: scroll; border: 1px solid #ccc; padding: 10px; }
input { padding: 10px; font-size: 16px; }
</style>
</head>
<body>
<h1>Digital Twin : Capteur IoT Interactif</h1>
<div>Temperature: <div id="temp" class="gauge">20°C</div></div>
<div>Humidité: <span id="humidity">50%</span></div>
<div>Setpoint: <span id="setpoint">22°C</span></div>
<div>Statut: <span id="status">nominal</span></div>
<div>Dernier update: <span id="timestamp"></span></div>
<input type="number" id="newSetpoint" min="18" max="28" value="22" placeholder="Nouveau setpoint">
<button onclick="setSetpoint()">Appliquer Setpoint</button>
<button onclick="fetchStatus()">Refresh API</button>
<div id="log"></div>
<script src="/socket.io/socket.io.js"></script>
<script>
const socket = io();
socket.on('update', (data) => {
document.getElementById('temp').textContent = data.temperature.toFixed(1) + '°C';
document.getElementById('humidity').textContent = data.humidity + '%';
document.getElementById('setpoint').textContent = data.setpoint + '°C';
document.getElementById('status').textContent = data.status;
document.getElementById('timestamp').textContent = new Date(data.timestamp).toLocaleString('fr-FR');
document.getElementById('temp').className = data.status === 'alerte' ? 'gauge alert' : 'gauge';
log('Update: ' + JSON.stringify(data));
});
function setSetpoint() {
const setpoint = parseFloat(document.getElementById('newSetpoint').value);
socket.emit('setSetpoint', setpoint);
log('Setpoint envoyé: ' + setpoint);
}
function fetchStatus() {
fetch('/api/status').then(res => res.json()).then(data => {
log('API: ' + JSON.stringify(data));
});
}
function log(msg) {
const logDiv = document.getElementById('log');
logDiv.innerHTML += '<p>' + new Date().toLocaleTimeString() + ': ' + msg + '</p>';
logDiv.scrollTop = logDiv.scrollHeight;
}
</script>
</body>
</html>Ajout input setpoint + bouton pour contrôle via Socket.io. Le twin réagit : temp converge vers setpoint. Teste POST indirectement. Remplacez index.html ; visu enrichie avec setpoint display.
Bonnes pratiques
- Sécurisez les données : Ajoutez validation Zod/ Joi sur API pour bornes réalistes.
- Scalez avec DB : Remplacez l'état mémoire par Redis/MongoDB pour persistance.
- Monitoring : Intégrez Prometheus pour metrics twin (latence updates).
- Sécurité prod : HTTPS + auth JWT sur sockets ; limitez CORS à domaines connus.
- Tests : Ajoutez Jest pour simuler loops et connexions.
Erreurs courantes à éviter
- Pas de CORS : Frontend bloque ; toujours configurer cors() ou io.cors.
- Mémoire leaks : setInterval sans clearInterval fuit ; utilisez variables refs.
- Pas de validation : Setpoints extrêmes crashent simu ; clamp avec Math.min/max.
- Sync initial manquant : Clients rejoignent sans état ; emit('update') sur connection.
Pour aller plus loin
Maîtrisez les Digital Twins avancés avec Unity pour 3D ou AWS IoT TwinMaker. Étudiez notre formation IoT Node.js. Ressources : Docs Socket.io, Gartner Digital Twins. Prochain : intégrez ML pour prédictions !