Ein digitaler Zwilling eines Förderbands zeigt auf einen Blick, ob eine Linie läuft, wie schnell und wo sie feststeckt. In diesem Tutorial bauen Sie ihn mit Three.js im Browser: Rollen drehen sich und Pakete fahren mit der realen, per MQTT gemeldeten Bandgeschwindigkeit, ein Stausignal stoppt die Pakete und färbt das Band rot, und Bediener können Start-, Stopp- und Reset-Befehle an die Linie zurücksenden. Ein Python-Simulator veröffentlicht realistische Förderbanddaten auf broker.connectmq.com, sodass Sie alles bauen und testen können, bevor Sie eine SPS anbinden.
Warum sich ein Förderband gut für einen digitalen Zwilling eignet
Förderbänder gibt es überall in Fertigung, Lager und Logistik, und sie fallen auf sichtbare, kostspielige Weise aus: Ein verklemmter Karton an einer Zusammenführung kann alles stromaufwärts stoppen. Die Signale sind meist schon in der SPS vorhanden und lassen sich ganz natürlich auf Bewegung abbilden:
| Signal | Typische Quelle | Einheit | Verhalten des Zwillings |
|---|---|---|---|
| Bandgeschwindigkeit | Antriebsrückmeldung oder Encoder | m/s | Rollen drehen sich, Pakete bewegen sich |
| Läuft | Motorschütz oder Antriebsstatus | Boolean | Bandfarbe, Bewegung an/aus |
| Stau | Lichtschranke länger als ein Schwellwert belegt | Boolean plus Zone | Band wird rot, Pakete stoppen in der Stauzone |
| Paketzähler | Lichtschranke am Auslauf | Anzahl | Durchsatzzähler |
| Motorstrom | Antrieb | A | Optional, Frühwarnung vor Überlast |
Weil das visuelle Verhalten direkt aus der Physik folgt (eine Rolle mit Radius r dreht sich mit v / r Radiant pro Sekunde), ist ein Förderband-Zwilling auch eine gute Übung, um Animationen aus realen Einheiten statt aus beliebigen Zahlen anzutreiben. Das Gesamtbild vermittelt unser Leitfaden zur Digital-Twin-Technologie.
Topic-Struktur
Wählen Sie ein eindeutiges Präfix; das Beispiel verwendet connectmq/demo/conv-7f3a. Im Werk ersetzt die ISA-95-Hierarchie dieses Präfix: Das Förderband bekommt seinen Platz im Unified Namespace (UNS), etwa acme/werk-leipzig/intralogistik/linie-1/foerderband-1/telemetry, sodass Zwilling, MES und Instandhaltung dieselbe Adresse nutzen. Mehr dazu in Sparkplug B und Unified Namespace.
| Topic | Richtung | Payload |
|---|---|---|
.../line1/conv1/telemetry |
Linie zum Zwilling | {"speed":0.8,"running":true,"jam":false,"jamZone":null,"count":1532,"current":4.1} |
.../line1/conv1/cmd |
Zwilling zur Linie | START, STOP, RESET |
.../line1/conv1/status |
Linie, retained, Last Will | online / offline |
Ein kombiniertes Telemetrieobjekt statt eines Topics pro Signal garantiert, dass der Zwilling nie eine inkonsistente Mischung sieht (zum Beispiel running: true aus einer Nachricht und speed: 0 aus einer älteren). Die Abwägungen zwischen kombinierten Topics und Topics pro Signal behandelt MQTT für Digital Twins.
Schritt 1: Der Förderband-Simulator
Der Simulator bildet ein Förderband mit drei Zonen ab, das mit 0,8 m/s läuft, Pakete zählt, gelegentlich in einer zufälligen Zone staut und Befehle befolgt. Er verwendet paho-mqtt 2.x (pip install "paho-mqtt>=2.0").
import json
import random
import time
import paho.mqtt.client as mqtt
P = "connectmq/demo/conv-7f3a/line1/conv1"
NOMINAL_SPEED = 0.8 # m/s
state = {"running": True, "jam": False, "jamZone": None, "speed": 0.0, "count": 0}
def on_connect(client, userdata, flags, reason_code, properties):
print("connected:", reason_code)
client.publish(f"{P}/status", "online", qos=1, retain=True)
client.subscribe(f"{P}/cmd", qos=1)
def on_message(client, userdata, msg):
cmd = msg.payload.decode().strip().upper()
print("command:", cmd)
if cmd == "START" and not state["jam"]:
state["running"] = True
elif cmd == "STOP":
state["running"] = False
elif cmd == "RESET":
state["jam"], state["jamZone"] = False, None
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="conv-sim-7f3a")
client.will_set(f"{P}/status", "offline", qos=1, retain=True)
client.on_connect = on_connect
client.on_message = on_message
client.connect("broker.connectmq.com", 1883, keepalive=30)
client.loop_start()
while True:
if state["running"] and not state["jam"] and random.random() < 0.01:
state["jam"], state["jamZone"] = True, random.choice([1, 2, 3])
state["running"] = False # die SPS stoppt das Band bei einem Stau
target = NOMINAL_SPEED if state["running"] else 0.0
state["speed"] += (target - state["speed"]) * 0.5 # Rampe wie bei einem Antrieb
if state["speed"] > 0.05 and random.random() < state["speed"] / 1.6:
state["count"] += 1
payload = {
"speed": round(state["speed"], 3),
"running": state["running"],
"jam": state["jam"],
"jamZone": state["jamZone"],
"count": state["count"],
"current": round(1.0 + state["speed"] * 4 + random.uniform(-0.1, 0.1), 2),
}
client.publish(f"{P}/telemetry", json.dumps(payload), qos=0, retain=True)
time.sleep(0.5)
Zwei Nachrichten pro Sekunde sind für den öffentlichen MQTT-Broker unproblematisch, der bis zu 10 Nachrichten pro Sekunde und Client erlaubt.
Schritt 2: Die Szene des Förderband-Zwillings bauen
Speichern Sie die Datei als index.html und stellen Sie sie mit npx serve . bereit. Die Rollen werden mit einem InstancedMesh gerendert, wodurch auch ein langes Förderband günstig zu zeichnen bleibt.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Conveyor Digital Twin</title>
<style>
body { margin: 0; overflow: hidden; background: #0f172a; font-family: system-ui, sans-serif; }
#ui { position: absolute; top: 10px; left: 10px; color: #fff; background: rgba(0,0,0,.6);
padding: 10px 12px; border-radius: 6px; font-size: 14px; }
#ui button { margin-right: 6px; margin-top: 8px; }
</style>
<script type="importmap">
{ "imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/",
"mqtt": "https://unpkg.com/mqtt@5.10.1/dist/mqtt.esm.js"
} }
</script>
</head>
<body>
<div id="ui">
<div id="txt">Connecting...</div>
<button data-cmd="START">Start</button><button data-cmd="STOP">Stop</button><button data-cmd="RESET">Reset jam</button>
</div>
<script type="module">
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import mqtt from "mqtt";
const P = "connectmq/demo/conv-7f3a/line1/conv1";
const LENGTH = 12, WIDTH = 1.2, ROLLER_R = 0.06, ROLLER_PITCH = 0.25, ZONES = 3;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0f172a);
const camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 0.1, 100);
camera.position.set(4, 5, 9);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
new OrbitControls(camera, renderer.domElement);
scene.add(new THREE.HemisphereLight(0xffffff, 0x334155, 1.3));
const sun = new THREE.DirectionalLight(0xffffff, 1.2); sun.position.set(3, 10, 4); scene.add(sun);
// Rahmen
const frameMat = new THREE.MeshStandardMaterial({ color: 0x64748b });
for (const z of [-WIDTH / 2 - 0.05, WIDTH / 2 + 0.05]) {
const rail = new THREE.Mesh(new THREE.BoxGeometry(LENGTH, 0.15, 0.08), frameMat);
rail.position.set(0, 0.8, z); scene.add(rail);
}
// Rollen als Instanzen
const count = Math.floor(LENGTH / ROLLER_PITCH);
const rollerGeo = new THREE.CylinderGeometry(ROLLER_R, ROLLER_R, WIDTH, 16);
rollerGeo.rotateX(Math.PI / 2); // Achse entlang Z
const rollerMat = new THREE.MeshStandardMaterial({ color: 0x94a3b8, metalness: 0.6, roughness: 0.4 });
const rollers = new THREE.InstancedMesh(rollerGeo, rollerMat, count);
const dummy = new THREE.Object3D();
const rollerX = (i) => -LENGTH / 2 + ROLLER_PITCH / 2 + i * ROLLER_PITCH;
scene.add(rollers);
// Zonenmarkierungen (leuchten bei Stau rot)
const zoneMats = [];
for (let z = 0; z < ZONES; z++) {
const m = new THREE.MeshStandardMaterial({ color: 0x1e293b, emissive: 0x000000 });
const strip = new THREE.Mesh(new THREE.BoxGeometry(LENGTH / ZONES - 0.1, 0.02, 0.1), m);
strip.position.set(-LENGTH / 2 + (z + 0.5) * (LENGTH / ZONES), 0.89, WIDTH / 2 + 0.05);
scene.add(strip); zoneMats.push(m);
}
// Pakete
const parcelMat = new THREE.MeshStandardMaterial({ color: 0xc08a4a });
const parcels = [];
for (let i = 0; i < 8; i++) {
const p = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.4, 0.6), parcelMat);
p.position.set(-LENGTH / 2 + i * 1.5, 0.8 + ROLLER_R + 0.2, 0);
scene.add(p); parcels.push(p);
}
// ---------- Zustand ----------
const s = { speed: 0, running: false, jam: false, jamZone: null, count: 0, online: false, at: 0 };
let shownSpeed = 0, rollerAngle = 0;
// ---------- MQTT ----------
const client = mqtt.connect("wss://broker.connectmq.com:8084/mqtt", {
clientId: "conv-twin-" + Math.random().toString(16).slice(2, 10),
protocolVersion: 5,
});
const dec = new TextDecoder();
client.on("connect", () => client.subscribe(`${P}/#`, { qos: 1 }));
client.on("message", (topic, payload) => {
const text = dec.decode(payload);
if (topic.endsWith("/status")) { s.online = text === "online"; return; }
if (!topic.endsWith("/telemetry")) return;
try { Object.assign(s, JSON.parse(text), { at: performance.now() }); } catch {}
});
document.querySelectorAll("button").forEach((b) =>
b.addEventListener("click", () => client.publish(`${P}/cmd`, b.dataset.cmd, { qos: 1 })));
// ---------- Animation ----------
const clock = new THREE.Clock();
const txt = document.getElementById("txt");
renderer.setAnimationLoop(() => {
const dt = Math.min(clock.getDelta(), 0.1);
const stale = performance.now() - s.at > 3000;
const v = stale ? 0 : s.speed;
shownSpeed += (v - shownSpeed) * (1 - Math.exp(-dt * 6));
// Rollen: Winkelgeschwindigkeit = v / r
rollerAngle -= (shownSpeed / ROLLER_R) * dt;
for (let i = 0; i < count; i++) {
dummy.position.set(rollerX(i), 0.8, 0);
dummy.rotation.set(0, 0, rollerAngle);
dummy.updateMatrix();
rollers.setMatrixAt(i, dummy.matrix);
}
rollers.instanceMatrix.needsUpdate = true;
// Pakete bewegen sich mit Bandgeschwindigkeit, am Ende beginnen sie von vorn
for (const p of parcels) {
p.position.x += shownSpeed * dt;
if (p.position.x > LENGTH / 2) p.position.x -= LENGTH;
}
// Stau-Visualisierung
zoneMats.forEach((m, i) => {
const jammed = s.jam && s.jamZone === i + 1;
m.emissive.setHex(jammed ? 0xff2222 : 0x000000);
m.emissiveIntensity = jammed ? 0.6 + 0.4 * Math.sin(performance.now() / 150) : 0;
});
rollerMat.color.setHex(!s.online || stale ? 0x475569 : s.jam ? 0xdc2626 : 0x94a3b8);
txt.textContent = `${s.online ? "Online" : "Offline"} | ${shownSpeed.toFixed(2)} m/s | ` +
`${s.jam ? `JAM in zone ${s.jamZone}` : s.running ? "Running" : "Stopped"} | ` +
`count ${s.count} | ${s.current ?? "-"} A${stale ? " | no data" : ""}`;
renderer.render(scene, camera);
});
addEventListener("resize", () => {
camera.aspect = innerWidth / innerHeight; camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
</script>
</body>
</html>
Starten Sie den Simulator, öffnen Sie die Seite und beobachten Sie, wie die Pakete mit 0,8 m/s fahren. Innerhalb von ein bis zwei Minuten tritt ein Stau auf: Das Band bremst ab, die Rollen werden rot und der betroffene Zonenstreifen pulsiert. Klicken Sie auf Reset jam und dann auf Start, und die Linie fährt wieder hoch.
Bewegung aus realen Einheiten ableiten
Die wichtigste Zeile im Zwilling ist rollerAngle -= (shownSpeed / ROLLER_R) * dt. Da die Szene in Metern arbeitet und der Rollenradius zum Modell passt, entspricht die Oberflächengeschwindigkeit der Rollen der Paketgeschwindigkeit und diese der gemessenen Bandgeschwindigkeit. Betrachter bemerken unbewusst, wenn sich Rollen für die Pakete „zu schnell“ drehen; physikalisch korrekte Bewegung ist es, die einen Zwilling vertrauenswürdig wirken lässt.
Die Geschwindigkeit wird dennoch geglättet, weil reale Antriebe hoch- und herunterfahren und Telemetrie nur zweimal pro Sekunde eintrifft. Der Glättungsfaktor ist kurz (etwa eine Sechstelsekunde), sodass ein plötzlicher Stopp weiterhin wie ein Stopp aussieht. Beachten Sie, dass der Simulator das Band bei einem Stau ebenfalls stoppt, genau wie eine SPS: Der Zwilling entscheidet nicht eigenständig, Pakete anzuhalten, sondern zeigt, was die Linie meldet.
Checkliste für den Förderband-Zwilling an realen Linien
Wenn Sie den Simulator durch echte SPS-Daten ersetzen, prüfen Sie diese Punkte:
- Geschwindigkeitsquelle. Bevorzugen Sie Antriebsrückmeldung oder einen Encoder statt des Sollwerts. Ein Band, das auf einer abgenutzten Trommel rutscht, läuft langsamer als vorgegeben, und der Zwilling sollte das zeigen.
- Stau-Definition. Ein Stau ist typischerweise eine Lichtschranke, die länger belegt ist, als ein Paket zum Passieren braucht. Implementieren Sie das in der SPS oder im Gateway, nicht im Browser, damit alle Konsumenten übereinstimmen.
- Zonennummerierung. Stimmen Sie die Zonennummern mit dem Steuerungstechniker ab und beschriften Sie sie physisch.
- Befehlssicherheit. Der Fernstart eines Förderbands ist eine Sicherheitsfrage, die in die Risikobeurteilung der Anlage gehört. Viele Standorte erlauben über einen Zwilling nur Stopp und Reset oder verlangen eine Bestätigung vor Ort. Umgehen Sie niemals das Sicherheitssystem der Maschine.
- Gateway. Meist liest ein SPS-Gateway oder ein Node-RED-Flow die Tags und veröffentlicht das oben gezeigte JSON; Node-RED mit Modbus ist ein gängiger Weg.
- Aktualisierungsrate. Zwei bis fünf Nachrichten pro Sekunde und Förderband reichen für die Visualisierung völlig aus. Höhere Raten erzeugen Last ohne sichtbaren Nutzen.
Fehlerbehebung
Pakete bewegen sich, aber die Rollen wirken eingefroren oder flackern. Bei hohen Geschwindigkeiten kann sich die Rolle pro Frame um fast ein ganzes Zylindersegment drehen, was einen Speichenrad-Effekt erzeugt. Fügen Sie eine sichtbare Streifentextur hinzu oder reduzieren Sie die Segmentanzahl, damit die Bewegung korrekt wahrgenommen wird.
Der Zwilling bewegt sich weiter, nachdem die Linie gestoppt hat. Ihr Gateway veröffentlicht vermutlich den Geschwindigkeitssollwert statt der Rückmeldung oder hat ganz aufgehört zu veröffentlichen. Die Prüfung auf veraltete Daten setzt die Geschwindigkeit nach drei Sekunden ohne Nachrichten auf null, deshalb zeigt die Statuszeile „no data“.
Buttons bewirken nichts. Prüfen Sie, ob der Simulator oder das Gateway genau das cmd-Topic abonniert, und beobachten Sie es mit mosquitto_sub, um zu bestätigen, dass der Befehl ankommt.
Die Seite verbindet sich und bricht dann alle paar Sekunden ab. Zwei Tabs oder zwei Betrachter teilen sich eine Client ID. Das zufällige Suffix im Beispiel verhindert das auf dem öffentlichen MQTT-Broker; auf einem privaten Broker, bei dem Client IDs an ausgegebene Zugangsdaten gebunden sind, geben Sie jedem Betrachter eigene Zugangsdaten.
Den Zwilling erweitern
- Bauen Sie eine ganze Linie, indem Sie das Förderband mit unterschiedlichen Präfixen wiederholen und Zusammenführungen und Weichen ergänzen; konfigurationsgesteuerte Bindings aus IoT-Daten an ein 3D-Modell binden halten das beherrschbar.
- Ergänzen Sie neben der 3D-Ansicht KPI-Kacheln für Durchsatz pro Stunde, Staus pro Schicht und Verfügbarkeit, wie im Tutorial zum Digital-Twin-Dashboard gezeigt.
- Ersetzen Sie die Boxen durch ein glTF-Modell Ihres tatsächlichen Förderbands und nutzen Sie dafür die Struktur aus einen webbasierten 3D Digital Twin bauen.
- Kombinieren Sie es mit einem Roboter oder Palettierer zu einer Zelle, ähnlich der Szene in der 3D-Digital-Twin-Webdemo von ConnectMQ, die ein Förderband neben einer CNC-Maschine, einem Roboterarm, einem Ofen, einem Tank und einer Pumpe enthält.
Betrieb in der Produktion
Liniendaten sollten nicht über einen öffentlichen MQTT-Broker laufen, denn er ist nur für Tests und Prototypen gedacht. Erstellen Sie einen privaten Broker mit Zugangsdaten und TLS, fügen Sie dann username, password und die ausgegebene clientId zu den mqtt.connect-Optionen hinzu (YOUR_USERNAME, YOUR_PASSWORD, YOUR_CLIENT_ID) und verwenden Sie im Gateway Port 8883 mit TLS. Ein verwalteter MQTT-Broker wie ConnectMQ übernimmt die Broker-Seite, einschließlich Secure WebSocket auf Port 8084 für Browser-Zwillinge, sodass sich Ihr Team auf die SPS-Integration und die Visualisierung konzentrieren kann.
Starten Sie mit ConnectMQ
Bauen Sie Ihren digitalen Zwilling für das Förderband noch heute auf dem öffentlichen MQTT-Broker und ziehen Sie ihn dann auf Ihren eigenen privaten Broker bei ConnectMQ um. Der kostenlose Serverless-Plan, ohne Kreditkarte, unterstützt bis zu 25 Verbindungen und enthält Alarmregeln mit einem Alarmprotokoll, sodass ein Stau im Portal auch einen Schwellwertalarm auslösen kann. Erstellen Sie Ihren kostenlosen Broker unter app.connectmq.com. Wenn Sie zuerst weiter experimentieren möchten, nutzen Sie den öffentlichen MQTT-Broker.
Häufig gestellte Fragen
Wie oft sollte ein Förderband Telemetrie für einen digitalen Zwilling veröffentlichen?
Zwei bis fünf Nachrichten pro Sekunde reichen in der Regel aus. Der Zwilling interpoliert zwischen den Nachrichten, sodass höhere Raten Netzwerklast bei geringem visuellem Nutzen erzeugen.
Sollte die Stauerkennung im Zwilling oder in der SPS erfolgen?
In der SPS oder im Gateway. Der Zwilling sollte den empfangenen Stauzustand anzeigen, damit jedes System, von Dashboards bis zu Alarmen, dasselbe Ereignis sieht.
Kann ich das Förderband über den digitalen Zwilling steuern?
Technisch ja, indem Sie Befehle veröffentlichen, die das Gateway an die SPS weiterleitet. Ob Sie das tun sollten, hängt von den Sicherheitsregeln Ihres Standorts ab; Stopp und Reset sind üblich, ein Fernstart erfordert dagegen oft eine Bestätigung vor Ort.
Wie zeige ich viele Förderbänder an, ohne den Browser auszubremsen?
Verwenden Sie Instanced Meshes für Rollen und Pakete, teilen Sie Materialien und halten Sie eine MQTT-Verbindung mit einem Wildcard-Abonnement für die gesamte Linie.



