Dijital İkiz

glTF ve Canlı MQTT Verisiyle Web Tabanlı 3D Dijital İkiz Oluşturma

Üretime hazır bir web tabanlı 3D dijital ikiz için adım adım rehber: model hazırlığı, Vite projesi, konfigürasyon odaklı MQTT bağlamaları, tıklayıp inceleme, bağlantı sağlığı ve statik yayınlama.

Tarayıcıda gösterilen fırın, tank, pompa ve CNC tezgâhından oluşan küçük bir fabrikanın web tabanlı 3D dijital ikizi
Bunu kendi MQTT broker’ınızda çalıştırınFree planla özel, yönetilen bir MQTT broker alın ve hazır kod örnekleriyle başlayın.
Ücretsiz deneyin Kod örnekleri Kart gerekmez

Web tabanlı 3D dijital ikiz, tarayıcısı olan herkesin hiçbir yazılım kurmadan bir makinenin, bir hattın ya da bütün bir tesisin canlı durumunu görmesini sağlar. Bu rehber, böyle bir ikizi doğru şekilde kurmayı adım adım anlatıyor: bir glTF modelini hazırlamak, bir Vite projesi oluşturmak, MQTT topic'lerini bir konfigürasyon dosyası üzerinden modelin parçalarına bağlamak, tıklayıp inceleme detayları eklemek ve sonucu statik bir site olarak yayınlamak. Veriler güvenli WebSocket üzerinden broker.connectmq.com adresinden gelir; bu, ConnectMQ'nun kendi 3D fabrika demosunda kullanılan yaklaşımın aynısıdır.

Web Tabanlı 3D Dijital İkizin Mimarisi

Kod yazmadan önce sistemin bütününü görmek faydalıdır. Tarayıcıdaki bir ikiz büyük ölçüde bir front-end uygulamasıdır; veriyi taşıma yükünü broker üstlenir.

Katman Sorumluluk Bu rehberdeki teknoloji
Cihazlar ve gateway'ler Telemetri ve durum yayınlamak Herhangi bir MQTT client (PLC gateway, ESP32, Python)
Broker Mesajları yönlendirmek, retained durumu tutmak Yönetilen MQTT broker, 8084 portunda WSS
Model Geometri, adlandırılmış parçalar, materyaller glTF binary (.glb)
Bağlama konfigürasyonu Hangi topic alanının hangi parçayı nasıl yönettiği Bir JavaScript config modülü
Çalışma zamanı Modeli yüklemek, abone olmak, animasyon, etkileşim Bir Vite uygulamasında Three.js ve MQTT.js

Temel fikir bağlama konfigürasyonudur. Her makine için ayrı kod yazmak yerine birkaç genel davranış (ısı parlaması, doluluk seviyesi, dönme, durum rengi) ve topic'leri model parçalarına bağlayan bir liste yazarsınız. Böylece yeni bir pompa eklemek, çalışma zamanına dokunmadan tek satırlık config ve adlandırılmış bir mesh eklemek anlamına gelir. Kavramla yeni tanışıyorsanız, dijital ikiz teknolojisine genel bakış yazımız görsel ikizlerin simülasyon ve analitik ikizlerle ilişkisini açıklıyor.

Adım 1: Varlıkları ve Topic'leri Planlayın

Yukarıda bahsedilen demo gibi tipik bir fabrika sahnesinde bir CNC tezgâhı, robot kol, fırın, tank, konveyör ve pompa bulunur. Örnek olarak bunlardan dördünü kullanacağız. Önce planı yazıya dökün; bu plan config dosyanız olacak.

Varlık Topic (prefix'inizin altında) Alan Görsel davranış
Fırın furnace1/telemetry temperature (20 ile 900 arası) Koyudan turuncuya emissive parlama
Tank tank1/telemetry level (0 ile 1 arası) Sıvı mesh'i Y ekseninde ölçeklenir
Pompa pump1/telemetry rpm (0 ile 3000 arası) Pervane döner
CNC tezgâhı cnc1/state state İkaz lambası rengi: çalışıyor, boşta, alarm

Demo için connectmq/demo/factory-7f3a prefix'ini kullanıyoruz; son eki kendi rastgele dizenizle değiştirin. Üretimde acme/plant1/area2/line1/furnace1 gibi hiyerarşik bir namespace kullanırsınız; dijital ikizler için MQTT yazımız bunun nasıl tasarlanacağını anlatıyor.

Adım 2: 3D Modeli Hazırlayın

Modeliniz hangi araçtan gelirse gelsin (Blender, bir CAD dışa aktarımı, satın alınmış bir varlık), dışa aktarmadan önce onu ikize hazır hâle getirin:

  1. Bağlanacak her parçaya benzersiz ve anlamlı bir ad verin; örneğin Furnace, Tank_Liquid, Pump_Impeller, CNC_Beacon. Adlar glTF dışa aktarımında korunur ve çalışma zamanı parçaları bu adlarla bulur.
  2. Hareketli parçaları ayrı nesnelere ayırın ve pivot noktasını dönme merkezine yerleştirin. Pompa gövdesiyle birleştirilmiş bir pervane kendi başına dönemez.
  3. Sıvıya birim yükseklik verin ve orijinini tabana koyun; böylece Y ekseninde ölçekleme tabandan yukarı doğru dolum etkisi yaratır.
  4. Görsel olarak önemli olmayan her şeyde poligon sayısını azaltın. CAD dışa aktarımları çoğu zaman milyonlarca üçgen içerir; bir tarayıcı ikizi genellikle bunun çok altında kalmalıdır.
  5. "Apply modifiers" seçeneği etkin olarak glTF binary (.glb) formatında dışa aktarın. Daha hızlı yükleme için isteğe bağlı olarak Draco veya Meshopt ile sıkıştırın.

Aşağıdaki kod, model bulunamazsa aynı adlara sahip basit kutular oluşturan bir yedek mekanizma içerir; böylece elinizde model olmadan da adımları takip edebilirsiniz.

Adım 3: Projenin İskeletini Oluşturun

npm create vite@latest web-twin -- --template vanilla
cd web-twin
npm install three mqtt
mkdir -p public/models   # put factory.glb here

Aşağıdaki çalışma zamanı, modeli bir modülün en üst seviyesinde await ile bekler; bu nedenle vite.config.js dosyasını oluşturarak Vite'a modern tarayıcıları hedeflemesini söyleyin:

export default { build: { target: "es2022" } };

Ortamlar arasında değişen ayarlar için bir .env dosyası oluşturun. Vite, VITE_ ön ekiyle başlayan değişkenleri tarayıcıya açar:

VITE_MQTT_URL=wss://broker.connectmq.com:8084/mqtt
VITE_TOPIC_PREFIX=connectmq/demo/factory-7f3a

Adım 4: Bağlama Konfigürasyonunu Yazın

src/bindings.js dosyasını oluşturun. Her girdi hangi topic ve alanın okunacağını, modelin hangi parçasının değişeceğini ve hangi davranışın uygulanacağını belirtir.

export const PREFIX = import.meta.env.VITE_TOPIC_PREFIX;

export const bindings = [
  { topic: "furnace1/telemetry", field: "temperature", node: "Furnace",       kind: "heat",   min: 20, max: 900 },
  { topic: "tank1/telemetry",    field: "level",       node: "Tank_Liquid",   kind: "fill",   min: 0,  max: 1 },
  { topic: "pump1/telemetry",    field: "rpm",         node: "Pump_Impeller", kind: "spin",   axis: "z" },
  { topic: "cnc1/state",         field: "state",       node: "CNC_Beacon",    kind: "status" },
];

export const statusColors = {
  RUNNING: 0x2ecc71,
  IDLE: 0xf1c40f,
  ALARM: 0xe74c3c,
  OFFLINE: 0x7f8c8d,
};

Adım 5: İkizin Çalışma Zamanı

src/main.js dosyasının içeriğini aşağıdaki çalışma zamanıyla değiştirin. Bu kod modeli yükler (ya da yer tutucular oluşturur), parçaların renklerini bağımsız olarak değiştirebilmesi için materyalleri klonlar, MQTT'ye bağlanır, mesajları bağlamalar üzerinden yönlendirir ve her şeyi tek bir render döngüsünde canlandırır.

import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
import mqtt from "mqtt";
import { PREFIX, bindings, statusColors } from "./bindings.js";

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x182030);
const camera = new THREE.PerspectiveCamera(45, innerWidth / innerHeight, 0.1, 200);
camera.position.set(10, 8, 12);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight);
document.body.style.margin = "0";
document.body.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
scene.add(new THREE.HemisphereLight(0xffffff, 0x445566, 1.3));
const sun = new THREE.DirectionalLight(0xffffff, 1.4);
sun.position.set(8, 15, 6);
scene.add(sun, new THREE.GridHelper(30, 30, 0x4a5568, 0x2d3748));

const panel = Object.assign(document.createElement("pre"), {
  style: "position:absolute;top:10px;left:10px;margin:0;padding:10px;color:#fff;" +
         "background:rgba(0,0,0,.6);border-radius:6px;font:13px monospace;max-width:320px",
  textContent: "Loading model...",
});
document.body.appendChild(panel);

// ----- Model loading with placeholder fallback -----
function buildPlaceholders() {
  const group = new THREE.Group();
  const add = (name, geo, color, pos) => {
    const m = new THREE.Mesh(geo, new THREE.MeshStandardMaterial({ color }));
    m.name = name; m.position.set(...pos); group.add(m); return m;
  };
  add("Furnace", new THREE.BoxGeometry(2, 2, 2), 0x553322, [-4, 1, 0]);
  const liquid = add("Tank_Liquid", new THREE.CylinderGeometry(0.9, 0.9, 1, 32), 0x2f7fd8, [0, 0, 0]);
  liquid.geometry.translate(0, 0.5, 0); // origin at the bottom
  add("Pump_Impeller", new THREE.BoxGeometry(0.2, 1.4, 0.2), 0xcccccc, [3, 1, 0]);
  add("CNC_Beacon", new THREE.SphereGeometry(0.3, 24, 16), 0x7f8c8d, [6, 2.4, 0]);
  add("CNC_Body", new THREE.BoxGeometry(2, 2, 1.5), 0x8899aa, [6, 1, 0]);
  return group;
}

async function loadModel() {
  try {
    const gltf = await new GLTFLoader().loadAsync("/models/factory.glb");
    return gltf.scene;
  } catch {
    console.warn("factory.glb not found, using placeholders");
    return buildPlaceholders();
  }
}

const root = await loadModel();
scene.add(root);

// ----- Resolve bindings to objects and give them private materials -----
const runtime = bindings.map((b) => {
  const obj = root.getObjectByName(b.node);
  if (!obj) console.warn(`Binding target "${b.node}" not found in model`);
  obj?.traverse((o) => { if (o.isMesh) o.material = o.material.clone(); });
  return { ...b, obj, target: b.min ?? 0, current: b.min ?? 0, last: null, at: 0 };
});
const byTopic = new Map();
for (const r of runtime) {
  const full = `${PREFIX}/${r.topic}`;
  if (!byTopic.has(full)) byTopic.set(full, []);
  byTopic.get(full).push(r);
}

// ----- MQTT -----
const client = mqtt.connect(import.meta.env.VITE_MQTT_URL, {
  clientId: import.meta.env.VITE_MQTT_CLIENT_ID || "web-twin-" + crypto.randomUUID().slice(0, 8),
  username: import.meta.env.VITE_MQTT_USERNAME || undefined,
  password: import.meta.env.VITE_MQTT_PASSWORD || undefined,
  protocolVersion: 5,
  reconnectPeriod: 2000,
});
let connected = false;
client.on("connect", () => { connected = true; client.subscribe(`${PREFIX}/#`, { qos: 0 }); });
client.on("close", () => (connected = false));
client.on("message", (topic, payload) => {
  const targets = byTopic.get(topic);
  if (!targets) return;
  let data;
  try { data = JSON.parse(new TextDecoder().decode(payload)); } catch { return; }
  for (const r of targets) {
    if (!(r.field in data)) continue;
    r.target = data[r.field];
    r.last = data;
    r.at = Date.now();
  }
});

// ----- Behaviours -----
const tmp = new THREE.Color();
const behaviours = {
  heat(r, k) {
    r.current += (r.target - r.current) * k;
    const t = THREE.MathUtils.clamp((r.current - r.min) / (r.max - r.min), 0, 1);
    r.obj.traverse((o) => {
      if (!o.isMesh) return;
      o.material.emissive.setRGB(1, 0.35, 0.05);
      o.material.emissiveIntensity = t * 1.5;
    });
  },
  fill(r, k) {
    r.current += (r.target - r.current) * k;
    r.obj.scale.y = Math.max(THREE.MathUtils.clamp(r.current, 0, 1) * 2.8, 0.001);
  },
  spin(r, k, dt) {
    r.current += (r.target - r.current) * k;
    r.obj.rotation[r.axis] += (r.current / 60) * Math.PI * 2 * dt * 0.05;
  },
  status(r) {
    const stale = Date.now() - r.at > 10000;
    tmp.setHex(stale ? statusColors.OFFLINE : statusColors[r.target] ?? statusColors.OFFLINE);
    r.obj.traverse((o) => {
      if (!o.isMesh) return;
      o.material.color.copy(tmp);
      o.material.emissive.copy(tmp);
      o.material.emissiveIntensity = 0.6;
    });
  },
};

// ----- Click to inspect -----
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
let selected = null;
renderer.domElement.addEventListener("pointerdown", (e) => {
  pointer.set((e.clientX / innerWidth) * 2 - 1, -(e.clientY / innerHeight) * 2 + 1);
  raycaster.setFromCamera(pointer, camera);
  const hit = raycaster.intersectObjects(root.children, true)[0];
  selected = hit ? runtime.find((r) => r.obj && (r.obj === hit.object || r.obj.getObjectById(hit.object.id))) : null;
});

// ----- Render loop -----
const clock = new THREE.Clock();
renderer.setAnimationLoop(() => {
  const dt = clock.getDelta();
  const k = 1 - Math.exp(-dt * 4);
  for (const r of runtime) if (r.obj) behaviours[r.kind](r, k, dt);

  const head = connected ? "MQTT connected" : "MQTT disconnected";
  panel.textContent = selected
    ? `${head}\n\n${selected.node}\n${JSON.stringify(selected.last, null, 2)}`
    : `${head}\nClick a machine for details`;
  controls.update();
  renderer.render(scene, camera);
});

addEventListener("resize", () => {
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
});

npm run dev komutunu çalıştırıp yerel URL'yi açın. Ardından bir terminalden veri gönderin:

P=connectmq/demo/factory-7f3a
mosquitto_pub -h broker.connectmq.com -p 1883 -i feed-7f3a -r -t "$P/furnace1/telemetry" -m '{"temperature":640}'
mosquitto_pub -h broker.connectmq.com -p 1883 -i feed-7f3a -r -t "$P/tank1/telemetry"    -m '{"level":0.7}'
mosquitto_pub -h broker.connectmq.com -p 1883 -i feed-7f3a -r -t "$P/pump1/telemetry"    -m '{"rpm":1800}'
mosquitto_pub -h broker.connectmq.com -p 1883 -i feed-7f3a -r -t "$P/cnc1/state"        -m '{"state":"RUNNING"}'

-r bayrağı retained mesaj yayınlar; böylece sayfayı daha sonra yeniden yükleseniz bile ikiz boş bir fabrika yerine bilinen son değerleri hemen gösterir. Bu tek bayrak, "ısınmayı bekleyen" bir ikiz ile ilk çizimde doğru olan bir ikiz arasındaki farktır; artıları ve eksileri dijital ikizler için retained MQTT mesajları yazımızda ele alınıyor.

Karşılığını Veren Tasarım Kararları

Kod yerine konfigürasyon. Tesise ikinci bir fırın geldiğinde bindings.js dosyasına bir girdi ve Furnace_2 adında bir mesh eklersiniz. Çalışma zamanı değişmez; bu da ileride config'i bir varlık veritabanından üretmeyi kolaylaştırır.

Bağlanan her parçaya özel materyal. glTF dışa aktarıcıları çoğu zaman tek bir materyali birçok mesh arasında paylaştırır. clone() adımı olmadan bir fırını parlatmak, o materyali kullanan tüm nesneleri parlatır.

Bayatlayabilen durum. status davranışı, on saniye boyunca durum mesajı gelmezse griye döner. Bir ikiz, gateway'i bir saat önce çökmüş bir makineyi asla "çalışıyor" olarak göstermemelidir.

Tek abonelik. Prefix altındaki tek bir wildcard abonelik, client'ı public broker'ın client başına 10 abonelik sınırının rahatça altında tutar; topic bazlı dağıtımı ise yönlendirici map yerel olarak yapar.

Morph target'lar ve animasyon klipleri dahil olmak üzere eşleme katmanına daha ayrıntılı bakmak için IoT verisini bir 3D modele bağlama yazımıza göz atın. 3D görünümün yanına KPI kartları ve grafikler eklemek için dijital ikiz paneli eğitimiyle devam edin.

Adım 6: Derleyin ve Yayınlayın

İkiz statik bir sitedir, bu yüzden barındırması basittir:

npm run build      # outputs dist/
npx serve dist     # local check of the production build

dist/ klasörünü herhangi bir statik hosting hizmetine veya CDN'li bir nesne depolama alanına yükleyin. Bunu yapmadan önce şu kontrol listesinden geçin:

  1. VITE_MQTT_URL değerini kendi özel broker'ınıza yönlendirin ve üretim için bir topic prefix'i belirleyin.
  2. Cihaz kimlik bilgilerini asla bundle içinde göndermeyin. Tarayıcının kendi kimlik bilgileri olur ve sayfa sizin oturum açma ekranınızın arkasında durur.
  3. Siteyi HTTPS üzerinden sunun; tarayıcılar HTTPS sayfalarından yapılan ws:// bağlantılarını engeller, ikizin 8084 portunda wss:// kullanmasının nedeni budur.
  4. Modeli sıkıştırın ve yavaş bir bağlantıda yükleme süresini kontrol edin.
  5. Broker'a ulaşılamadığında ne olduğunu test edin: panelde "MQTT disconnected" yazmalı ve durumlar griye dönmelidir.

İkizi Kendi Broker'ınıza Taşıma

Public broker geliştirme ve demolar için idealdir, ancak üzerindeki her şey herkes tarafından görülebilir. Gerçek bir tesis için barındırılan bir MQTT broker, sunucu işletmeden kimlik bilgileri, TLS ve izolasyon sağlar. ConnectMQ ile portalda bir broker oluşturur, üretilen kullanıcı adını, şifreyi ve client ID'yi kopyalar; VITE_MQTT_USERNAME, VITE_MQTT_PASSWORD, VITE_MQTT_CLIENT_ID ve URL'yi üretim ortamınıza eklersiniz. Broker ayrıca ISA-95 tarzı topic hiyerarşilerini algılar ve Sparkplug B verisini okunabilir ISA-95 topic'lerine yeniden yayınlayabilir; bu da fabrika sahası Sparkplug konuşsa bile web ikizine bağlanabileceği temiz, JSON dostu topic'ler sunar.

ConnectMQ ile Başlayın

Bitmiş bir sonucu görmenin en hızlı yolu, güvenli WebSocket üzerinden bir fabrika sahnesine canlı veri aktaran ConnectMQ'nun 3D dijital ikiz web demosudur. Kendi web tabanlı 3D dijital ikizinizi çalıştırmaya hazır olduğunuzda, ücretsiz Serverless planı için kredi kartı gerekmeden app.connectmq.com adresinden kaydolun ve VITE_MQTT_URL değerini yeni broker'ınıza yönlendirin. Önce barındırma modelleri arasında mı seçim yapıyorsunuz? Bulut MQTT broker rehberimiz seçenekleri karşılaştırıyor.

Sık Sorulan Sorular

Web dijital ikizi için en iyi 3D format hangisidir?

glTF binary (.glb) web için standarttır: kompakttır, hızlı yüklenir ve Three.js ile Babylon.js tarafından yerel olarak desteklenir. STEP veya FBX gibi CAD formatlarını kullanmadan önce glTF'ye dönüştürüp sadeleştirin.

İkiz, sayfa yüklendikten hemen sonra doğru değerleri nasıl gösterir?

Cihazlar en son durumlarını retained mesaj olarak yayınlar; böylece broker, tarayıcı abone olur olmaz her topic'in son değerini iletir. Retained mesajlar olmadan ikiz, bir sonraki güncelleme gelene kadar boş kalır.

Web tabanlı bir 3D dijital ikizden makineleri kontrol edebilir miyim?

Evet; örneğin kullanıcı bir makineye tıkladığında sayfadan komut topic'lerine yayın yaparak. Komutlara dikkatle yaklaşın: onay isteyin, QoS 1 kullanın ve cihazın gerçek durumunu geri yayınlamasına izin verin.

Tarayıcı ikizi tabletlerde çalışır mı?

Evet. WebGL modern mobil tarayıcılarda çalışır. Kare hızlarını akıcı tutmak için poligon sayılarını ve doku boyutlarını makul düzeyde tutun ve örnekte olduğu gibi piksel oranını sınırlayın.

İlgili yazılar

Tüm yazılar →

Özel MQTT broker’ınız birkaç tık uzağınızda

Hesabınızı oluşturun, broker kimlik bilgilerinizi alın ve ilk cihazınızı bağlayın — kurulacak, yamalanacak ya da ölçeklendirilecek sunucu yok.