IoT-Tutorials

.NET MAUI MQTT App: Geräte unter Android, iOS und Windows überwachen und steuern

Entwickeln Sie mit MQTTnet eine plattformübergreifende .NET-MAUI-App, die Live-Telemetrie anzeigt, Befehle an Geräte sendet, Reconnects im App-Lebenszyklus beherrscht und mit einem ConnectMQ-Broker läuft.

Smartphone und Laptop mit derselben .NET-MAUI-MQTT-App, die eine Live-Temperatur und einen Relais-Schalter zeigt
Probieren Sie es auf Ihrem eigenen MQTT-Broker ausHolen Sie sich im Free-Tarif einen privaten, verwalteten MQTT-Broker und starten Sie mit fertigen Codebeispielen.
Kostenlos testen Codebeispiele Keine Kreditkarte erforderlich

Mit einer .NET-MAUI-MQTT-App liefern Sie eine einzige C#-Codebasis aus, die Geräte unter Android, iOS, Windows und macOS überwacht und steuert. In diesem Tutorial bauen Sie mit MQTTnet eine kleine MAUI-App, die eine Live-Temperatur und die Gerätepräsenz anzeigt und ein Relais schaltet – und Sie behandeln den Teil, den die meisten Beispiele auslassen: Was passiert, wenn die App in den Hintergrund wechselt? Die Beispiele nutzen den öffentlichen Broker broker.connectmq.com und wechseln dann zu privaten ConnectMQ-Zugangsdaten. Im industriellen Umfeld entsteht so etwa eine Service-App für die Instandhaltung, die Maschinenzustände aus dem Unified Namespace (UNS) eines Werks anzeigt.

Was Sie bauen

  • Eine einzelne Seite mit Verbindungsanzeige, aktueller Temperatur und Relais-Schalter
  • Ein ViewModel, das genau einen MQTTnet-Client besitzt und bindbare Properties bereitstellt
  • Befehle, die an .../relay/set veröffentlicht werden, und Zustände, die aus .../relay/state gelesen werden
  • Reconnect beim Fortsetzen der App, inklusive erneutem Abonnieren

Die Topics liegen alle unter einem eindeutigen Präfix, damit Sie nicht mit anderen Nutzern des geteilten Brokers kollidieren:

Topic Richtung Payload
connectmq/demo/m4ui7x/device1/temperature Gerät zur App {"value": 22.4}
connectmq/demo/m4ui7x/device1/status Gerät zur App (retained) online / offline
connectmq/demo/m4ui7x/device1/relay/state Gerät zur App (retained) on / off
connectmq/demo/m4ui7x/device1/relay/set App zum Gerät on / off

Die Trennung von set und state ist ein bewusst gewähltes Muster: Die App fordert eine Änderung an, das Gerät bestätigt den tatsächlichen Zustand. Der Schalter auf dem Bildschirm zeigt, was das Gerät meldet, nicht, was der Nutzer angetippt hat.

Das Projekt anlegen

Sie benötigen das .NET-8- oder .NET-9-SDK mit dem MAUI-Workload (dotnet workload install maui).

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

CommunityToolkit.Mvvm erspart den Großteil des Property-Changed-Boilerplates. Es ist optional, macht das ViewModel aber deutlich kürzer.

Das ViewModel

Legen Sie MainViewModel.cs an. Die entscheidende Regel in jeder .NET-MAUI-MQTT-App: MQTTnet löst Events auf Hintergrund-Threads aus, an die UI gebundene Properties müssen aber auf dem Main Thread aktualisiert werden. Wer das vergisst, bekommt unter Android und iOS Abstürze, die verwirren, weil sie unter Windows nicht jedes Mal auftreten.

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());
    }
}

Das Flag _suppressToggle verhindert eine Echo-Schleife: Meldet das Gerät einen neuen Zustand, würde das Setzen von RelayOn sonst erneut einen Befehl veröffentlichen. Die Client ID wird einmal erzeugt und in Preferences gespeichert, sodass sie über App-Starts hinweg stabil bleibt – wichtig, falls Sie später persistente Sessions aktivieren.

Die Seite

Ersetzen Sie den Inhalt von MainPage.xaml:

<?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>

Und 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();
    }
}

Registrieren Sie beide in MauiProgram.cs mit builder.Services.AddSingleton<MainViewModel>(); und builder.Services.AddSingleton<MainPage>(); und stellen Sie sicher, dass AppShell die Seite aus der DI auflöst (die ShellContent mit ContentTemplate aus der Standardvorlage funktioniert, sobald die Seite registriert ist).

Den App-Lebenszyklus behandeln

Mobile Betriebssysteme suspendieren Hintergrund-Apps und schließen deren Sockets. Android hält den Prozess eventuell noch eine Weile am Leben, stoppt aber das Netzwerk; iOS suspendiert fast sofort. Eine MQTT-Verbindung im Hintergrund offen halten zu wollen, ohne Foreground Service (Android) oder Push-Notification-Strategie (iOS), ist ein aussichtsloser Kampf. Der praktikable Ansatz für eine Monitoring-App:

  1. Beim Wechsel in den Hintergrund sauber trennen.
  2. Beim Fortsetzen neu verbinden und neu abonnieren.
  3. Für Zustände auf Retained Messages setzen, damit der Bildschirm im Moment des Reconnects korrekt ist.

Verdrahten Sie das in App.xaml.cs, indem Sie CreateWindow überschreiben und sich in die Fenster-Events einhängen:

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;
}

Da das Gerät status und relay/state als Retained Messages veröffentlicht, liefert der Broker sie beim Abonnieren sofort aus. Die App zeigt nach dem Aufwachen also nie einen veralteten Schalter. Für Alarme bei geschlossener App ist ein serverseitig abonnierender Backend-Service, der Push-Benachrichtigungen versendet, die richtige Architektur; siehe dazu den ASP.NET-Core-MQTT-Background-Service.

Das Gerät simulieren

Zum Testen brauchen Sie keine Hardware. Übernehmen Sie im Terminal die Rolle des Geräts:

# Retained-Präsenz und Relaiszustand
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

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

# Die Befehle der App beobachten
mosquitto_sub -h broker.connectmq.com -p 1883 -i dev1-sim-d -t connectmq/demo/m4ui7x/device1/relay/set -v

Tippen Sie in der App auf den Schalter, und der Befehl erscheint im letzten Terminal. Veröffentlichen Sie dann wie das Gerät on auf relay/state, und der Schalter bestätigt. Echte Firmware folgt demselben Vertrag; das ESP32-MQTT-Tutorial zeigt eine Geräteseite, die Sie mit dieser App kombinieren können.

Feedback anzeigen, solange ein Befehl aussteht

Da der Schalter den bestätigten Zustand des Geräts widerspiegelt, gibt es eine kurze Lücke zwischen Antippen und Bestätigung. Nutzer deuten diese Lücke als „die App ist kaputt“, wenn Sie sie nicht sichtbar machen. Ein einfacher Ansatz ist eine Pending-Property, die beim Veröffentlichen des Befehls aktiviert und beim Eintreffen von relay/state deaktiviert wird, gebunden an einen ActivityIndicator. Ergänzen Sie ein Timeout von wenigen Sekunden: Kommt keine Bestätigung, zeigen Sie „Gerät hat nicht geantwortet“ und belassen den Schalter im zuletzt bestätigten Zustand. Dieses kleine Detail macht eine .NET-MAUI-MQTT-App vertrauenswürdig und deckt echte Probleme wie ein Offline-Gerät oder einen Firmware-Fehler auf, statt sie zu verbergen.

Wenn Sie schon vor dem Antippen wissen möchten, ob das Gerät erreichbar ist, nutzen Sie das retained status-Topic, das über den Last Will des Geräts gesteuert wird. Deaktivieren Sie den Schalter, solange der Status offline ist.

Plattformspezifische Stolperfallen

Plattform Stolperfalle Lösung
Android SocketException oder Connection refused im Emulator Sicherstellen, dass der Emulator Internet hat; die Berechtigung INTERNET ist in der MAUI-Vorlage standardmäßig enthalten
Android Absturz beim Aktualisieren von Labels aus dem Message-Handler Immer auf den MainThread marshallen
iOS Verbindung bricht Sekunden nach dem Wechsel in den Hintergrund ab Erwartet; bei Stopped trennen, bei Resumed neu verbinden
Windows Läuft unter Windows, stürzt auf Mobilgeräten ab Meist ein Threading-Fehler, den der Windows-Dispatcher verdeckt; früh auf einem Smartphone testen
Alle Unverschlüsseltes TCP 1883 in Mobilfunknetzen oder Gast-WLANs blockiert TLS auf 8883 bevorzugen oder WebSocket über 8084 mit WithWebSocketServer
Alle Zwei App-Instanzen mit derselben ID werfen sich gegenseitig hinaus Client ID pro Installation speichern, wie oben gezeigt

Produktive Zugangsdaten und Sicherheit

Eine mobile App auszuliefern heißt, dass die Binärdatei dekompiliert werden kann – betten Sie also niemals ein geteiltes Admin-Passwort ein. Die Optionen, von der einfachsten zur robustesten:

  • Eine Zugangsberechtigung pro Nutzer oder pro Installation ausstellen, die Ihr Backend nach dem Login bereitstellt.
  • Befehle hinter Ihrer eigenen API halten: Die App ruft Ihr Backend auf, und das Backend veröffentlicht an den Broker.
  • Alle Zugangsdaten, die die App selbst hält, in SecureStorage statt in Preferences ablegen.

Bei ConnectMQ erhält jeder Broker einen generierten Benutzernamen, ein Passwort und eine Client ID, und Client IDs sind an ihre Zugangsdaten gebunden. Tragen Sie sie mit .WithCredentials("YOUR_USERNAME", "YOUR_PASSWORD") und .WithClientId("YOUR_CLIENT_ID") in den Options-Builder ein und lassen Sie WithTcpServer auf Ihren Broker-Host zeigen. Der Artikel zu MQTT-Sicherheit: Best Practices behandelt die Strategie für Zugangsdaten ausführlicher, und das MQTTnet-Tutorial erklärt die hier verwendeten Client-Optionen.

Solange Sie die Oberfläche bauen, ist das Testen auf dem kostenlosen öffentlichen MQTT-Broker in Ordnung; er ist für Tests und Prototypen gedacht, nicht für Produktionsdaten. Produktive Apps, mit denen echte Nutzer echte Anlagen steuern, brauchen einen privaten, verwalteten MQTT-Broker mit Authentifizierung und TLS. Einen breiteren Blick auf Mobil- und Desktop-Optionen jenseits von .NET bietet der Vergleich der MQTT-Client-Bibliotheken.

Starten Sie mit ConnectMQ

Erstellen Sie Ihren privaten Broker unter app.connectmq.com im kostenlosen Serverless-Plan, ganz ohne Kreditkarte. Er unterstützt bis zu 25 Verbindungen – genug für ein Entwicklungsteam und eine Handvoll Testgeräte –, und mit dem MQTT-Client im Browser des Portals übernehmen Sie die Geräterolle ohne Terminal.

Häufig gestellte Fragen

Kann eine .NET-MAUI-App MQTT-Nachrichten im Hintergrund empfangen?

Allein nicht zuverlässig. Mobile Betriebssysteme suspendieren Netzwerkverkehr im Hintergrund; nutzen Sie daher Retained Messages für den Zustand beim Fortsetzen und einen serverseitigen Subscriber mit Push-Benachrichtigungen für Alarme bei geschlossener App.

Funktioniert MQTTnet unter iOS und Android mit MAUI?

Ja. MQTTnet ist eine reine .NET-Bibliothek und läuft auf allen MAUI-Zielplattformen. Verwenden Sie TLS auf Port 8883 und achten Sie darauf, dass UI-Aktualisierungen auf dem Main Thread erfolgen.

Sollte sich die mobile App direkt mit dem Broker verbinden oder über eine API gehen?

Für reines Monitoring ist eine direkte Verbindung mit Nur-Lese-Zugangsdaten effizient. Für die Steuerung echter Anlagen leiten viele Teams Befehle über ihr eigenes Backend, um vor dem Veröffentlichen Autorisierung und Audit-Logging anwenden zu können.

Wie migriere ich eine Xamarin.Forms-MQTT-App nach MAUI?

Der MQTT-Code lässt sich in der Regel unverändert übernehmen, sofern Sie MQTTnet genutzt haben. Aktualisieren Sie auf die aktuelle MQTTnet-API (MqttClientFactory in Version 5), ersetzen Sie Xamarin.Essentials-Aufrufe durch MAUI-Entsprechungen und testen Sie das Threading auf jeder Plattform erneut.

Verwandte Artikel

Die besten MQTT-CLI-Tools zum Testen, Skripten und Debuggen

Ein praxisnaher Vergleich von mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, der MQTT.js-CLI, curl und mqttui – mit demselben Test gegen broker.connectmq.com, damit Sie das richtige Tool zum Skripten und Debuggen wählen.

8 Min. Lesezeit

Alle Artikel →

Ihr privater MQTT-Broker ist nur wenige Klicks entfernt

Konto anlegen, Broker-Zugangsdaten erhalten und das erste Gerät verbinden — ohne Server, die installiert, gepatcht oder skaliert werden müssen.