Dijital İkiz

MQTT'ye Bağlı Unity Dijital İkiz: Canlı Telemetri ve Komutlar

Bir Unity sahnesini MQTTnet ile bir MQTT broker'a bağlayın, bir pompayı ana thread üzerinde canlı telemetriyle canlandırın, cihaza başlat ve durdur komutları gönderin ve üretim için TLS'ye geçin.

Canlı MQTT telemetrisiyle dönen ve renk değiştiren bir endüstriyel pompanın Unity 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

Bir Unity dijital ikiz, gerçek makine verilerini bir oyun motoruna taşır; burada fiziksel tabanlı render, VR ve AR desteği ve ayrıntılı sahneler kurmak için olgun bir editör elde edersiniz. Bu eğitimde bir Unity sahnesini MQTTnet kütüphanesiyle MQTT'ye bağlayacak, bir pompayı canlı telemetriyle canlandıracak, cihaza başlat ve durdur komutları gönderecek ve ardından bağlantıyı TLS ile güvenli hâle getireceksiniz. Her şeyi küçük bir Python simülatörüyle broker.connectmq.com adresindeki public broker'a karşı test edeceksiniz.

Unity, Dijital İkiz İçin Ne Zaman Doğru Araçtır?

Three.js gibi tarayıcı motorları, bir ikizi çok sayıda kişinin önüne koymanın en kolay yoludur. Unity ise ikiz kendi başına bir uygulama olduğunda öne çıkar:

İhtiyaç Unity Tarayıcı (Three.js / Babylon.js)
Operatör eğitimi, VR veya AR gözlükleri Güçlü, yerleşik XR desteği WebXR ile mümkün, araç desteği daha az
Çok ayrıntılı sahneler, fizik, aydınlatma Güçlü İyi, tarayıcı kaynaklarıyla sınırlı
Çok sayıda kullanıcı için kurulumsuz erişim Her platform için ayrı build gerekir Güçlü, yalnızca bir URL
TCP üzerinden doğrudan MQTT Evet, masaüstü ve mobil build'lerde Hayır, yalnızca WebSocket
Ekip yetkinlikleri C# ve Unity editörü JavaScript ve web araçları

İkizinizin herhangi bir tarayıcıda bir bağlantıdan açılması gerekiyorsa bunun yerine Three.js dijital ikiz eğitimiyle başlayın. Bir kontrol odası uygulaması, eğitim simülatörü veya gözlük deneyimi söz konusuysa Unity güçlü bir tercihtir. Her ikisinin ardındaki kavramlar için dijital ikiz teknolojisi rehberimize bakın.

Ne Oluşturacaksınız?

Bir Unity sahnesinde şunları yapan bir pompa:

  1. connectmq/demo/unity-7f3a/pump1/telemetry topic'ine abone olur ve JSON'dan rpm, temperature ve running değerlerini okur.
  2. Pervanesini bildirilen hızda döndürür ve ısındıkça gövde rengini maviden kırmızıya kaydırır.
  3. UI butonlarından connectmq/demo/unity-7f3a/pump1/cmd topic'ine START ve STOP komutları yayınlar.
  4. Ağ bağlantısı koptuğunda otomatik olarak yeniden bağlanır.

Paylaşılan public broker'da topic'lerinizin benzersiz olması için unity-7f3a yerine kendi rastgele son ekinizi kullanın.

Adım 1: MQTTnet'i Projeye Ekleyin

MQTTnet, Unity'nin .NET Standard 2.1 profilinde çalışan olgun ve açık kaynaklı bir .NET MQTT client'ıdır. Bu eğitimde 4.3.x API'si kullanılmaktadır.

  1. Project Settings > Player > Other Settings bölümünde Api Compatibility Level değerini .NET Standard 2.1 olarak ayarlayın.
  2. NuGet'ten MQTTnet 4.3.x paketini indirin (.nupkg dosyası bir zip arşividir), lib/netstandard2.1/MQTTnet.dll dosyasını çıkarıp Assets/Plugins/ klasörüne koyun. Alternatif olarak NuGetForUnity paket yöneticisiyle kurabilirsiniz.
  3. IL2CPP ile build alıyorsanız (mobil ve çoğu konsol), kod ayıklamanın MQTTnet'in reflection ile yüklediği tipleri silmemesi için Assets/link.xml dosyasını ekleyin:
<linker>
  <assembly fullname="MQTTnet" preserve="all"/>
</linker>

MQTTnet 5'in birkaç tipin adını değiştirdiğini unutmayın (örneğin MqttFactory, MqttClientFactory oldu). İleride yükseltirseniz factory çağrılarını uyarlayın; script'in genel yapısı aynı kalır.

Adım 2: Pompayı Simüle Edin

Telemetri üretmek ve komutlara tepki vermek için bu Python script'ini herhangi bir bilgisayarda çalıştırın. Script paho-mqtt 2.x kullanır (pip install "paho-mqtt>=2.0").

import json
import math
import time

import paho.mqtt.client as mqtt

PREFIX = "connectmq/demo/unity-7f3a/pump1"
state = {"running": True}


def on_connect(client, userdata, flags, reason_code, properties):
    print("Simulator connected:", reason_code)
    client.subscribe(f"{PREFIX}/cmd", qos=1)


def on_message(client, userdata, msg):
    cmd = msg.payload.decode().strip().upper()
    if cmd in ("START", "STOP"):
        state["running"] = cmd == "START"
        print("Command:", cmd)


client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="unity-sim-7f3a")
client.on_connect = on_connect
client.on_message = on_message
client.connect("broker.connectmq.com", 1883, keepalive=30)
client.loop_start()

t = 0.0
temperature = 30.0
while True:
    t += 0.5
    if state["running"]:
        rpm = 1450 + 120 * math.sin(t / 5)
        temperature = min(temperature + 0.4, 85.0)
    else:
        rpm = 0.0
        temperature = max(temperature - 0.8, 25.0)
    payload = {"rpm": round(rpm), "temperature": round(temperature, 1),
               "running": state["running"]}
    client.publish(f"{PREFIX}/telemetry", json.dumps(payload), qos=0)
    time.sleep(0.5)

Saniyede iki mesaj, public broker'ın client başına saniyede 10 mesaj sınırının rahatça içinde kalır.

Adım 3: Unity Dijital İkiz Script'i

Assets/Scripts/PumpTwin.cs dosyasını oluşturun. Boş bir GameObject'e ekleyin, ardından pervane Transform'unuzu ve pompa gövdesi Renderer'ını inspector alanlarına sürükleyin. Başlangıç için gövde olarak bir silindir ve pervane kanadı olarak yassılaştırılmış bir küp yeterlidir.

using System;
using System.Collections.Concurrent;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using MQTTnet;
using MQTTnet.Client;
using MQTTnet.Formatter;
using MQTTnet.Protocol;
using UnityEngine;

[Serializable]
public class PumpTelemetry
{
    public float rpm;
    public float temperature;
    public bool running;
}

public class PumpTwin : MonoBehaviour
{
    [Header("Broker")]
    public string host = "broker.connectmq.com";
    public int port = 1883;
    public bool useTls = false;
    public string username = "";
    public string password = "";
    public string clientId = "";
    public string topicPrefix = "connectmq/demo/unity-7f3a/pump1";

    [Header("Scene")]
    public Transform impeller;
    public Renderer pumpBody;
    public Color coldColor = new Color(0.2f, 0.5f, 0.9f);
    public Color hotColor = new Color(0.9f, 0.2f, 0.15f);
    public Color offlineColor = Color.gray;

    private IMqttClient _client;
    private CancellationTokenSource _cts;
    private readonly ConcurrentQueue<string> _inbox = new ConcurrentQueue<string>();
    private PumpTelemetry _target = new PumpTelemetry { temperature = 20f };
    private float _rpm, _temperature = 20f, _lastMessageTime = -999f;

    private async void Start()
    {
        if (string.IsNullOrEmpty(clientId))
            clientId = "unity-" + Guid.NewGuid().ToString("N").Substring(0, 8);

        _cts = new CancellationTokenSource();
        _client = new MqttFactory().CreateMqttClient();

        // Runs on a background thread: only enqueue, never touch Unity objects here.
        _client.ApplicationMessageReceivedAsync += e =>
        {
            var seg = e.ApplicationMessage.PayloadSegment;
            if (seg.Count > 0)
                _inbox.Enqueue(Encoding.UTF8.GetString(seg.Array, seg.Offset, seg.Count));
            return Task.CompletedTask;
        };

        _client.DisconnectedAsync += async e =>
        {
            if (_cts.IsCancellationRequested) return;
            Debug.LogWarning($"MQTT disconnected: {e.Reason}. Retrying in 3 s");
            await Task.Delay(3000);
            if (!_cts.IsCancellationRequested) await ConnectAsync();
        };

        await ConnectAsync();
    }

    private async Task ConnectAsync()
    {
        var builder = new MqttClientOptionsBuilder()
            .WithTcpServer(host, port)
            .WithClientId(clientId)
            .WithProtocolVersion(MqttProtocolVersion.V500)
            .WithCleanSession()
            .WithKeepAlivePeriod(TimeSpan.FromSeconds(30));
        if (!string.IsNullOrEmpty(username)) builder.WithCredentials(username, password);
        if (useTls) builder.WithTlsOptions(o => o.UseTls());

        try
        {
            await _client.ConnectAsync(builder.Build(), _cts.Token);
            var subscribe = new MqttFactory().CreateSubscribeOptionsBuilder()
                .WithTopicFilter(f => f
                    .WithTopic($"{topicPrefix}/telemetry")
                    .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce))
                .Build();
            await _client.SubscribeAsync(subscribe, _cts.Token);
            Debug.Log("MQTT connected and subscribed");
        }
        catch (Exception ex)
        {
            Debug.LogWarning($"MQTT connect failed: {ex.Message}");
        }
    }

    private void Update()
    {
        // Drain messages on the main thread.
        while (_inbox.TryDequeue(out var json))
        {
            try
            {
                _target = JsonUtility.FromJson<PumpTelemetry>(json);
                _lastMessageTime = Time.time;
            }
            catch (Exception ex)
            {
                Debug.LogWarning($"Bad payload: {ex.Message}");
            }
        }

        float k = 1f - Mathf.Exp(-Time.deltaTime * 4f); // frame-rate independent easing
        _rpm = Mathf.Lerp(_rpm, _target.rpm, k);
        _temperature = Mathf.Lerp(_temperature, _target.temperature, k);

        if (impeller != null)
            impeller.Rotate(Vector3.up, _rpm / 60f * 360f * Time.deltaTime * 0.1f, Space.Self);

        if (pumpBody != null)
        {
            bool stale = Time.time - _lastMessageTime > 5f;
            float t = Mathf.InverseLerp(25f, 85f, _temperature);
            pumpBody.material.color = stale ? offlineColor : Color.Lerp(coldColor, hotColor, t);
        }
    }

    // Hook these to UI Buttons (OnClick) in the inspector.
    public void SendStart() => _ = PublishCommand("START");
    public void SendStop() => _ = PublishCommand("STOP");

    private async Task PublishCommand(string command)
    {
        if (_client == null || !_client.IsConnected)
        {
            Debug.LogWarning("Not connected, command not sent");
            return;
        }
        var message = new MqttApplicationMessageBuilder()
            .WithTopic($"{topicPrefix}/cmd")
            .WithPayload(command)
            .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
            .Build();
        await _client.PublishAsync(message, _cts.Token);
    }

    private async void OnDestroy()
    {
        _cts?.Cancel();
        if (_client != null && _client.IsConnected)
            await _client.DisconnectAsync();
        _client?.Dispose();
    }
}

Simülatör çalışırken Play'e basın. Pervane hızlanır, gövde maviden kırmızıya doğru ısınır ve SendStop() metoduna bağlı bir Stop butonuna tıklamak simülatörün pompayı durdurmasını sağlar; bir saniye içinde ikiz pervaneyi yavaşlatır ve gövde soğumaya başlar. Simülatörü tamamen durdurursanız gövde beş saniye sonra griye döner.

Unity MQTT Entegrasyonu Nasıl Çalışır?

Thread yönetimi bir numaralı tuzaktır. MQTTnet, ApplicationMessageReceivedAsync olayını bir thread-pool thread'inde tetikler, ancak Unity API'lerinin neredeyse tamamı (transform'lar, materyaller, Debug.DrawLine, nesne oluşturma) ana thread'den çağrılmalıdır. Bunları handler'dan çağırmak hata fırlatır ya da daha kötüsü, aralıklı olarak başarısız olur. Script bu sorunu ham payload'ları bir ConcurrentQueue içine koyup Update() içinde işleyerek önler.

Yumuşatma ağın ritmini gizler. Telemetri saniyede iki kez gelir, ancak Unity saniyede 60 veya daha fazla kare render eder. En son hedefe doğru lerp yapmak hareketi kesintisiz kılar. Üstel faktör, kare hızından bağımsız olarak yumuşatmayı aynı tutar.

Komutlar QoS 1, telemetri QoS 0 kullanır. Kaybolan bir telemetri örneğinin yerini yarım saniye sonra yenisi alır; kaybolan bir durdurma komutu ise gerçek bir sorundur. Ayrıntılar için MQTT QoS seviyeleri yazımızı okuyun.

Doğruluğun kaynağı cihazdır. Stop'a bastığınızda ikiz kendi durumunu asla değiştirmez. Pompanın running: false bildirmesini bekler. Bu, cihaz bir komutu reddettiğinde veya kaçırdığında ikizin dürüst kalmasını sağlar; bu ilke dijital ikiz durum senkronizasyonu yazımızda daha ayrıntılı ele alınıyor.

Unity Dijital İkizi Ölçeklendirme

Makine başına bir script bir demo için yeterlidir, ancak bir tesiste çok sayıda varlık bulunur. Daha büyük sahneler için daha temiz bir yapı:

  1. Uygulama başına tek bir bağlantı yöneticisi: IMqttClient nesnesine sahip olan, plant1/# gibi bir wildcard ile abone olan ve mesajları topic'e göre yönlendiren bir singleton.
  2. Varlık başına bir bağlama bileşeni (TwinBinding): bir topic son eki ve yönettiği parçalara bir referans içerir. Yönetici bağlamaları topic anahtarlı bir dictionary'de arar.
  3. Konfigürasyon için ScriptableObject'ler: böylece broker ayarları ve topic prefix'leri, kod değişikliği olmadan geliştirme ve üretim build'leri arasında farklılaşabilir.
  4. Durum topic'lerinde retained mesajlar: böylece ikiz başladığı anda doğru durumu gösterir.

Alanları mesh'lere, materyallere ve animasyonlara eşlemek, motordan bağımsız olarak aynı problemdir; IoT verisini bir 3D modele bağlama yazımız bu kalıpları ayrıntılı olarak ele alıyor.

Platform Notları ve Sorun Giderme

  • WebGL build'leri TCP soketi açamaz ve MQTTnet'in WebSocket aktarımı Unity WebGL'de bulunmayan .NET API'lerine dayanır. Tarayıcı build'i için MQTT.js gibi bir JavaScript MQTT client'ını bir .jslib eklentisi üzerinden çağırın veya tarayıcı sürümünü bir web motoruyla geliştirin.
  • Android, INTERNET iznine ihtiyaç duyar; Unity ağ kodu algıladığında bu izni otomatik ekler. Cihazda bağlantılar sessizce başarısız oluyorsa bunu iki kez kontrol edin.
  • Connect failed: NotAuthorized hatası yanlış kimlik bilgileri veya client ID anlamına gelir. Connect failed: timeout genellikle portun engellendiğini gösterir; nc -vz broker.connectmq.com 8883 ile test edin.
  • Birkaç saniyede bir tekrarlanan bağlantı kopma döngüsü, iki örneğin aynı client ID'yi paylaştığı anlamına gelir; örneğin editör ve bağımsız bir build. Rastgele bir değer almak için geliştirme sırasında clientId alanını boş bırakın.

Bağlantıyı Güvenli Hâle Getirme

Public broker anonimdir ve herkes tarafından okunabilir; bu yüzden yalnızca testler için kullanın. Gerçek bir tesis için bir yönetilen MQTT broker hizmetiyle özel bir broker oluşturun ve TLS üzerinden bağlanın. ConnectMQ ile portal size üretilmiş bir kullanıcı adı, şifre ve client ID verir; inspector'da Port değerini 8883 yapın, Use Tls kutusunu işaretleyin ve YOUR_USERNAME, YOUR_PASSWORD ve YOUR_CLIENT_ID alanlarını doldurun. Broker'ın sertifikası Let's Encrypt tarafından verildiği için .NET, ek dosyaya gerek kalmadan sertifikayı işletim sisteminin güven deposuna karşı doğrular. Kimlik bilgilerini çalışma zamanında bir config dosyasından veya ortam değişkeninden yükleyerek kaynak kontrolünün dışında tutun.

ConnectMQ ile Başlayın

Bir Unity dijital ikiz, tesisten ve her operatör iş istasyonundan her zaman erişilebilen bir broker'a ihtiyaç duyar. ConnectMQ bunu tam olarak barındırılan bir hizmet olarak sunar: kredi kartı gerekmeden, MQTT 5, TLS ve 25 bağlantıya kadar destekle Serverless planında app.connectmq.com adresinden ücretsiz bir broker oluşturun. Tarayıcı içi MQTT client'ı ve canlı topic gezgini, cihazlarınızın ne yayınladığını Unity'de bağlamadan önce kontrol etmeyi kolaylaştırır. Hazır olana kadar ücretsiz public MQTT broker üzerinde prototip geliştirmeye de devam edebilirsiniz.

Sık Sorulan Sorular

Unity'de en iyi çalışan MQTT kütüphanesi hangisidir?

MQTTnet, .NET Standard 2.1 üzerinde MQTT 3.1.1 ve 5.0, TLS ve async API'leri desteklediği için masaüstü ve mobil Unity build'lerinde en yaygın kullanılan seçenektir. WebGL build'leri için bir eklenti üzerinden köprülenen bir JavaScript client'ına ihtiyacınız vardır.

Mesaj handler'ında nesneleri güncellediğimde neden hata alıyorum?

Handler bir arka plan thread'inde çalışır ve Unity API'lerinin çoğu yalnızca ana thread'de kullanılabilir. Mesajları handler'da kuyruğa alın ve Update() içinde uygulayın.

Bir Unity dijital ikiz gerçek ekipmanı kontrol edebilir mi?

Evet, cihazın abone olduğu topic'lere komut yayınlayarak. QoS 1 kullanın, yıkıcı işlemleri UI'da onaylatın ve ikizin gerçekte ne olduğunu yansıtması için cihazın gerçek durumunu geri yayınlamasını sağlayın.

Bu yöntem VR gözlükleriyle çalışır mı?

Evet. MQTT script'i render kurulumundan bağımsızdır; bu nedenle aynı bileşen, gözlüğün build hedefi kullanılan ağ API'lerini desteklediği sürece XR için yapılandırılmış bir sahnede de çalışır.

İ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.