Gêmeo digital

Tutorial de gêmeo digital de esteira transportadora: velocidade ao vivo e detecção de travamentos com MQTT

Crie no navegador um gêmeo digital de esteira transportadora cujos rolos e caixas se movem na velocidade real da esteira recebida via MQTT, que destaca travamentos, conta a produção e envia comandos de partida, parada e reset de volta para a linha.

Gêmeo digital de esteira transportadora com rolos, caixas e um alerta vermelho de travamento alimentado por dados MQTT ao vivo
Rode isso no seu próprio broker MQTTTenha um broker MQTT privado e gerenciado no plano Free e comece com exemplos de código prontos.
Teste grátis Exemplos de código Sem cartão de crédito

Um gêmeo digital de esteira transportadora mostra de relance se a linha está andando, em que velocidade e onde está travada. Neste tutorial, você vai construir um no navegador com Three.js: os rolos giram e as caixas avançam na velocidade real da esteira informada via MQTT, um sinal de travamento para as caixas e deixa a esteira vermelha, e os operadores podem enviar comandos de partida, parada e reset de volta para a linha. Um simulador em Python publica dados realistas da esteira no servidor MQTT público em broker.connectmq.com, para que você possa construir e testar tudo antes de conectar um CLP.

Por que uma esteira é uma boa candidata a gêmeo digital

Esteiras estão por toda parte na manufatura, na armazenagem e na logística, e falham de formas visíveis e caras: uma caixa travada em um ponto de junção pode parar tudo o que está a montante. Os sinais normalmente já estão disponíveis no CLP e se traduzem naturalmente em movimento:

Sinal Origem típica Unidade Comportamento no gêmeo
Velocidade da esteira Realimentação do inversor ou encoder m/s Rolos giram, caixas se movem
Em operação Contator do motor ou status do inversor booleano Cor da esteira, movimento ligado/desligado
Travamento Fotocélula bloqueada por mais tempo que um limite booleano mais zona Esteira fica vermelha, caixas param na zona do travamento
Contagem de caixas Fotocélula na descarga contagem Contador de produção
Corrente do motor Inversor A Opcional, alerta antecipado de sobrecarga

Como o comportamento visual decorre diretamente da física (um rolo de raio r gira a v / r radianos por segundo), um gêmeo de esteira também é uma boa forma de aprender a conduzir a animação com unidades reais em vez de números arbitrários. Para uma visão mais ampla, consulte nosso guia de tecnologia de gêmeo digital.

Estrutura de topics

Escolha um prefixo único; o exemplo usa connectmq/demo/conv-7f3a:

Topic Direção Payload
.../line1/conv1/telemetry Da linha para o gêmeo {"speed":0.8,"running":true,"jam":false,"jamZone":null,"count":1532,"current":4.1}
.../line1/conv1/cmd Do gêmeo para a linha START, STOP, RESET
.../line1/conv1/status Linha, retained, last will online / offline

Enviar um único objeto de telemetria combinado, em vez de um topic por sinal, garante que o gêmeo nunca veja uma mistura inconsistente (por exemplo, running: true de uma mensagem e speed: 0 de outra mais antiga). As vantagens e desvantagens de topics combinados e por sinal são discutidas em MQTT para gêmeos digitais.

Passo 1: o simulador da esteira

O simulador modela uma esteira de três zonas rodando a 0,8 m/s, conta as caixas, trava ocasionalmente em uma zona aleatória e obedece a comandos. Ele usa 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  # o CLP para a esteira em caso de travamento
    target = NOMINAL_SPEED if state["running"] else 0.0
    state["speed"] += (target - state["speed"]) * 0.5  # rampa como um inversor faria
    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)

Duas mensagens por segundo é um ritmo tranquilo para o servidor MQTT público, que permite até 10 mensagens por segundo por cliente.

Passo 2: monte a cena do gêmeo digital da esteira

Salve como index.html e sirva com npx serve .. Os rolos são renderizados com um InstancedMesh, o que mantém barato o desenho de uma esteira longa.

<!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);

    // Estrutura
    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);
    }

    // Rolos como instâncias
    const count = Math.floor(LENGTH / ROLLER_PITCH);
    const rollerGeo = new THREE.CylinderGeometry(ROLLER_R, ROLLER_R, WIDTH, 16);
    rollerGeo.rotateX(Math.PI / 2); // eixo ao longo de 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);

    // Marcadores de zona (acendem em vermelho no travamento)
    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);
    }

    // Caixas
    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);
    }

    // ---------- Estado ----------
    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 })));

    // ---------- Animação ----------
    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));

      // Rolos: velocidade angular = 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;

      // Caixas se movem na velocidade da esteira e voltam ao início no fim
      for (const p of parcels) {
        p.position.x += shownSpeed * dt;
        if (p.position.x > LENGTH / 2) p.position.x -= LENGTH;
      }

      // Visualização do travamento
      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>

Inicie o simulador, abra a página e veja as caixas avançarem a 0,8 m/s. Em um ou dois minutos ocorre um travamento: a esteira desacelera, os rolos ficam vermelhos e a faixa da zona afetada pulsa. Clique em Reset jam e depois em Start, e a linha volta a acelerar.

Movimento conduzido por unidades reais

A linha mais importante do gêmeo é rollerAngle -= (shownSpeed / ROLLER_R) * dt. Como a cena usa metros e o raio do rolo corresponde ao modelo, a velocidade superficial dos rolos é igual à velocidade das caixas, que por sua vez é igual à velocidade medida da esteira. Quem assiste percebe inconscientemente quando os rolos giram "rápido demais" para as caixas; um movimento fisicamente correto é o que faz um gêmeo transmitir confiança.

A velocidade ainda é suavizada, porque inversores reais aceleram e desaceleram em rampa e a telemetria chega apenas duas vezes por segundo. O fator de suavização é curto (cerca de um sexto de segundo), então uma parada repentina continua parecendo uma parada. Observe que o simulador também para a esteira em caso de travamento, exatamente como um CLP faria: o gêmeo não decide sozinho parar as caixas, ele mostra o que a linha informa.

Checklist do gêmeo digital de esteira para linhas reais

Ao substituir o simulador por dados reais do CLP, verifique estes pontos:

  1. Origem da velocidade. Prefira a realimentação do inversor ou um encoder ao setpoint. Uma esteira patinando em um tambor gasto anda mais devagar do que o comandado, e o gêmeo deve mostrar isso.
  2. Definição de travamento. Um travamento normalmente é uma fotocélula bloqueada por mais tempo do que uma caixa leva para passar. Implemente isso no CLP ou no gateway, não no navegador, para que todos os consumidores concordem.
  3. Numeração das zonas. Combine a numeração das zonas com o engenheiro de automação e identifique-as fisicamente.
  4. Segurança dos comandos. A partida remota de uma esteira é uma questão de segurança. Muitas plantas permitem apenas parada e reset a partir de um gêmeo, ou exigem confirmação local. Nunca contorne o sistema de segurança da máquina.
  5. Gateway. Um gateway de CLP ou um fluxo do Node-RED normalmente lê as tags e publica o JSON mostrado acima; Node-RED com Modbus é um caminho comum.
  6. Taxa de atualização. De duas a cinco mensagens por segundo por esteira é mais que suficiente para visualização. Taxas maiores adicionam carga sem benefício visível.

Solução de problemas

As caixas se movem, mas os rolos parecem parados ou piscam. Em velocidades altas, a rotação do rolo por quadro pode se aproximar de um segmento inteiro do cilindro, o que cria o efeito de roda de carroça. Adicione uma textura com faixa visível ou reduza o número de segmentos para que o movimento seja lido corretamente.

O gêmeo continua se movendo depois que a linha para. Provavelmente seu gateway está publicando o setpoint de velocidade em vez da realimentação, ou parou de publicar por completo. A verificação de dados desatualizados zera a velocidade após três segundos sem mensagens, e é por isso que a linha de status mostra "no data".

Os botões não fazem nada. Verifique se o simulador ou gateway assina exatamente o topic cmd e acompanhe-o com mosquitto_sub para confirmar que o comando chega.

A página conecta e cai a cada poucos segundos. Duas abas ou dois visualizadores compartilham o mesmo client ID. O sufixo aleatório do exemplo evita isso no servidor MQTT público; em um broker privado, onde os client IDs estão vinculados às credenciais emitidas, dê a cada visualizador suas próprias credenciais.

Ampliando o gêmeo

  • Monte uma linha inteira repetindo a esteira com prefixos diferentes e adicionando junções e desviadores; vínculos orientados por configuração, como em vincular dados IoT a um modelo 3D, mantêm isso gerenciável.
  • Adicione cards de KPI para produção por hora, número de travamentos por turno e disponibilidade ao lado da visualização 3D, como mostrado no tutorial de dashboard de gêmeo digital.
  • Substitua as caixas geométricas por um modelo glTF da sua esteira real, usando a estrutura de como construir um gêmeo digital 3D baseado na web.
  • Combine-a com um robô ou paletizador para montar uma célula, semelhante à cena da demo web de gêmeo digital 3D da ConnectMQ, que inclui uma esteira ao lado de uma máquina CNC, um braço robótico, um forno, um tanque e uma bomba.

Executando em produção

Dados da linha, seja de um centro de distribuição, de uma mineradora ou de um frigorífico, não devem trafegar pelo servidor MQTT público, que serve apenas para testes e protótipos. Crie um broker privado com credenciais e TLS, depois adicione username, password e o clientId emitido às opções de mqtt.connect (YOUR_USERNAME, YOUR_PASSWORD, YOUR_CLIENT_ID) e use a porta 8883 com TLS no gateway. Um broker MQTT gerenciado como a ConnectMQ cuida do lado do broker, incluindo WebSocket seguro na porta 8084 para gêmeos no navegador, para que sua equipe se concentre na integração com o CLP e na visualização.

Comece a usar a ConnectMQ

Construa hoje seu gêmeo digital de esteira transportadora no servidor MQTT público e depois migre-o para o seu próprio broker privado na ConnectMQ. O plano Serverless gratuito, sem necessidade de cartão de crédito, suporta até 25 conexões e inclui regras de alerta com um registro de alertas, para que um travamento também possa disparar um alerta por limite no portal. Crie seu broker gratuito em app.connectmq.com. Se preferir continuar experimentando antes, use o servidor MQTT público (gratuito).

Perguntas frequentes

Com que frequência uma esteira deve publicar telemetria para um gêmeo digital?

De duas a cinco mensagens por segundo geralmente é suficiente. O gêmeo interpola entre as mensagens, então taxas maiores adicionam carga de rede com pouco benefício visual.

A detecção de travamento deve acontecer no gêmeo ou no CLP?

No CLP ou no gateway. O gêmeo deve exibir o estado de travamento que recebe, para que todos os sistemas, dos dashboards aos alertas, vejam o mesmo evento.

Posso controlar a esteira a partir do gêmeo digital?

Tecnicamente sim, publicando comandos que o gateway encaminha ao CLP. Se você deve fazer isso depende das regras de segurança da sua planta; parada e reset são comuns, enquanto a partida remota muitas vezes exige confirmação local.

Como exibir muitas esteiras sem deixar o navegador lento?

Use instanced meshes para rolos e caixas, compartilhe materiais e mantenha uma única conexão MQTT com uma assinatura curinga para a linha inteira.

Artigos relacionados

Todos os artigos →

Seu broker MQTT privado está a poucos cliques de distância

Crie uma conta, receba as credenciais do seu broker e conecte seu primeiro dispositivo — sem servidores para instalar, atualizar ou escalar.