IoT Eğitimleri

.NET MAUI MQTT Uygulaması: Android, iOS ve Windows'ta Cihazları İzleyin ve Kontrol Edin

MQTTnet ile canlı telemetri gösteren, cihazlara komut gönderen, uygulama yaşam döngüsünde yeniden bağlanan ve bir ConnectMQ broker'a bağlanan tek bir platformlar arası .NET MAUI uygulaması geliştirin.

Canlı sıcaklık okuması ve röle anahtarıyla aynı .net maui mqtt uygulamasını çalıştıran telefon ve dizüstü bilgisayar
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 .NET MAUI MQTT uygulaması, Android, iOS, Windows ve macOS'tan cihazları izleyen ve kontrol eden tek bir C# kod tabanı yayınlamanızı sağlar. Bu eğitimde MQTTnet ile canlı sıcaklığı gösteren, cihazın çevrimiçi durumunu görüntüleyen ve bir röleyi açıp kapatan küçük bir MAUI uygulaması geliştireceksiniz; ayrıca çoğu örneğin atladığı kısmı da ele alacaksınız: uygulama arka plana geçtiğinde ne olur? Örnekler herkese açık broker broker.connectmq.com kullanır, ardından özel ConnectMQ kimlik bilgilerine geçer.

Ne Geliştireceksiniz?

  • Bağlantı göstergesi, son sıcaklık değeri ve bir röle anahtarı içeren tek bir sayfa
  • Tek bir MQTTnet client'a sahip olan ve bağlanabilir (bindable) özellikler sunan bir view model
  • .../relay/set topic'ine yayınlanan komutlar ve .../relay/state topic'inden okunan durum
  • Uygulama devam ettiğinde yeniden bağlanma ve yeniden abone olma

Paylaşılan broker'ın diğer kullanıcılarıyla çakışmamanız için tümü benzersiz bir önek altında olan topic'ler:

Topic Yön Payload
connectmq/demo/m4ui7x/device1/temperature cihazdan uygulamaya {"value": 22.4}
connectmq/demo/m4ui7x/device1/status cihazdan uygulamaya (retained) online / offline
connectmq/demo/m4ui7x/device1/relay/state cihazdan uygulamaya (retained) on / off
connectmq/demo/m4ui7x/device1/relay/set uygulamadan cihaza on / off

set ile state topic'lerini ayırmak bilinçli bir desendir: uygulama bir değişiklik ister, cihaz gerçek durumu doğrular. Ekrandaki anahtar, kullanıcının neye dokunduğunu değil, cihazın ne bildirdiğini yansıtır.

Projeyi Oluşturun

MAUI iş yüküne (dotnet workload install maui) sahip .NET 8 veya 9 SDK gerekir.

dotnet new maui -n DeviceControl
cd DeviceControl
dotnet add package MQTTnet
dotnet add package CommunityToolkit.Mvvm

CommunityToolkit.Mvvm, property-changed ile ilgili tekrar eden kodun çoğunu ortadan kaldırır. İsteğe bağlıdır, ancak view model'i çok daha kısa hale getirir.

View Model

MainViewModel.cs dosyasını oluşturun. Her .NET MAUI MQTT uygulamasındaki kritik kural şudur: MQTTnet olayları arka plan thread'lerinde tetikler ve arayüze bağlı özellikler ana thread üzerinde güncellenmelidir. Bunu unutmak Android ve iOS'ta, Windows'ta her seferinde yaşanmadığı için kafa karıştıran çökmelere yol açar.

using System.Text.Json;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MQTTnet;
using MQTTnet.Formatter;
using MQTTnet.Protocol;

namespace DeviceControl;

public partial class MainViewModel : ObservableObject
{
    private const string Prefix = "connectmq/demo/m4ui7x/device1";
    private readonly IMqttClient _client = new MqttClientFactory().CreateMqttClient();
    private readonly MqttClientOptions _options;
    private bool _suppressToggle;

    [ObservableProperty] private string connectionText = "Disconnected";
    [ObservableProperty] private string temperatureText = "--";
    [ObservableProperty] private string deviceStatus = "unknown";
    [ObservableProperty] private bool relayOn;

    public MainViewModel()
    {
        _options = new MqttClientOptionsBuilder()
            .WithTcpServer("broker.connectmq.com", 8883)
            .WithTlsOptions(o => o.UseTls())
            .WithClientId(GetOrCreateClientId())
            .WithProtocolVersion(MqttProtocolVersion.V500)
            .WithKeepAlivePeriod(TimeSpan.FromSeconds(30))
            .Build();

        _client.ApplicationMessageReceivedAsync += e =>
        {
            var topic = e.ApplicationMessage.Topic;
            var payload = e.ApplicationMessage.ConvertPayloadToString();
            MainThread.BeginInvokeOnMainThread(() => Apply(topic, payload));
            return Task.CompletedTask;
        };

        _client.DisconnectedAsync += _ =>
        {
            MainThread.BeginInvokeOnMainThread(() => ConnectionText = "Disconnected");
            return Task.CompletedTask;
        };
    }

    private static string GetOrCreateClientId()
    {
        var id = Preferences.Get("mqtt_client_id", null as string);
        if (id is null)
        {
            id = $"maui-{Guid.NewGuid():N}"[..20];
            Preferences.Set("mqtt_client_id", id);
        }
        return id;
    }

    private void Apply(string topic, string payload)
    {
        if (topic.EndsWith("/temperature"))
        {
            try
            {
                using var doc = JsonDocument.Parse(payload);
                TemperatureText = $"{doc.RootElement.GetProperty("value").GetDouble():0.0} °C";
            }
            catch (Exception) { TemperatureText = "invalid"; }
        }
        else if (topic.EndsWith("/status"))
        {
            DeviceStatus = payload;
        }
        else if (topic.EndsWith("/relay/state"))
        {
            _suppressToggle = true;
            RelayOn = payload == "on";
            _suppressToggle = false;
        }
    }

    [RelayCommand]
    public async Task ConnectAsync()
    {
        if (_client.IsConnected) return;
        try
        {
            ConnectionText = "Connecting...";
            await _client.ConnectAsync(_options);
            await _client.SubscribeAsync(new MqttClientSubscribeOptionsBuilder()
                .WithTopicFilter(f => f.WithTopic($"{Prefix}/temperature"))
                .WithTopicFilter(f => f.WithTopic($"{Prefix}/status"))
                .WithTopicFilter(f => f.WithTopic($"{Prefix}/relay/state")
                    .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce))
                .Build());
            ConnectionText = "Connected";
        }
        catch (Exception ex)
        {
            ConnectionText = $"Error: {ex.Message}";
        }
    }

    public async Task DisconnectAsync()
    {
        if (_client.IsConnected) await _client.DisconnectAsync();
    }

    partial void OnRelayOnChanged(bool value)
    {
        if (_suppressToggle || !_client.IsConnected) return;
        _ = _client.PublishAsync(new MqttApplicationMessageBuilder()
            .WithTopic($"{Prefix}/relay/set")
            .WithPayload(value ? "on" : "off")
            .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
            .Build());
    }
}

_suppressToggle bayrağı bir yankı döngüsünü önler: cihaz yeni bir durum bildirdiğinde RelayOn değerini ayarlamak, aksi takdirde yeniden bir komut yayınlardı. Client ID bir kez üretilir ve Preferences içinde saklanır; böylece uygulama açılışları arasında sabit kalır. Bu, daha sonra kalıcı oturumları etkinleştirirseniz önem kazanır.

Sayfa

MainPage.xaml dosyasının içeriğini değiştirin:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:DeviceControl"
             x:Class="DeviceControl.MainPage"
             x:DataType="local:MainViewModel"
             Title="Device Control">
    <VerticalStackLayout Padding="24" Spacing="16">
        <Label Text="{Binding ConnectionText}" FontSize="14" />
        <Label Text="Temperature" FontSize="16" />
        <Label Text="{Binding TemperatureText}" FontSize="48" FontAttributes="Bold" />
        <Label Text="{Binding DeviceStatus, StringFormat='Device: {0}'}" />
        <HorizontalStackLayout Spacing="12">
            <Label Text="Relay" VerticalOptions="Center" />
            <Switch IsToggled="{Binding RelayOn}" />
        </HorizontalStackLayout>
        <Button Text="Reconnect" Command="{Binding ConnectCommand}" />
    </VerticalStackLayout>
</ContentPage>

Ve MainPage.xaml.cs:

namespace DeviceControl;

public partial class MainPage : ContentPage
{
    private readonly MainViewModel _vm;

    public MainPage(MainViewModel vm)
    {
        InitializeComponent();
        BindingContext = _vm = vm;
    }

    protected override async void OnAppearing()
    {
        base.OnAppearing();
        await _vm.ConnectAsync();
    }
}

Her ikisini de MauiProgram.cs içinde builder.Services.AddSingleton<MainViewModel>(); ve builder.Services.AddSingleton<MainPage>(); ile kaydedin ve AppShell'in sayfayı DI üzerinden çözümlediğinden emin olun (varsayılan şablondaki ContentTemplate içeren ShellContent, sayfa kaydedildikten sonra çalışır).

Uygulama Yaşam Döngüsünü Yönetmek

Mobil işletim sistemleri arka plandaki uygulamaları askıya alır ve soketlerini kapatır. Android süreci bir süre canlı tutabilir ama ağı durdurur; iOS ise neredeyse anında askıya alır. Bir foreground service (Android) veya push bildirim stratejisi (iOS) olmadan arka planda bir MQTT bağlantısını açık tutmaya çalışmak kaybedilmiş bir savaştır. İzleme uygulaması için pratik yaklaşım:

  1. Uygulama arka plana geçtiğinde bağlantıyı düzgünce kesin.
  2. Uygulama devam ettiğinde yeniden bağlanın ve yeniden abone olun.
  3. Durum için retained mesajlara güvenin; böylece ekran yeniden bağlandığınız anda doğru olur.

Bunu App.xaml.cs içinde CreateWindow metodunu override edip pencere olaylarına bağlanarak kurun:

protected override Window CreateWindow(IActivationState? activationState)
{
    var window = new Window(new AppShell());
    var vm = activationState!.Context.Services.GetRequiredService<MainViewModel>();
    window.Stopped += async (_, _) => await vm.DisconnectAsync();
    window.Resumed += async (_, _) => await vm.ConnectAsync();
    return window;
}

Cihaz status ve relay/state mesajlarını retained olarak yayınladığı için broker bunları abone olunur olunmaz teslim eder. Uygulama uyandıktan sonra asla bayat bir anahtar göstermez. Uygulama kapalıyken uyarılar için doğru mimari, sunucu tarafında abone olan ve push bildirimleri gönderen bir backend servisidir; bu kısım için ASP.NET Core MQTT background service yazısına bakın.

Cihazı Simüle Edin

Test için donanıma ihtiyacınız yok. Bir terminalden cihaz rolünü üstlenin:

# Retained çevrimiçi durumu ve röle durumu
mosquitto_pub -h broker.connectmq.com -p 1883 -i dev1-sim-a -r -t connectmq/demo/m4ui7x/device1/status -m online
mosquitto_pub -h broker.connectmq.com -p 1883 -i dev1-sim-b -r -t connectmq/demo/m4ui7x/device1/relay/state -m off

# Sıcaklık okuması
mosquitto_pub -h broker.connectmq.com -p 1883 -i dev1-sim-c -t connectmq/demo/m4ui7x/device1/temperature -m '{"value": 22.4}'

# Uygulamanın gönderdiği komutları izleyin
mosquitto_sub -h broker.connectmq.com -p 1883 -i dev1-sim-d -t connectmq/demo/m4ui7x/device1/relay/set -v

Uygulamadaki anahtara dokunun; komut son terminalde görünür. Ardından cihazın yapacağı gibi relay/state topic'ine on yayınlayın; anahtar durumu doğrular. Gerçek yazılım da aynı sözleşmeyi izler; ESP32 MQTT eğitimi bu uygulamayla eşleştirebileceğiniz bir cihaz tarafı gösterir.

Komut Beklemedeyken Geri Bildirim Göstermek

Anahtar cihazın doğrulanmış durumunu yansıttığı için dokunma ile doğrulama arasında kısa bir boşluk olur. Bu boşluğu göstermezseniz kullanıcılar bunu "uygulama bozuk" olarak algılar. Basit bir yaklaşım, komut yayınlandığında açılan ve relay/state geldiğinde kapanan, bir ActivityIndicator'a bağlı bir Pending özelliğidir. Birkaç saniyelik bir zaman aşımı ekleyin: doğrulama gelmezse "Cihaz yanıt vermedi" gösterin ve anahtarı son doğrulanan durumda bırakın. Bu küçük ayrıntı bir .NET MAUI MQTT uygulamasını güvenilir hissettirir ve çevrimdışı bir cihaz veya bir yazılım hatası gibi gerçek sorunları gizlemek yerine görünür kılar.

Kullanıcı dokunmadan önce cihaza ulaşılıp ulaşılamadığını da bilmek istiyorsanız, cihazın Last Will mesajıyla yönetilen retained status topic'ine güvenin. Durum offline iken anahtarı devre dışı bırakın.

Platforma Özgü Tuzaklar

Platform Tuzak Çözüm
Android Emülatörde SocketException veya bağlantı reddedildi Emülatörün internet erişimi olduğundan emin olun; INTERNET izni MAUI şablonunda varsayılan olarak bulunur
Android Mesaj handler'ından etiketleri güncellerken çökme Her zaman MainThread'e aktarın
iOS Arka plana geçtikten saniyeler sonra bağlantı kopuyor Beklenen davranış; Stopped olayında bağlantıyı kesin, Resumed olayında yeniden bağlanın
Windows Windows'ta çalışıyor, mobilde çöküyor Genellikle Windows dispatcher'ının gizlediği bir threading hatası; erkenden bir telefonda test edin
Tümü Düz TCP 1883 mobil ağlarda veya misafir Wi-Fi'de engelli 8883'te TLS'yi veya WithWebSocketServer ile 8084 üzerinden WebSocket'i tercih edin
Tümü Aynı ID'ye sahip iki uygulama örneği birbirini düşürüyor Yukarıda gösterildiği gibi kurulum başına bir client ID saklayın

Üretim Kimlik Bilgileri ve Güvenlik

Bir mobil uygulama yayınlamak, binary'nin decompile edilebileceği anlamına gelir; bu yüzden asla paylaşılan bir yönetici parolası gömmeyin. En basitten en sağlama doğru seçenekler:

  • Oturum açıldıktan sonra backend'inizin sağladığı, kullanıcı veya kurulum başına bir kimlik bilgisi verin.
  • Komutları kendi API'nizin arkasında tutun: uygulama backend'inizi çağırır, backend broker'a yayın yapar.
  • Uygulamanın tuttuğu her kimlik bilgisini Preferences yerine SecureStorage içinde saklayın.

ConnectMQ ile her broker üretilmiş bir kullanıcı adı, parola ve client ID ile gelir ve client ID'ler kendi kimlik bilgilerine bağlıdır. Bunları options builder'a .WithCredentials("YOUR_USERNAME", "YOUR_PASSWORD") ve .WithClientId("YOUR_CLIENT_ID") ile ekleyin ve WithTcpServer değerini broker host'unuza yöneltin. MQTT güvenliği en iyi uygulamaları makalesi kimlik bilgisi stratejisini daha ayrıntılı ele alır; MQTTnet eğitimi ise burada kullanılan client seçeneklerini açıklar.

Arayüzü geliştirirken ücretsiz herkese açık MQTT broker üzerinde test yapmak sorun değildir. Gerçek kullanıcıların gerçek ekipmanı kontrol ettiği üretim uygulamaları, kimlik doğrulama ve TLS içeren özel bir barındırılan MQTT broker gerektirir. .NET dışındaki mobil ve masaüstü seçeneklerine daha geniş bir bakış için MQTT istemci kütüphaneleri karşılaştırmasına göz atın.

ConnectMQ ile Başlayın

app.connectmq.com adresinde kredi kartı gerekmeden ücretsiz Serverless planda özel broker'ınızı oluşturun. Plan en fazla 25 bağlantıyı destekler; bu, bir geliştirme ekibi ve birkaç test cihazı için yeterlidir. Portalın tarayıcı içi MQTT istemcisi, terminal olmadan cihaz rolünü üstlenmenizi sağlar.

Sık Sorulan Sorular

Bir .NET MAUI uygulaması arka planda MQTT mesajları alabilir mi?

Kendi başına güvenilir şekilde alamaz. Mobil işletim sistemleri arka plandaki ağ erişimini askıya alır; bu yüzden uygulama devam ettiğinde durum için retained mesajları, uygulama kapalıyken uyarılar için ise push bildirimleri gönderen sunucu tarafı bir subscriber kullanın.

MQTTnet, MAUI ile iOS ve Android'de çalışır mı?

Evet. MQTTnet saf bir .NET kütüphanesidir ve tüm MAUI hedeflerinde çalışır. 8883 portunda TLS kullanın ve arayüz güncellemelerinin ana thread üzerinde yapıldığından emin olun.

Mobil uygulama doğrudan broker'a mı bağlanmalı, yoksa bir API üzerinden mi geçmeli?

İzleme için salt okunur kimlik bilgileriyle doğrudan bağlantı verimlidir. Gerçek ekipmanın kontrolü için pek çok ekip, yayın yapmadan önce yetkilendirme ve denetim kaydı uygulayabilmek amacıyla komutları kendi backend'leri üzerinden yönlendirir.

Bir Xamarin.Forms MQTT uygulamasını MAUI'ye nasıl taşırım?

MQTTnet kullandıysanız MQTT kodu genellikle değişmeden taşınır. Güncel MQTTnet API'sine (5. sürümde MqttClientFactory) geçin, Xamarin.Essentials çağrılarını MAUI karşılıklarıyla değiştirin ve threading'i her platformda yeniden test edin.

İlgili yazılar

Test, Betik ve Hata Ayıklama için En İyi MQTT CLI Araçları

mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, MQTT.js CLI, curl ve mqttui'nin uygulamalı karşılaştırması; betik yazma ve hata ayıklama için doğru aracı seçebilmeniz için aynı test broker.connectmq.com üzerinde çalıştırıldı.

7 dk okuma

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.