Sviluppare un MQTT Broker su iOS con .NET MAUI

In questa guida vedremo come realizzare un’applicazione iOS che istanzia un server MQTT raggiungibile sulla rete del dispositivo su cui questa viene eseguita.

Per realizzarla utilizzeremo il framework .NET MAUI di Microsoft e la libreria .NET open source MQTTnet per la gestione del broker MQTT.

MQTT?

Iniziamo con il dire che MQTT è un protocollo di messaggistica publish-subscribe standard ISO. Questo pattern publish-subscribe richiede un broker che si occupa di distribuire i messaggi ai destinatari.

Per fare un esempio, Facebook Messanger fa uso di MQTT per la messaggistica tra gli utenti.

.NET MAUI?

.NET MAUI, invece, è un framework multipiattaforma per la creazione di applicazioni native mobili e desktop.

MAUI è l’evoluzione di Xamarin.Forms ed è eseguibile su Android, iOS, macOS e Windows utilizzando un’unica codebase.

Preciso che in questo articolo svilupperemo su una macchina macOS con chip M1 eseguendo l’applicazione sul simulatore iOS Simulator. Il progetto sarà comunque multipiattaforma e compatibile con i sistemi operativi Android, Windows e macOS.

Requisiti

Prima di iniziare con lo sviluppo vero e proprio abbiamo bisogno di soddisfare i seguenti requisiti:

Iniziamo

Avviamo Visual Studio 2022 per Mac e creiamo un nuovo progetto multipiattaforma App .NET MAUI selezionando il linguaggio C#:

Successivamente selezioniamo: il framework .NET 6, il percorso ed il nome del progetto. In questo caso lo abbiamo chiamato MQTTBrokerDemo:

Creando il progetto ci troveremo davanti al template base:

Selezioniamo un simulatore iOS dal menu a tendina in alto a sinistra, iPhone 13 iOS 15.5 per esempio, ed eseguiamo.

Vediamo subito una delle chicche di .NET MAUI, il Ricaricamento rapido XAML.

Lasciando in esecuzione l’applicazione sul simulatore iOS, modifichiamo all’interno del file MainPage.xaml la stringa di testo “Hello, World!” con “Ciao Mondo!

<Label
    Text="Ciao Mondo!"
    SemanticProperties.HeadingLevel="Level1"
    FontSize="32"
    HorizontalOptions="Center" />

Senza nemmeno bisogno di salvare il file modificato sul simulatore la stringa si aggiornerà autonomamente:

La struttura del progetto

Vediamo come si presenta la struttura di cartelle e file di un progetto .NET MAUI.

MauiProgram.cs si occupa di istanziare un oggetto MauiApp, la nostra applicazione appunto, permettendone di configurare diversi aspetti: il punto di ingresso Application (nel nostro caso App, App.xaml.cs), i font utilizzati e/o utilizzabili, la configurazione di librerie di terze parti, ecc…

Visual Studio raggruppa i file .xaml con il relativo file .cs di logica in un’unico oggetto espandibile.

AppShell.xaml e MainPage.xaml sono pagine, rispettivamente: il contenitore e la pagina vera e propria, quella che contiene la stringa di testo “Ciao Mondo!” che abbiamo modificato in precedenza.

La cartella Platform contiene le risorse riferite ad una specifica piattaforma. All’interno di Platform/iOS, ad esempio, troviamo il file Info.plist utilizzato per configurare vari aspetti di un progetto Xcode come l’identificativo del pacchetto (com.organization.demoapp).

Infine la cartella Resources contiene tutte le risorse dell’applicazione come: icone, font, stili, immagini e file generici vari.

Iniziamo lo sviluppo

L’idea è quella di realizzare tre pagine all’interno del nostro contenitore AppShell.xaml a cui si potrà accedere tramite una TabBar posizionata in basso alla schermata:

  • Pagina Configurazione per impostare il parametro della porta utilizzata dal nostro broker MQTT;
  • Pagina Client per mostrare l’elenco dei client connessi al broker;
  • Pagina Messaggi per mostrare l’elenco dei messaggi ricevuti in ingresso dai vari client connessi.

All’interno del progetto aggiungiamo quindi un nuovo file che chiamiamo ClientPage e selezioniamo come tipo “.NET MAUI ContentPage (XAML)

Rifacciamo lo stesso procedimento e creiamo la pagina MessaggiPage.

La pagina MainPage già presente sarà la nostra pagina di configurazione.

Scarichiamo ora le tre icone da assegnare alle Tab (ho utilizzato il set di icone open source Ionicons):

Aggiungiamole come elemento esistente all’interno della cartella Resources/Images.

Piccoli problemi con iOS

Su iOS le icone in formato .svg non vengono ridimensionate a dovere quanto utilizzate come icone all’interno della TabBar, per ovviare a questo problema, bisogna aprire ognuno dei file .svg ed aggiungere la dimensione manualmente all’interno del file vettoriale come sotto:

In questo modo le icone verranno dimensionate correttamente all’interno della TabBar.

Su GitHub si possono trovare diverse issue aperte su questa problematica e si possono usare diversi workaround per risolvere:

Continiamo con lo sviluppo…

All’interno del file AppShell.xaml sostituiamo l’oggetto ShellContent presente come sotto:

<TabBar>
    <ShellContent
        Title="Configurazione"
        ContentTemplate="{DataTemplate local:MainPage}"
        Icon="settings.png" />

    <ShellContent
        Title="Client"
        ContentTemplate="{DataTemplate local:ClientPage}"
        Icon="laptop.png"/>

    <ShellContent
        Title="Messaggi"
        ContentTemplate="{DataTemplate local:MessaggiPage}"
        Icon="mail.png"/>
</TabBar>

Nota: .NET MAUI converte in autonomia tutte le immagini .svg in .png. E’ necessario quindi aggiungere l’estensione .png quando si fa riferimento ad un’immagine all’interno del codice.

Avviando l’applicazione vedremo la TabBar cliccabile con le due nuove pagine vuote in aggiunta alla pagina principale MainPage già presente nel template di base.

Libreria MQTTNet

Aggiungiamo ora la libreria MQTTNet al progetto.

Click destro sulla voce Dipendenze, Gestisci pacchetti NuGet…, cerchiamo MQTTNet ed aggiungiamo il primo pacchetto trovato:

Creiamo all’interno del progetto una nuova classe con un pattern Singleton e chiamiamola Broker.

using System;
using MQTTnet;
using MQTTnet.Server;

namespace MQTTBrokerDemo
{
    public class Broker
    {
        private static Broker instance = null;
        private static readonly object padlock = new object();

        public MqttServer mqttServer;

        public Broker()
        {

        }

        public static Broker Instance
        {
            get
            {
                lock (padlock)
                {
                    if (instance == null)
                    {
                        instance = new Broker();
                    }
                    return instance;
                }
            }
        }

    }
}

Utilizzando questo pattern l’oggetto mqttServer può essere richiamato all’interno di tutta l’applicazione facendo riferimento sempre alla stessa istanza.

Proseguiamo con la progettazione delle pagine.

Pagina Configurazione

Apriamo il file MainPage.xaml e sostituiamo il contenuto dell’oggetto VerticalStackLayout con il seguente:

<VerticalStackLayout
    Spacing="16"
    Padding="30,30">

    <Label Text="Porta"></Label>

    <Entry x:Name="entryPorta"
           Placeholder="Inserisci porta MQTT broker"
           ClearButtonVisibility="WhileEditing"
           Keyboard="Numeric"
           ReturnType="Done"
           Text="1883"></Entry>

    <Label Text="Stato Broker"></Label>

    <Entry x:Name="entryStatoBroker"
           IsEnabled="False"
           Text="NON AVVIATO"></Entry>

    <Button
        x:Name="buttonAvviaTerminaBroker"
        Text="Avvia Broker"
        Clicked="OnAvviaTerminaBrokerClicked" />

</VerticalStackLayout>

Vendendo il codice nel dettaglio:

Abbiamo aggiunto due oggetti di tipo Entry, casella di testo, una numerica per impostare la porta TCP utilizzata dal broker MQTT, ed una testuale disabilitata per evidenziare all’utente lo stato del server.

Abbiamo inoltre aggiunto un oggetto Button che fungerà sia da pulsante di avvio che da pulsante Termina Broker a seconda dello stato di attivazione o meno del server.

Passiamo ora al file di logica della schermata MainPage.xaml.cs e popoliamolo come segue:

using MQTTnet;
using MQTTnet.Server;

namespace MQTTBrokerDemo;

public partial class MainPage : ContentPage
{
    bool started = false;

    public MainPage()
    {
        InitializeComponent();      
    }

    private async void OnAvviaTerminaBrokerClicked(object sender, EventArgs e)
    {
        if (!started)
        {
            var options = new MqttServerOptionsBuilder()
                .WithDefaultEndpoint()
                .WithDefaultEndpointPort(Convert.ToInt32(entryPorta.Text))
                .Build();

            Broker.Instance.mqttServer = new MqttFactory().CreateMqttServer(options);

            await Broker.Instance.mqttServer.StartAsync();

            started = true;

            entryStatoBroker.Text = "AVVIATO";
            buttonAvviaTerminaBroker.Text = "Termina Broker";
        }
        else
        {
            if (Broker.Instance.mqttServer != null && Broker.Instance.mqttServer.IsStarted)
            {
                await Broker.Instance.mqttServer.StopAsync();

            }

            started = false;

            entryStatoBroker.Text = "NON AVVIATO";
            buttonAvviaTerminaBroker.Text = "Avvia Broker";
        }
    }
}

All’interno dell’evento OnAvviaTerminaBrokerClicked() in base allo stato del broker, avviato o meno, instanziamo un nuovo server MQTT utilizzando la porta impostata all’interno della casella di testo dedicata.

Anche gli oggetti entryStatoBroker e buttonAvviaTerminaBroker vengono modificati di conseguenza per adattarsi in base allo stato del server MQTT.

Avviamo l’applicazione e successivamente avviamo il broker:

Proviamo a collegarci tramite un client MQTT, in questo caso utilizziamo MQTTX:

Impostiamo come host 127.0.0.1, porta 1883 (o quella scelta in fase di avvio del broker) e connettiamoci.

Se tutto ha funzionato come deve il client evidenzierà l’avvenuta connessione.

Lasciamo ora aperto il client MQTT che ci servirà più avanti e passiamo a sviluppare la pagina elenco dei client connessi al broker.

Pagina Client

Apriamo il file ClientPage.xaml e sostituiamo il contenuto dell’oggetto VerticalStackLayout con il seguente:

<ListView Margin="30,30" ItemsSource="{Binding ClientList}">
    <ListView.ItemTemplate>
        <DataTemplate>
            <TextCell Text="{Binding Nome}"></TextCell>

        </DataTemplate>

    </ListView.ItemTemplate>

</ListView>

Abbiamo così creato un oggetto di tipo ListView che abbiamo collegato alla collection ClientList tramite Binding (vedremo poi di seguito la gestione lato C#).

Ogni elemento della lista verrà disegnato a schermo seguendo le indicazioni contenute nell’oggetto ItemTemplate. In questo caso utilizziamo un oggetto TextCell per mostrare solo una riga di testo.

Per ogni record presente nella collection ClientList, verrà mostrata la sua proprietà Nome (che corrisponderà all’identificativo del client MQTT connesso).

Passiamo ora la logica della pagina compilando il file ClientPage.xaml.cs:

using System.Collections.ObjectModel;

namespace MQTTBrokerDemo;

public class Client
{
    public string Nome { get; set; }
}

public partial class ClientPage : ContentPage
{
    private ObservableCollection<Client> _clientList { get; set; }
    public ObservableCollection<Client> ClientList {
        get { return _clientList; }
        set
        {
            _clientList = value;
            OnPropertyChanged(nameof(ClientList));
        }
    }

    protected override void OnAppearing()
    {
        base.OnAppearing();

        if (Broker.Instance.mqttServer != null)
        {
            Broker.Instance.mqttServer.ClientConnectedAsync += MqttServer_ClientConnectedAsync;
            Broker.Instance.mqttServer.ClientDisconnectedAsync += MqttServer_ClientDisconnectedAsync;

            UpdateList();
        }
    }

    protected override void OnDisappearing()
    {
        base.OnDisappearing();

        if (Broker.Instance.mqttServer != null)
        {
            Broker.Instance.mqttServer.ClientConnectedAsync -= MqttServer_ClientConnectedAsync;
            Broker.Instance.mqttServer.ClientDisconnectedAsync -= MqttServer_ClientDisconnectedAsync;
        }
    }

    private void UpdateList()
    {
        MainThread.BeginInvokeOnMainThread(() =>
        {
            ClientList.Clear();

            var clients = Broker.Instance.mqttServer.GetClientsAsync().GetAwaiter().GetResult();
            foreach (var client in clients)
            {
                ClientList.Add(new Client()
                {
                    Nome = client.Id
                });
            }
        });
    }

    private Task MqttServer_ClientDisconnectedAsync(MQTTnet.Server.ClientDisconnectedEventArgs arg)
    {
        UpdateList();

        return Task.CompletedTask;
    }

    private Task MqttServer_ClientConnectedAsync(MQTTnet.Server.ClientConnectedEventArgs arg)
    {
        UpdateList();

        return Task.CompletedTask;
    }

    public ClientPage()
    {
        InitializeComponent();

        ClientList = new ObservableCollection<Client>();

        BindingContext = this;
    }
}

Vediamolo passo passo…

public class Client
{
    public string Nome { get; set; }
}

Qui dichiariamo l’oggetto Client con una sola proprietà di tipo stringa chiamata Nome.
Ogni client MQTT che si collega al broker si presenta con un identificativo che sarà il nome mostrato in lista.

private ObservableCollection<Client> _clientList { get; set; }
public ObservableCollection<Client> ClientList {
    get { return _clientList; }
    set
    {
        _clientList = value;
        OnPropertyChanged(nameof(ClientList));
    }
}

Successivamente creiamo una ObservableCollection di oggetti di tipo Client e, molto importante, quando impostiamo la collection, set, è necessario notificare alla GUI MAUI la proprietà che subisce una modifica, in questo caso l’intera lista.

L’utilizzo di collezioni di tipo ObservableCollection è necessario per permettere alla lista di aggiornarsi in tempo reale all’interno dell’interfaccia grafica.

protected override void OnAppearing()
{
    base.OnAppearing();

    if (Broker.Instance.mqttServer != null)
    {
        Broker.Instance.mqttServer.ClientConnectedAsync += MqttServer_ClientConnectedAsync;
        Broker.Instance.mqttServer.ClientDisconnectedAsync += MqttServer_ClientDisconnectedAsync;

        UpdateList();
    }
}

protected override void OnDisappearing()
{
    base.OnDisappearing();

    if (Broker.Instance.mqttServer != null)
    {
        Broker.Instance.mqttServer.ClientConnectedAsync -= MqttServer_ClientConnectedAsync;
        Broker.Instance.mqttServer.ClientDisconnectedAsync -= MqttServer_ClientDisconnectedAsync;
    }
}

private Task MqttServer_ClientDisconnectedAsync(MQTTnet.Server.ClientDisconnectedEventArgs arg)
{
    UpdateList();

    return Task.CompletedTask;
}

private Task MqttServer_ClientConnectedAsync(MQTTnet.Server.ClientConnectedEventArgs arg)
{
    UpdateList();

    return Task.CompletedTask;
}

Successivamente, all’apparizione della pagina a schermo, intercettiamo gli eventi ClientConnected e ClientDisconnected e, quando invocati, aggiorniamo la lista dei client connessi tramite il metodo UpdateList() che vedremo di seguito.

I due eventi ci permetteranno di aggiornare la pagina in tempo reale nel caso un client MQTT si connetta o disconnetta dal broker.

Quando la pagina scompare dallo schermo, OnDisappearing, i due eventi vengono eliminati.

private void UpdateList()
{
    MainThread.BeginInvokeOnMainThread(() =>
    {
        ClientList.Clear();

        var clients = Broker.Instance.mqttServer.GetClientsAsync().GetAwaiter().GetResult();
        foreach (var client in clients)
        {
            ClientList.Add(new Client()
            {
                Nome = client.Id
            });
        }
    });
}

UpdateList() ci permette di ottenere dall’istanza del server MQTT i client connessi ed aggiungerli così alla collection.

Quando vengono effettuate operazioni sulla collezione ClientList, bisogna accertarsi che vengano effettuate sul Thread principale. Ecco perchè l’utilizzo del metodo BeginInvokeOnMainThread.

Questo passaggio in questo caso è necessario in quanto il metodo UpdateList() viene richiamato all’interno di Task evento dell’MQTT server, Task che vengono eseguiti su processi paralleli a quello principale.

public ClientPage()
{
    InitializeComponent();

    ClientList = new ObservableCollection<Client>();

    BindingContext = this;
}

All’interno del costruttore viene inizializzata la collection ed impostato l’oggetto BindingContext come istanza corrente ClientPage, così da poter utilizzare ClientList per il binding ItemsSource della ListView vista sopra (ClientPage.xaml).

Non ci resta che eseguire l’applicativo, avviare il broker MQTT tramite l’apposito pulsante, posizionarci sulla pagina Client e collegare ad esso un client MQTT.

Il client collegato appare correttamente nella lista.

Pagina Messaggi

Prendendo spunto dalla pagina Client appena creata, progettiamo la pagina Messaggi, del tutto simile a quella appena vista, con la differenza che al posto dei client connessi verranno mostrati i messaggi ricevuti da questi.

Apriamo il file MessaggiPage.xaml e sostituiamo l’oggetto VerticalStackLayout come segue:

<ListView Margin="30,30" ItemsSource="{Binding Messages}">
    <ListView.ItemTemplate>
        <DataTemplate>
            <TextCell Text="{Binding Payload}" Detail="{Binding ClientInfo}"></TextCell>

        </DataTemplate>

    </ListView.ItemTemplate>

</ListView>

Il codice è quasi interamente uguale al precedente con l’aggiunta della proprietà Detail che ci permette di mostrare una seconda riga alla cella di testo (TextCell).

Compiliamo poi il file di logica MessaggiPage.xaml.cs come segue:

using System.Collections.ObjectModel;

namespace MQTTBrokerDemo;

public class Message
{
    public string Payload { get; set; }
    public string ClientInfo { get; set; }
}

public partial class MessaggiPage : ContentPage
{
    private ObservableCollection<Message> _messages { get; set; }
    public ObservableCollection<Message> Messages
    {
        get { return _messages; }
        set
        {
            _messages = value;
            OnPropertyChanged(nameof(Messages));
        }
    }

    protected override void OnAppearing()
    {
        base.OnAppearing();

        if (Broker.Instance.mqttServer != null)
        {
            Broker.Instance.mqttServer.InterceptingPublishAsync += MqttServer_InterceptingPublishAsync;
        }
    }

    protected override void OnDisappearing()
    {
        base.OnDisappearing();

        if (Broker.Instance.mqttServer != null)
        {
            Broker.Instance.mqttServer.InterceptingPublishAsync -= MqttServer_InterceptingPublishAsync;
        }
    }

    private Task MqttServer_InterceptingPublishAsync(MQTTnet.Server.InterceptingPublishEventArgs arg)
    {
        MainThread.BeginInvokeOnMainThread(() =>
        {
            Messages.Insert(0, new Message()
            {
                Payload = System.Text.Encoding.Default.GetString(arg.ApplicationMessage.Payload),
                ClientInfo = $"{arg.ClientId} - Topic: {arg.ApplicationMessage.Topic}"
            });
        });

        return Task.CompletedTask;
    }

    public MessaggiPage()
    {
        InitializeComponent();

        Messages = new ObservableCollection<Message>();

        BindingContext = this;
    }
}

Vediamo più nel dettaglio le differenze che ci sono con la pagina Client:

Broker.Instance.mqttServer.InterceptingPublishAsync += MqttServer_InterceptingPublishAsync;

Qui l’evento che andiamo ad intercettare è InterceptingPublishAsync, invocato ogniqualvolta il broker MQTT riceve un messaggio da un client ad esso collegato.

MqttServer_InterceptingPublishAsync(MQTTnet.Server.InterceptingPublishEventArgs arg)
{
    MainThread.BeginInvokeOnMainThread(() =>
    {
        Messages.Insert(0, new Message()
        {
            Payload = System.Text.Encoding.Default.GetString(arg.ApplicationMessage.Payload),
            ClientInfo = $"{arg.ClientId} - Topic: {arg.ApplicationMessage.Topic}"
        });
    });

    return Task.CompletedTask;
}

A differenza della pagina precedente, qui non viene pulita la collection ma viene sempre inserito in cima l’ultimo messaggio ricevuto.

Eseguiamo il tutto, colleghiamo il client MQTT ed inviamo qualche messaggio su un topic di esempio, che noi abbiamo chiamato demo (lato client viene eseguita un’operazione publish).

I messaggi ricevuti vengono mostrati in tempo reale sul dispositivo.

Conclusioni

.NET MAUI è un rivale di pesi massimi del calibro di Ionic Framework e Flutter.

E’ certamente un gran bel passo avanti rispetto al precedente prodotto Microsoft Xamarin.Forms, ma, dal nostro punto di vista, presenta ancora alcune carenze.

La documentazione, per esempio, risulta completa e lacunosa allo stesso tempo, molto dettagliata come Microsoft ci ha abituato ma in certi casi un livello di dettaglio troppo elevato che rende confusionario cercare cose semplici come ad esempio i controlli GUI da poter utilizzare all’interno di una pagina, Label, Entry, ecc…. Ionic Framework da questo punto di vista, lo troviamo molto più intuitivo.

L’export multipiattaforma tramite un’unica codebase funziona molto bene, la grafica si adegua al sistema correttamente, così come sui framework concorrenti.

Qualche incertezza però c’è ancora, basti vedere il paragrafo di creazione della TabBar dove siamo stati obbligati ad impostare manualmente la dimensione delle icone perché sul sistema operativo iOS il ridimensionamento automatico non funziona (su Android, invece, funziona senza alcun workaround).

Il parco librerie di terze parti è abbastanza grande, liste come Awesome .NET MAUI aiutano a trovare il componente di cui abbiamo bisogno ed anche a scoprire nuove potenzialità del framework.

Per concludere, .NET MAUI non è perfetto e non è ancora completo rispetto alla concorrenza. La possibilità però di utilizzare librerie .NET su mobile, librerie come MQTTNet, lo rendono per certi versi unico.

Realizzare un semplice applicativo MQTT server come quello visto sopra, con altri framework non sarebbe stato altrettanto semplice, se non impossibile.

GItHub

Il progetto .NET MAUI realizzato in questo articolo è disponibile su GitHub.

Fonti

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.

Questo sito usa Akismet per ridurre lo spam. Scopri come i tuoi dati vengono elaborati.