Tutoriais de IoT

App MQTT com .NET MAUI: Monitore e Controle Dispositivos no Android, iOS e Windows

Crie um único app multiplataforma .NET MAUI com MQTTnet que exibe telemetria ao vivo e envia comandos aos dispositivos, trata reconexões no ciclo de vida do app e roda em um broker MQTT gerenciado da ConnectMQ.

Celular e notebook rodando o mesmo app .net maui mqtt com uma leitura de temperatura ao vivo e um interruptor de relé
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 app .NET MAUI MQTT permite entregar uma única base de código C# que monitora e controla dispositivos a partir do Android, iOS, Windows e macOS. Neste tutorial você vai criar um pequeno app MAUI com MQTTnet que mostra uma temperatura ao vivo, exibe a presença do dispositivo e aciona um relé, como o de uma bomba em uma estação de saneamento, e vai tratar a parte que a maioria dos exemplos ignora: o que acontece quando o app vai para segundo plano. Os exemplos usam o servidor MQTT público broker.connectmq.com, que serve apenas para testes, e depois passam para credenciais privadas de um broker MQTT gerenciado da ConnectMQ.

O que você vai construir

  • Uma única página com um indicador de conexão, a temperatura mais recente e um interruptor de relé
  • Um view model que detém um cliente MQTTnet e expõe propriedades com binding
  • Comandos publicados em .../relay/set e estado lido de .../relay/state
  • Reconexão ao retomar o app, com nova assinatura

Topics, todos sob um prefixo exclusivo para não colidir com outros usuários do broker compartilhado:

Topic Direção Payload
connectmq/demo/m4ui7x/device1/temperature dispositivo para o app {"value": 22.4}
connectmq/demo/m4ui7x/device1/status dispositivo para o app (retido) online / offline
connectmq/demo/m4ui7x/device1/relay/state dispositivo para o app (retido) on / off
connectmq/demo/m4ui7x/device1/relay/set app para o dispositivo on / off

Separar set de state é um padrão intencional: o app solicita uma mudança e o dispositivo confirma o estado real. O interruptor na tela reflete o que o dispositivo informa, não o que o usuário tocou.

Criando o projeto

Você precisa do .NET 8 ou 9 SDK com o workload do MAUI (dotnet workload install maui).

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

O CommunityToolkit.Mvvm elimina a maior parte do código repetitivo de property-changed. Ele é opcional, mas deixa o view model bem mais curto.

O view model

Crie o MainViewModel.cs. A regra crítica em qualquer app .NET MAUI MQTT: o MQTTnet dispara eventos em threads de segundo plano, e as propriedades ligadas à interface precisam ser atualizadas na thread principal. Esquecer isso causa crashes no Android e no iOS que confundem, porque nem sempre acontecem no Windows.

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

A flag _suppressToggle evita um loop de eco: quando o dispositivo informa um novo estado, definir RelayOn publicaria um comando de novo. O client ID é gerado uma única vez e guardado em Preferences, então permanece estável entre as execuções, o que importa se mais tarde você ativar sessões persistentes.

A página

Substitua o conteúdo de 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>

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

Registre os dois no MauiProgram.cs com builder.Services.AddSingleton<MainViewModel>(); e builder.Services.AddSingleton<MainPage>();, e garanta que o AppShell resolva a página pela injeção de dependência (o ShellContent com ContentTemplate do template padrão funciona assim que a página é registrada).

Tratando o ciclo de vida do app

Os sistemas operacionais móveis suspendem apps em segundo plano e fecham seus sockets. O Android pode manter o processo vivo por um tempo, mas corta a rede; o iOS suspende quase imediatamente. Tentar manter uma conexão MQTT aberta em segundo plano sem um foreground service (Android) ou uma estratégia de notificações push (iOS) é uma batalha perdida. A abordagem prática para um app de monitoramento:

  1. Desconecte de forma limpa quando o app for para segundo plano.
  2. Reconecte e assine novamente quando ele for retomado.
  3. Confie em mensagens retidas para o estado, para que a tela esteja correta no instante em que você reconectar.

Faça a ligação no App.xaml.cs sobrescrevendo CreateWindow e conectando os eventos da janela:

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

Como o dispositivo publica status e relay/state como mensagens retidas, o broker as entrega imediatamente na assinatura. O app nunca mostra um interruptor desatualizado depois de acordar. Para alertas enquanto o app está fechado, a arquitetura certa é um serviço de backend assinando no lado do servidor e enviando notificações push; veja o background service MQTT no ASP.NET Core para essa parte.

Simulando o dispositivo

Você não precisa de hardware para testar. A partir de um terminal, faça o papel do dispositivo:

# Presença e estado do relé retidos
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

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

# Acompanhe os comandos que o app envia
mosquitto_sub -h broker.connectmq.com -p 1883 -i dev1-sim-d -t connectmq/demo/m4ui7x/device1/relay/set -v

Toque no interruptor do app e o comando aparece no último terminal. Depois publique on em relay/state, como o dispositivo faria, e o interruptor confirma. Um firmware real segue o mesmo contrato; o tutorial de MQTT com ESP32 mostra um lado de dispositivo que você pode combinar com este app.

Mostrando feedback enquanto um comando está pendente

Como o interruptor reflete o estado confirmado pelo dispositivo, existe um pequeno intervalo entre o toque e a confirmação. Os usuários interpretam esse intervalo como "o app está com defeito", a menos que você o mostre. Uma abordagem simples é uma propriedade Pending que liga quando o comando é publicado e desliga quando relay/state chega, ligada a um ActivityIndicator. Adicione um timeout de alguns segundos: se nenhuma confirmação chegar, mostre "O dispositivo não respondeu" e mantenha o interruptor no último estado confirmado. Esse pequeno detalhe faz um app .NET MAUI MQTT transmitir confiança e expõe problemas reais, como um dispositivo offline ou um bug de firmware, em vez de escondê-los.

Se você também quer saber se o dispositivo está acessível antes de o usuário tocar, confie no topic status retido, alimentado pelo Last Will do dispositivo. Desative o interruptor enquanto o status for offline.

Armadilhas por plataforma

Plataforma Armadilha Correção
Android SocketException ou conexão recusada no emulador Garanta que o emulador tenha internet; a permissão INTERNET já vem no template MAUI por padrão
Android Crash ao atualizar labels a partir do handler de mensagens Sempre encaminhe para a MainThread
iOS A conexão cai segundos depois de ir para segundo plano Esperado; desconecte em Stopped e reconecte em Resumed
Windows Funciona no Windows, quebra no celular Geralmente um bug de threading escondido pelo dispatcher do Windows; teste cedo em um celular
Todas TCP simples na 1883 bloqueado em redes móveis ou Wi-Fi de visitantes Prefira TLS na 8883 ou WebSocket na 8084 com WithWebSocketServer
Todas Duas instâncias do app com o mesmo ID derrubam uma à outra Guarde um client ID por instalação, como mostrado acima

Credenciais de produção e segurança

Distribuir um app móvel significa que o binário pode ser descompilado, então nunca embuta uma senha de administrador compartilhada. Opções, da mais simples à mais robusta:

  • Emita uma credencial por usuário ou por instalação, provisionada pelo seu backend após o login.
  • Mantenha os comandos atrás da sua própria API: o app chama o seu backend, e o backend publica no broker.
  • Guarde qualquer credencial que o app precise manter no SecureStorage, não em Preferences.

Com a ConnectMQ, cada broker vem com usuário, senha e client ID gerados, e os client IDs são vinculados às suas credenciais. Coloque-os no options builder com .WithCredentials("YOUR_USERNAME", "YOUR_PASSWORD") e .WithClientId("YOUR_CLIENT_ID") e aponte o WithTcpServer para o host do seu broker. O artigo de boas práticas de segurança MQTT aborda a estratégia de credenciais em mais profundidade, e o tutorial de MQTTnet explica as opções de cliente usadas aqui.

Testar no servidor MQTT público (gratuito) não tem problema enquanto você constrói a interface. Apps de produção, em que usuários reais controlam equipamentos reais, precisam de um broker MQTT gerenciado privado, com autenticação e TLS. Para uma visão mais ampla das opções para mobile e desktop além do .NET, veja a comparação de bibliotecas de cliente MQTT.

Comece a usar a ConnectMQ

Crie o seu broker privado em app.connectmq.com no plano Serverless gratuito, sem cartão de crédito. Ele suporta até 25 conexões, o suficiente para uma equipe de desenvolvimento e alguns dispositivos de teste, e o cliente MQTT no navegador do portal permite fazer o papel do dispositivo sem terminal.

Perguntas frequentes

Um app .NET MAUI consegue receber mensagens MQTT em segundo plano?

Não de forma confiável por conta própria. Os sistemas operacionais móveis suspendem a rede em segundo plano, então use mensagens retidas para o estado ao retomar e um subscriber no lado do servidor com notificações push para alertas enquanto o app está fechado.

O MQTTnet funciona no iOS e no Android com o MAUI?

Sim. O MQTTnet é uma biblioteca .NET pura e roda em todos os alvos do MAUI. Use TLS na porta 8883 e garanta que as atualizações de interface aconteçam na thread principal.

O app móvel deve se conectar diretamente ao broker ou passar por uma API?

Para monitoramento, uma conexão direta com credenciais somente leitura é eficiente. Para controlar equipamentos reais, muitas equipes encaminham os comandos pelo próprio backend para aplicar autorização e logs de auditoria antes de publicar.

Como migro um app MQTT em Xamarin.Forms para o MAUI?

O código MQTT geralmente migra sem alterações se você usava o MQTTnet. Atualize para a API atual do MQTTnet (MqttClientFactory na versão 5), substitua as chamadas do Xamarin.Essentials pelos equivalentes do MAUI e teste novamente o threading em cada plataforma.

Artigos relacionados

Melhores Ferramentas CLI MQTT para Testes, Scripts e Depuração

Uma comparação prática entre mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, a CLI do MQTT.js, curl e mqttui, com o mesmo teste rodando no broker.connectmq.com, para você escolher a ferramenta certa para scripts e depuração.

9 min de leitura

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.