{"id":20182,"date":"2025-06-09T12:56:07","date_gmt":"2025-06-09T12:56:07","guid":{"rendered":"https:\/\/cloudsurfers.it\/?p=20182"},"modified":"2025-06-09T12:56:21","modified_gmt":"2025-06-09T12:56:21","slug":"inviare-dati-in-tempo-reali-e-storici-da-un-applicativo-net-server-ad-unapplicazione-flutter-client-utilizzando-il-protocollo-mqtt","status":"publish","type":"post","link":"https:\/\/cloudsurfers.it\/index.php\/inviare-dati-in-tempo-reali-e-storici-da-un-applicativo-net-server-ad-unapplicazione-flutter-client-utilizzando-il-protocollo-mqtt\/","title":{"rendered":"Inviare dati in tempo reali e storici da un applicativo .NET (server) ad un&#8217;applicazione Flutter (client) utilizzando il protocollo MQTT"},"content":{"rendered":"<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"800\" height=\"400\" src=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2025\/06\/image.png\" alt=\"\" class=\"wp-image-20183\" srcset=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2025\/06\/image.png 800w, https:\/\/cloudsurfers.it\/wp-content\/uploads\/2025\/06\/image-300x150.png 300w, https:\/\/cloudsurfers.it\/wp-content\/uploads\/2025\/06\/image-768x384.png 768w\" sizes=\"auto, (max-width: 800px) 100vw, 800px\" \/><\/figure>\n<\/div>\n\n\n<p>In questa guida realizzeremo passo passo un&#8217;applicazione console&nbsp;<a href=\"https:\/\/dotnet.microsoft.com\/it-it\/\" target=\"_blank\" rel=\"noreferrer noopener\">.NET<\/a>&nbsp;(il nostro server) che invier\u00e0 la temperatura attuale e le ultime rilevazioni effettuate di una serie di sensori ad un&#8217;applicazione&nbsp;<a href=\"https:\/\/flutter.dev\/\" target=\"_blank\" rel=\"noreferrer noopener\">Flutter<\/a>&nbsp;Android (il nostro client).<\/p>\n\n\n\n<p>L&#8217;applicazione Flutter permetter\u00e0 di selezionare il sensore da un&#8217;elenco e visualizzarne le temperature rilevate. La comunicazione verr\u00e0 effettuata tramite protocollo&nbsp;<a href=\"https:\/\/mqtt.org\/\" target=\"_blank\" rel=\"noreferrer noopener\">MQTT<\/a>&nbsp;e come ambiente di sviluppo utilizzeremo&nbsp;<a href=\"https:\/\/code.visualstudio.com\/\" target=\"_blank\" rel=\"noreferrer noopener\">VSCode<\/a>.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>I valori temperatura saranno numeri interi generati casualmente, non saranno realmente recuperati da sensori fisici. Il concetto sensori temperatura serve solo per rendere l&#8217;idea di quale applicazione potrebbe avere un progetto simile.<\/p>\n<\/blockquote>\n\n\n\n<h3 class=\"wp-block-heading\">Il protocollo MQTT<\/h3>\n\n\n\n<p>Come gi\u00e0 visto in precedenza nel&nbsp;<a href=\"https:\/\/cloudsurfers.it\/index.php\/sviluppare-mqtt-broker-ios-net-maui\/\" target=\"_blank\" rel=\"noreferrer noopener\">seguente articolo<\/a>, MQTT \u00e8 un protocollo di messaggistica&nbsp;<em>publish-subscribe<\/em>, opera sopra a TCP\/IP ed \u00e8 molto leggero, ideale negli ambiti in cui \u00e8 richiesto un&#8217;utilizzo della banda ridotto ed un basso impatto energetico. Perfetto per un sensore temperatura ad esempio.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"la-struttura-del-nostro-esempio\">La struttura del nostro esempio<\/h3>\n\n\n\n<p>Come gi\u00e0 anticipato il nostro progetto sar\u00e0 suddiviso in due parti: l&#8217;<strong>applicativo console<\/strong>&nbsp;(server) e l&#8217;<strong>applicazione Flutter<\/strong>&nbsp;(client). Vediamoli pi\u00f9 nel dettaglio:<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"console-application-net\">Console Application .NET<\/h4>\n\n\n\n<p>L&#8217;applicativo console .NET si occuper\u00e0 di due compiti:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Esporre un endpoint API GET&nbsp;<code>localhost:8080\/api\/sensors<\/code>&nbsp;che restituir\u00e0 l&#8217;elenco dei sensori temperatura disponibili in formato&nbsp;<em>JSON<\/em>.<br>Oltre ovviamente al nome del sensore, propriet\u00e0&nbsp;<em>sensorName<\/em>, verranno restituiti&nbsp;<em>latestValueTopic<\/em>&nbsp;e&nbsp;<em>historyTopic<\/em>&nbsp;rispettivamente ad indicare il topic&nbsp;<em>MQTT<\/em>&nbsp;per il ricevimento dell&#8217;ultimo valore di temperatura e per il ricevimento dello storico valori precedenti.<\/li>\n\n\n\n<li>Istanziare un broker&nbsp;<em>MQTT<\/em>&nbsp;che invii per ogni sensore (ad un&#8217;intervallo pre-impostato) un messaggio contenente l&#8217;ultima temperatura rilevata (un valore casuale da 0 a 100) ed un messaggio contenente lo storico aggiornato delle rilevazioni precedenti.<br>Per questioni di dimensioni lo storico riporter\u00e0 le ultime 99 rilevazioni per ogni sensore, la centesima sar\u00e0 l&#8217;ultima rilevata.<br>I topic saranno cos\u00ec strutturati:\n<ul class=\"wp-block-list\">\n<li><code>sensor1\/latest<\/code><\/li>\n\n\n\n<li><code>sensor1\/history<\/code><\/li>\n\n\n\n<li><code>sensor2\/latest<\/code><\/li>\n\n\n\n<li><code>sensor2\/history<\/code><\/li>\n\n\n\n<li>&#8230;<\/li>\n<\/ul>\n<\/li>\n<\/ul>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"applicazione-flutter\">Applicazione Flutter<\/h4>\n\n\n\n<p>L&#8217;applicazione Flutter una volta avviata mostrer\u00e0 l&#8217;elenco dei sensori disponibili. Selezionandone uno, mostrer\u00e0 l&#8217;ultimo valore di temperatura rilevato con il riferimento data e ora UTC e le precedenti rilevazioni, lo storico appunto.<\/p>\n\n\n\n<p>Il progetto avr\u00e0 questa struttura cartelle:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n- sensors_example\n    - broker\n        - ...\n    - client\n        - ...\n<\/pre><\/div>\n\n\n<p>Iniziamo con la realizzazione.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"il-broker\">Il broker<\/h3>\n\n\n\n<p>Creiamo una nuova cartella per il progetto con&nbsp;<code>mkdir sensors_example; cd sensors_example<\/code>.<\/p>\n\n\n\n<p>Lanciamo&nbsp;<code>dotnet new console -o broker<\/code>&nbsp;per creare il progetto console .NET ed apriamo&nbsp;<em>VSCode<\/em>&nbsp;sulla cartella&nbsp;<code>sensors_example<\/code>&nbsp;con&nbsp;<code>code .<\/code>.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"630\" height=\"259\" src=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2025\/06\/image-1.png\" alt=\"\" class=\"wp-image-20192\" srcset=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2025\/06\/image-1.png 630w, https:\/\/cloudsurfers.it\/wp-content\/uploads\/2025\/06\/image-1-300x123.png 300w\" sizes=\"auto, (max-width: 630px) 100vw, 630px\" \/><\/figure>\n<\/div>\n\n\n<p>Predisponiamo il task di compilazione e lancio dell&#8217;applicativo creando una nuova cartella chiamata&nbsp;<code>.vscode<\/code>&nbsp;nella root di progetto con all&#8217;interno un file&nbsp;<code>tasks.json<\/code>:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n{\n    &quot;version&quot;: &quot;2.0.0&quot;,\n    &quot;tasks&quot;: &#x5B;\n        {\n            &quot;label&quot;: &quot;build&quot;,\n            &quot;command&quot;: &quot;dotnet&quot;,\n            &quot;type&quot;: &quot;process&quot;,\n            &quot;args&quot;: &#x5B;\n                &quot;build&quot;,\n                &quot;${workspaceFolder}\/broker\/broker.csproj&quot;,\n                &quot;\/property:GenerateFullPaths=true&quot;,\n                &quot;\/consoleloggerparameters:NoSummary&quot;\n            ],\n            &quot;problemMatcher&quot;: &quot;$msCompile&quot;\n        }\n    ]\n}\n<\/pre><\/div>\n\n\n<p>e successivamente&nbsp;<code>launch.json<\/code>:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n{\n    &quot;version&quot;: &quot;0.2.0&quot;,\n    &quot;configurations&quot;: &#x5B;\n        {\n            &quot;name&quot;: &quot;broker&quot;,\n            &quot;type&quot;: &quot;coreclr&quot;,\n            &quot;request&quot;: &quot;launch&quot;,\n            &quot;preLaunchTask&quot;: &quot;build&quot;,\n            &quot;program&quot;: &quot;${workspaceFolder}\/broker\/bin\/Debug\/net9.0\/broker.dll&quot;,\n            &quot;args&quot;: &#x5B;],\n            &quot;cwd&quot;: &quot;${workspaceFolder}\/broker\/bin\/Debug\/net9.0&quot;,\n            &quot;stopAtEntry&quot;: false,\n            &quot;console&quot;: &quot;internalConsole&quot;,\n            &quot;internalConsoleOptions&quot;: &quot;openOnSessionStart&quot;,\n        }\n    ]\n}\n<\/pre><\/div>\n\n\n<p>Tramite&nbsp;<em>F5<\/em>&nbsp;lanciamo quindi la nuova configurazione&nbsp;<strong>broker<\/strong>&nbsp;e verifichiamo che funzioni correttamente.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"665\" height=\"80\" src=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2025\/06\/image-2.png\" alt=\"\" class=\"wp-image-20193\" srcset=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2025\/06\/image-2.png 665w, https:\/\/cloudsurfers.it\/wp-content\/uploads\/2025\/06\/image-2-300x36.png 300w\" sizes=\"auto, (max-width: 665px) 100vw, 665px\" \/><\/figure>\n<\/div>\n\n\n<p>All&#8217;interno del file di progetto&nbsp;<code>broker.csproj<\/code>&nbsp;andiamo a sostituire&nbsp;<code>Microsoft.NET.Sdk<\/code>&nbsp;con&nbsp;<code>Microsoft.NET.Sdk.Web<\/code>&nbsp;(prima riga del file), ed aggiungiamo tutti i pacchetti necessari:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: xml; title: ; notranslate\" title=\"\">\n&lt;ItemGroup&gt;\n    &lt;PackageReference Include=&quot;Microsoft.AspNetCore.Hosting&quot; Version=&quot;2.3.0&quot; \/&gt;\n    &lt;PackageReference Include=&quot;Microsoft.AspNetCore.Server.Kestrel&quot; Version=&quot;2.3.0&quot; \/&gt;\n    &lt;PackageReference Include=&quot;Microsoft.Extensions.Hosting&quot; Version=&quot;9.0.5&quot; \/&gt;\n    &lt;PackageReference Include=&quot;Microsoft.AspNetCore.Http&quot; Version=&quot;2.3.0&quot; \/&gt;\n    &lt;PackageReference Include=&quot;MQTTnet&quot; Version=&quot;5.0.1.1416&quot; \/&gt;\n    &lt;PackageReference Include=&quot;MQTTnet.Server&quot; Version=&quot;5.0.1.1416&quot; \/&gt;\n    &lt;PackageReference Include=&quot;Serilog.Extensions.Hosting&quot; Version=&quot;9.0.0&quot; \/&gt;\n    &lt;PackageReference Include=&quot;Serilog.Settings.Configuration&quot; Version=&quot;9.0.0&quot; \/&gt;\n    &lt;PackageReference Include=&quot;Serilog.Sinks.Console&quot; Version=&quot;6.0.0&quot; \/&gt;\n&lt;\/ItemGroup&gt;\n<\/pre><\/div>\n\n\n<p>Dato che il nostro applicativo istanzier\u00e0, oltre ad un server MQTT, anche un endpoint API tramite Kestrel, bisogna utilizzare l&#8217;SDK Web.<\/p>\n\n\n\n<p>Per il broker MQTT utilizziamo la libreria&nbsp;<a href=\"https:\/\/github.com\/dotnet\/MQTTnet\" target=\"_blank\" rel=\"noreferrer noopener\"><strong>MQTTnet<\/strong><\/a>&nbsp;e per il logging la libreria&nbsp;<a href=\"https:\/\/github.com\/serilog\/serilog\" target=\"_blank\" rel=\"noreferrer noopener\"><strong>Serilog<\/strong><\/a>.<\/p>\n\n\n\n<p>Creiamo ora il file&nbsp;<code>appsettings.json<\/code>&nbsp;che conterr\u00e0 la configurazione&nbsp;<em>Serilog<\/em>&nbsp;e la propriet\u00e0&nbsp;<em>SensorHistoryItemSize<\/em>&nbsp;(propriet\u00e0 che indicher\u00e0 il numero massimo di elementi salvati nello storico).<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n{\n    &quot;AppSettings&quot;: {\n        &quot;SensorHistoryItemSize&quot;: 100\n    },\n    &quot;Serilog&quot;: {\n        &quot;MinimumLevel&quot;: {\n            &quot;Default&quot;: &quot;Debug&quot;,\n            &quot;Override&quot;: {\n                &quot;Microsoft&quot;: &quot;Fatal&quot;\n            }\n        },\n        &quot;WriteTo&quot;: &#x5B;\n            {\n                &quot;Name&quot;: &quot;Console&quot;\n            }\n        ],\n        &quot;Enrich&quot;: &#x5B;\n            &quot;FromLogContext&quot;,\n            &quot;WithExceptionDetails&quot;\n        ],\n        &quot;Properties&quot;: {\n            &quot;ApplicationName&quot;: &quot;broker&quot;,\n            &quot;Environment&quot;: &quot;Int&quot;\n        }\n    }\n}\n<\/pre><\/div>\n\n\n<p>Cos\u00ec configurato, Serilog scriver\u00e0 sulla console.<\/p>\n\n\n\n<p>Impostiamo&nbsp;<em>CopyToOutputDirectory<\/em>&nbsp;per il nuovo file di configurazione nel file di progetto&nbsp;<code>broker.csproj<\/code><\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: xml; title: ; notranslate\" title=\"\">\n&lt;ItemGroup&gt;\n    &lt;None Update=&quot;appsettings.json&quot;&gt;\n      &lt;CopyToOutputDirectory&gt;PreserveNewest&lt;\/CopyToOutputDirectory&gt;\n    &lt;\/None&gt;\n&lt;\/ItemGroup&gt;\n<\/pre><\/div>\n\n\n<p>Creiamo la classe&nbsp;<code>AppSettings.cs<\/code>&nbsp;per la propriet\u00e0&nbsp;<em>SensorHistoryItemSize<\/em><\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\nnamespace broker\n{\n    public class AppSettings\n    {\n        public int SensorHistoryItemSize { get; set; }\n    }\n}\n<\/pre><\/div>\n\n\n<p>Aggiungiamo le classi dei modelli&nbsp;<em>Sensor<\/em>&nbsp;e&nbsp;<em>Payload<\/em>&nbsp;(<code>Models.cs<\/code>)<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\nnamespace broker\n{\n    class Sensor\n    {\n        public required string SensorName { get; set; }\n        public required string LatestValueTopic { get; set; }\n        public required string HistoryTopic { get; set; }\n    }\n\n    class Payload\n    {\n        public DateTime TimestampUtc { get; set; }\n        public required string SensorName { get; set; }\n        public int Value { get; set; }\n    }\n}\n<\/pre><\/div>\n\n\n<p>e il file&nbsp;<code>Cns.cs<\/code>&nbsp;per le costanti<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\nnamespace broker\n{\n    internal class Cns\n    {\n        internal const string AppSettingsFile = &quot;appsettings.json&quot;;\n\n        internal readonly static IReadOnlyList&lt;Sensor&gt; Sensors = &#x5B;\n            new Sensor{\n                SensorName = &quot;sensor1&quot;,\n                LatestValueTopic = &quot;sensor1\/latest&quot;,\n                HistoryTopic = &quot;sensor1\/history&quot;,\n            },\n            new Sensor{\n                SensorName = &quot;sensor2&quot;,\n                LatestValueTopic = &quot;sensor2\/latest&quot;,\n                HistoryTopic = &quot;sensor2\/history&quot;,\n            },\n            new Sensor{\n                SensorName = &quot;sensor3&quot;,\n                LatestValueTopic = &quot;sensor3\/latest&quot;,\n                HistoryTopic = &quot;sensor3\/history&quot;,\n            },\n        ];\n    }\n}\n<\/pre><\/div>\n\n\n<p>Iniziamo ora ad implementare il&nbsp;<em>WebHost<\/em>&nbsp;principale dell&#8217;applicazione.<\/p>\n\n\n\n<p>Modifichiamo il file&nbsp;<code>Program.cs<\/code>&nbsp;come segue<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\nusing broker;\nusing Serilog;\nusing System.Text.Json;\n\nvar configuration = new ConfigurationBuilder()\n    .SetBasePath(Directory.GetCurrentDirectory())\n    .AddJsonFile(Cns.AppSettingsFile)\n    .Build();\n\nvar hostBuilder = new HostBuilder()\n    .ConfigureWebHostDefaults(webBuilder =&gt;\n    {\n        webBuilder.UseUrls(&quot;https:\/\/localhost:8080&quot;);\n        webBuilder.UseKestrel();\n\n        webBuilder.Configure(app =&gt;\n        {\n            app.UseHttpsRedirection();\n\n            app.Map(&quot;\/api\/sensors&quot;, appBuilder =&gt;\n            {\n                appBuilder.Run(async context =&gt;\n                {\n                    context.Response.ContentType = &quot;application\/json&quot;;\n\n                    var options = new JsonSerializerOptions\n                    {\n                        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,\n                        WriteIndented = true\n                    };\n\n                    await context.Response.WriteAsync(JsonSerializer.Serialize(Cns.Sensors, options));\n                });\n            });\n\n            app.Run(async context =&gt;\n            {\n                context.Response.ContentType = &quot;text\/plain&quot;;\n                await context.Response.WriteAsync(&quot;Welcome to MQTT Sensor Example, visit \/api\/sensors for available sensor list&quot;);\n            });\n        });\n    })\n    .ConfigureLogging(logging =&gt;\n    {\n        Log.Logger = new LoggerConfiguration()\n                    .ReadFrom.Configuration(configuration)\n                    .CreateLogger();\n    });\n\nawait hostBuilder.RunConsoleAsync();\n<\/pre><\/div>\n\n\n<p>Vediamo il codice sopra pi\u00f9 nel dettaglio:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\nvar configuration = new ConfigurationBuilder()\n  .SetBasePath(Directory.GetCurrentDirectory())\n  .AddJsonFile(Cns.AppSettingsFile)\n  .Build();\n<\/pre><\/div>\n\n\n<ul class=\"wp-block-list\">\n<li>Carica la configurazione contenuta nel file&nbsp;<code>appsettings.json<\/code>&nbsp;per iniettarla successivamente nella creazione del logger:<\/li>\n<\/ul>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\nLog.Logger = new LoggerConfiguration()\n                    .ReadFrom.Configuration(configuration)\n                    .CreateLogger();\n<\/pre><\/div>\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\nwebBuilder.UseUrls(&quot;https:\/\/localhost:8080&quot;);\nwebBuilder.UseKestrel();\n...\napp.UseHttpsRedirection();\n<\/pre><\/div>\n\n\n<ul class=\"wp-block-list\">\n<li>Abilita Kestrel in ascolto sulla porta 8080 e re-dirige il traffico HTTP verso HTTPS<\/li>\n<\/ul>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\napp.Run(async context =&gt;\n{\n  context.Response.ContentType = &quot;text\/plain&quot;;\n  await context.Response.WriteAsync(&quot;Welcome to MQTT Sensor Example, visit \/api\/sensors for available sensor list&quot;);\n});\n<\/pre><\/div>\n\n\n<ul class=\"wp-block-list\">\n<li>Permette di restituire il messaggio informativo indicato se non \u00e8 richiesta l&#8217;API impostata&nbsp;<code>\/api\/sensors<\/code>&nbsp;seguente<\/li>\n<\/ul>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\napp.Map(&quot;\/api\/sensors&quot;, appBuilder =&gt;\n{\n  appBuilder.Run(async context =&gt;\n  {\n      context.Response.ContentType = &quot;application\/json&quot;;\n\n      var options = new JsonSerializerOptions\n      {\n          PropertyNamingPolicy = JsonNamingPolicy.CamelCase,\n          WriteIndented = true\n      };\n\n      await context.Response.WriteAsync(JsonSerializer.Serialize(Cns.Sensors, options));\n  });\n});\n<\/pre><\/div>\n\n\n<p>Visitando&nbsp;<code>https:\/\/localhost:8080<\/code>&nbsp;otterremo<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"622\" height=\"44\" src=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2025\/06\/image-3.png\" alt=\"\" class=\"wp-image-20194\" srcset=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2025\/06\/image-3.png 622w, https:\/\/cloudsurfers.it\/wp-content\/uploads\/2025\/06\/image-3-300x21.png 300w\" sizes=\"auto, (max-width: 622px) 100vw, 622px\" \/><\/figure>\n<\/div>\n\n\n<p>mentre visitando&nbsp;<code>https:\/\/localhost:8080\/api\/sensors<\/code><\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"369\" height=\"311\" src=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2025\/06\/image-4.png\" alt=\"\" class=\"wp-image-20195\" srcset=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2025\/06\/image-4.png 369w, https:\/\/cloudsurfers.it\/wp-content\/uploads\/2025\/06\/image-4-300x253.png 300w\" sizes=\"auto, (max-width: 369px) 100vw, 369px\" \/><\/figure>\n<\/div>\n\n\n<p>Per la parte MQTT aggiungeremo un&nbsp;<em>HostedService<\/em>&nbsp;aggiungendo il file&nbsp;<code>ConsoleHostedService.cs<\/code>&nbsp;con il seguente contenuto:<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\nusing System.Text.Json;\nusing broker;\nusing Microsoft.Extensions.Options;\nusing MQTTnet;\nusing MQTTnet.Server;\nusing Serilog;\n\ninternal sealed class ConsoleHostedService(IOptions&lt;AppSettings&gt; appSettings,\n                            IHostApplicationLifetime appLifetime) : IHostedService\n{\n    private readonly AppSettings _appSettings = appSettings.Value;\n    private readonly IHostApplicationLifetime _appLifetime = appLifetime;\n    private MqttServer? _mqttServer;\n    private readonly List&lt;Payload&gt; _history = &#x5B;];\n\n    public Task StartAsync(CancellationToken cancellationToken)\n    {\n        _appLifetime.ApplicationStarted.Register(OnStarted);\n        _appLifetime.ApplicationStopping.Register(OnStopping);\n        _appLifetime.ApplicationStopped.Register(OnStopped);\n\n        return Task.CompletedTask;\n    }\n\n    private async void OnStarted()\n    {\n        try\n        {\n            Log.Logger.Information(&quot;Starting broker service&quot;);\n\n            var options = new MqttServerOptionsBuilder()\n                .WithDefaultEndpoint()\n                .WithDefaultEndpointPort(1883)\n                .Build();\n\n            var mqttFactory = new MqttServerFactory();\n            _mqttServer = mqttFactory.CreateMqttServer(options);\n\n            _mqttServer.ClientConnectedAsync += e =&gt;\n            {\n                Log.Logger.Information(&quot;Client {clientId} connected&quot;, e.ClientId);\n                return Task.CompletedTask;\n            };\n\n            _mqttServer.ClientDisconnectedAsync += e =&gt;\n            {\n                Log.Logger.Information(&quot;Client {clientId} disconnected&quot;, e.ClientId);\n                return Task.CompletedTask;\n            };\n\n            await _mqttServer.StartAsync();\n\n            var rnd = new Random();\n\n            while (true)\n            {\n                foreach (var sensor in Cns.Sensors)\n                {\n                    try\n                    {\n                        \/\/ random int from 0 to 100\n                        var value = rnd.Next(101);\n\n                        var payload = new Payload\n                        {\n                            TimestampUtc = DateTime.UtcNow,\n                            SensorName = sensor.SensorName,\n                            Value = value,\n                        };\n\n                        var jsonOptions = new JsonSerializerOptions\n                        {\n                            PropertyNamingPolicy = JsonNamingPolicy.CamelCase,\n                            WriteIndented = true\n                        };\n\n                        _history.Add(payload);\n                        if (_history.Where(t =&gt; t.SensorName == sensor.SensorName).Count() &gt; _appSettings.SensorHistoryItemSize)\n                            _history.Remove(_history.First(t =&gt; t.SensorName == sensor.SensorName));\n\n                        var latestValueMessage = new MqttApplicationMessageBuilder()\n                            .WithTopic(sensor.LatestValueTopic)\n                            .WithPayload(JsonSerializer.Serialize(payload, jsonOptions))\n                            .WithRetainFlag(true)\n                            .Build();\n\n                        await _mqttServer.InjectApplicationMessage(new InjectedMqttApplicationMessage(latestValueMessage), CancellationToken.None);\n\n                        Log.Logger.Information(&quot;Message sended for sensor {sensorName} on topic {topic} with value {value}&quot;, sensor.SensorName, sensor.LatestValueTopic, value);\n\n                        var sensorHistory = _history.Where(t =&gt; t.SensorName == sensor.SensorName).ToList();\n\n                        var historyMessage = new MqttApplicationMessageBuilder()\n                            .WithTopic(sensor.HistoryTopic)\n                            .WithPayload(JsonSerializer.Serialize(sensorHistory.SkipLast(1), jsonOptions))\n                            .WithRetainFlag(true)\n                            .Build();\n\n                        await _mqttServer.InjectApplicationMessage(new InjectedMqttApplicationMessage(historyMessage), CancellationToken.None);\n\n                        Log.Logger.Information(&quot;Updated history for sensor {sensorName} on topic {topic} (history size {historySize})&quot;, sensor.SensorName, sensor.HistoryTopic, sensorHistory.Count);\n                    }\n                    finally\n                    {\n                        Thread.Sleep(1000);\n                    }\n                }\n\n                Thread.Sleep(5000);\n            }\n        }\n        catch (Exception ex)\n        {\n            Log.Logger.Error(ex, &quot;Unhandled exception!&quot;);\n            _appLifetime.StopApplication();\n        }\n    }\n\n    private void OnStopping()\n    {\n        Log.Logger.Information(&quot;Stopping broker service&quot;);\n        _mqttServer?.Dispose();\n    }\n\n    private void OnStopped()\n    {\n        Log.Logger.Information(&quot;Stopped broker service&quot;);\n    }\n\n\n    public Task StopAsync(CancellationToken cancellationToken)\n    {\n        return Task.CompletedTask;\n    }\n}\n<\/pre><\/div>\n\n\n<p>Successivamente aggiungiamo il nuovo&nbsp;<strong>HostedService<\/strong>&nbsp;al&nbsp;<em>WebHost<\/em>&nbsp;(<code>Program.cs<\/code>), appena dopo la configurazione del logger ed iniettiamo la classe per la configurazione (<em>AppSettings<\/em>).<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\n...\n.ConfigureServices((hostContext, services) =&gt;\n{\n    services.AddOptions&lt;AppSettings&gt;().Bind(configuration.GetSection(&quot;AppSettings&quot;));\n    services.AddHostedService&lt;ConsoleHostedService&gt;();\n});\n<\/pre><\/div>\n\n\n<p>Vediamo il codice della logica MQTT pi\u00f9 nel dettaglio:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Il broker MQTT viene istanzato con le impostazioni di default utilizzando la porta&nbsp;<code>1883<\/code>.<\/li>\n\n\n\n<li>I due eventi&nbsp;<strong>ClientConnectedAsync<\/strong>&nbsp;e&nbsp;<strong>ClientDisconnectedAsync<\/strong>&nbsp;ci forniranno informazioni in merito ai client che vengono connessi ed eventualmente disconnessi<\/li>\n<\/ul>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\nvar options = new MqttServerOptionsBuilder()\n  .WithDefaultEndpoint()\n  .WithDefaultEndpointPort(1883)\n  .Build();\n\nvar mqttFactory = new MqttServerFactory();\n_mqttServer = mqttFactory.CreateMqttServer(options);\n  \n_mqttServer.ClientConnectedAsync += e =&gt;\n{\n    Log.Logger.Information(&quot;Client {clientId} connected&quot;, e.ClientId);\n    return Task.CompletedTask;\n};\n\n_mqttServer.ClientDisconnectedAsync += e =&gt;\n{\n    Log.Logger.Information(&quot;Client {clientId} disconnected&quot;, e.ClientId);\n    return Task.CompletedTask;\n};\n\nawait _mqttServer.StartAsync();\n<\/pre><\/div>\n\n\n<ul class=\"wp-block-list\">\n<li>Successivamente in modo ciclico verranno inviati due messaggi per ogni sensore: uno contenente l&#8217;ultimo valore (un intero casuale da 0 a 100)<\/li>\n<\/ul>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\nvar latestValueMessage = new MqttApplicationMessageBuilder()\n  .WithTopic(sensor.LatestValueTopic)\n  .WithPayload(JsonSerializer.Serialize(payload))\n  .WithRetainFlag(true)\n  .Build();\n\nawait _mqttServer.InjectApplicationMessage(new InjectedMqttApplicationMessage(latestValueMessage), CancellationToken.None);\n<\/pre><\/div>\n\n\n<ul class=\"wp-block-list\">\n<li>ed uno contenente lo storico relativo al sensore corrente escluso l&#8217;ultimo valore inviato sul topic&nbsp;<em>latest<\/em><\/li>\n<\/ul>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\nvar historyMessage = new MqttApplicationMessageBuilder()\n  .WithTopic(sensor.HistoryTopic)\n  .WithPayload(JsonSerializer.Serialize(sensorHistory.SkipLast(1)))\n  .WithRetainFlag(true)\n  .Build();\n\nawait _mqttServer.InjectApplicationMessage(new InjectedMqttApplicationMessage(historyMessage), CancellationToken.None);\n<\/pre><\/div>\n\n\n<ul class=\"wp-block-list\">\n<li>Come si pu\u00f2 vedere, entrambe i messaggi hanno il&nbsp;<em>flag<\/em>&nbsp;<strong>Retain<\/strong>&nbsp;impostato a&nbsp;<em>TRUE<\/em>. Questo \u00e8 strettamente necessario per far si che i client, quando connessi, ricevano sempre l&#8217;ultimo messaggio disponibile su entrambe i&nbsp;<em>topic<\/em>. In questo modo avremo sempre a disposizione l&#8217;ultimo valore di temperatura e lo storico, anche se i messaggi sono stati inviati dal broker in un momento precedente rispetto a quando il client ha stabilito la connessione con esso.<\/li>\n\n\n\n<li>Nello storico, per ogni sensore, vengono mantenuti N valori. N viene impostato tramite la variabile&nbsp;<em>SensorHistoryItemSize<\/em><\/li>\n<\/ul>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: csharp; title: ; notranslate\" title=\"\">\n_history.Add(payload);\nif (_history.Where(t =&gt; t.SensorName == sensor.SensorName).Count() &gt; _appSettings.SensorHistoryItemSize)\n  _history.Remove(_history.First(t =&gt; t.SensorName == sensor.SensorName));\n<\/pre><\/div>\n\n\n<p>A questo punto il nostro server \u00e8 completato.<br>Eseguendolo verranno stampate all&#8217;interno della console le informazioni di log relative ai messaggi inviati sui vari&nbsp;<em>topic<\/em>.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"727\" height=\"185\" src=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2025\/06\/image-5.png\" alt=\"\" class=\"wp-image-20197\" srcset=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2025\/06\/image-5.png 727w, https:\/\/cloudsurfers.it\/wp-content\/uploads\/2025\/06\/image-5-300x76.png 300w\" sizes=\"auto, (max-width: 727px) 100vw, 727px\" \/><\/figure>\n<\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"il-client\">Il client<\/h3>\n\n\n\n<p>Il client che creeremo, come detto nell&#8217;introduzione, sar\u00e0 un&#8217;applicazione Flutter, nello specifico un&#8217;applicazione&nbsp;<strong>Android<\/strong>. Il framework e le librerie utilizzare sono cross-platform, la guida quindi, rimane valida anche per un&#8217;applicazione Flutter iOS.<\/p>\n\n\n\n<p>Nella root di progetto (<code>sensors_example<\/code>) eseguiamo il comando&nbsp;<code>flutter create -e client --platforms android<\/code>&nbsp;per creare un progetto vuoto Flutter con il solo template del progetto Android.<\/p>\n\n\n\n<p>Aggiungiamo dentro al file&nbsp;<code>launch.json<\/code>&nbsp;(cartella&nbsp;<code>.vscode<\/code>) la configurazione per il l&#8217;esecuzione dell&#8217;applicativo client<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n{\n    &quot;name&quot;: &quot;client&quot;,\n    &quot;request&quot;: &quot;launch&quot;,\n    &quot;type&quot;: &quot;dart&quot;,\n    &quot;program&quot;: &quot;client\/lib\/main.dart&quot;\n}\n<\/pre><\/div>\n\n\n<p>Eseguendo con F5 otterremo questo risultato<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"258\" height=\"570\" src=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2025\/06\/image-6-edited.png\" alt=\"\" class=\"wp-image-20200\" style=\"width:223px;height:auto\" srcset=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2025\/06\/image-6-edited.png 258w, https:\/\/cloudsurfers.it\/wp-content\/uploads\/2025\/06\/image-6-edited-136x300.png 136w\" sizes=\"auto, (max-width: 258px) 100vw, 258px\" \/><\/figure>\n<\/div>\n\n\n<p>Aggiungiamo al progetto le librerie necessarie<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: yaml; title: ; notranslate\" title=\"\">\nhttp: ^1.4.0\nmqtt_client: ^10.8.0\nintl: ^0.20.2\n<\/pre><\/div>\n\n\n<p><a href=\"https:\/\/pub.dev\/packages\/http\" target=\"_blank\" rel=\"noreferrer noopener\"><strong>http<\/strong><\/a>&nbsp;per la chiamata API dell&#8217;elenco dei&nbsp;<em>topic<\/em>,&nbsp;<a href=\"https:\/\/pub.dev\/packages\/mqtt_client\" target=\"_blank\" rel=\"noreferrer noopener\"><strong>mqtt_client<\/strong><\/a>&nbsp;per connettersi al broker MQTT istanziato tramite il server ed infine&nbsp;<a href=\"https:\/\/pub.dev\/packages\/intl\" target=\"_blank\" rel=\"noreferrer noopener\"><strong>intl<\/strong><\/a>&nbsp;per lo strumento&nbsp;<em>DateFormat<\/em>&nbsp;che utilizzeremo per la formattazione della data relativa alla rilevazione della temperatura.<\/p>\n\n\n\n<p>Iniziamo con la creazione dei modelli&nbsp;<em>SensorVM<\/em>&nbsp;(<code>lib\/models\/sensor.dart<\/code>) e&nbsp;<em>PayloadVM<\/em>&nbsp;(<code>lib\/models\/payload.dart<\/code>), rispettivamente per l&#8217;elenco dei sensori con le relative informazioni sui&nbsp;<em>topic<\/em>&nbsp;MQTT ed i messaggi contenenti i valori temperatura rilevati<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nclass SensorVM {\n  final String sensorName;\n  final String latestValueTopic;\n  final String historyTopic;\n\n  SensorVM({\n    required this.sensorName,\n    required this.latestValueTopic,\n    required this.historyTopic,\n  });\n\n  Map&amp;lt;String, dynamic&gt; toMap() {\n    return {\n      &#039;sensorName&#039;: sensorName,\n      &#039;latestValueTopic&#039;: latestValueTopic,\n      &#039;historyTopic&#039;: historyTopic,\n    };\n  }\n\n  factory SensorVM.fromMap(Map&amp;lt;String, dynamic&gt; map) {\n    return SensorVM(\n      sensorName: map&#x5B;&#039;sensorName&#039;],\n      latestValueTopic: map&#x5B;&#039;latestValueTopic&#039;],\n      historyTopic: map&#x5B;&#039;historyTopic&#039;],\n    );\n  }\n}\n\n<\/pre><\/div>\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nclass PayloadVM {\n  final DateTime timestampUtc;\n  final String sensorName;\n  final int value;\n\n  PayloadVM({\n    required this.timestampUtc,\n    required this.sensorName,\n    required this.value,\n  });\n\n  Map&amp;lt;String, dynamic&gt; toMap() {\n    return {\n      &#039;timestampUtc&#039;: timestampUtc.toIso8601String(),\n      &#039;sensorName&#039;: sensorName,\n      &#039;value&#039;: value,\n    };\n  }\n\n  factory PayloadVM.fromMap(Map&amp;lt;String, dynamic&gt; map) {\n    return PayloadVM(\n      timestampUtc: DateTime.parse(map&#x5B;&#039;timestampUtc&#039;]),\n      sensorName: map&#x5B;&#039;sensorName&#039;],\n      value: map&#x5B;&#039;value&#039;],\n    );\n  }\n}\n\n<\/pre><\/div>\n\n\n<p>Procediamo con la creazione del provider di navigazione (<code>lib\/providers\/navigation.dart<\/code>) per la gestione di un&nbsp;<em>NavigatorState<\/em>&nbsp;condiviso tra le varie pagine<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nimport &#039;package:flutter\/material.dart&#039;;\n\nclass NavigationProvider {\n  NavigationProvider._privateContructor();\n\n  static final NavigationProvider instance =\n      NavigationProvider._privateContructor();\n\n  GlobalKey&amp;lt;NavigatorState&gt; navigatorKey = GlobalKey&amp;lt;NavigatorState&gt;();\n}\n<\/pre><\/div>\n\n\n<p>creiamo poi il provider per la gestione delle chiamate API (<code>lib\/providers\/api.dart<\/code>)<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nimport &#039;dart:async&#039;;\nimport &#039;dart:convert&#039;;\n\nimport &#039;package:http\/http.dart&#039; as http;\n\nimport &#039;..\/models\/sensor.dart&#039;;\n\nclass ApiProvider {\n  ApiProvider._privateContructor();\n\n  static ApiProvider instance = ApiProvider._privateContructor();\n\n  Future&amp;lt;List&amp;lt;SensorVM&gt;&gt; getSensors() async {\n    final response = await http.get(Uri.https(&#039;10.0.2.2:8080&#039;, &#039;\/api\/sensors&#039;));\n    final itemList = jsonDecode(response.body);\n    return List.generate(\n      itemList.length,\n      (index) =&gt; SensorVM.fromMap(itemList&#x5B;index]),\n    );\n  }\n}\n<\/pre><\/div>\n\n\n<p>L&#8217;indirizzo&nbsp;<code>10.0.2.2<\/code>&nbsp;fa riferimento all&#8217;indirizzo&nbsp;<code>localhost<\/code>&nbsp;della macchina di sviluppo che esegue l&#8217;emulatore Android (<a href=\"https:\/\/developer.android.com\/studio\/run\/emulator-networking?hl=it\" target=\"_blank\" rel=\"noreferrer noopener\">qui i dettagli<\/a>).<\/p>\n\n\n\n<p>Siamo ora pronti a creare le due pagine: la lista dei sensori ed il dettaglio del singolo sensore.<\/p>\n\n\n\n<p>Iniziamo con la lista dei sensori&nbsp;<code>lib\/sensor_list.dart<\/code><\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nimport &#039;package:flutter\/material.dart&#039;;\n\nimport &#039;models\/sensor.dart&#039;;\nimport &#039;providers\/api.dart&#039;;\n\nclass SensorListPage extends StatefulWidget {\n  const SensorListPage({super.key});\n\n  @override\n  State&amp;lt;StatefulWidget&gt; createState() {\n    return _SensorListPageState();\n  }\n}\n\nclass _SensorListPageState extends State&amp;lt;SensorListPage&gt; {\n  late List&amp;lt;SensorVM&gt; _sensorList;\n\n  Future&amp;lt;void&gt; initSensorList() async {\n    _sensorList = await ApiProvider.instance.getSensors();\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: const Text(&quot;Sensor List&quot;)),\n      body: SafeArea(\n        child: FutureBuilder(\n          future: initSensorList(),\n          builder: (context, snapshot) {\n            if (snapshot.connectionState == ConnectionState.done) {\n              return ListView.separated(\n                padding: const EdgeInsets.all(8),\n                itemBuilder: (context, index) {\n                  return ListTile(\n                    onTap: () async {\n                      await Navigator.pushNamed(\n                        context,\n                        &#039;\/sensors\/detail&#039;,\n                        arguments: {&quot;sensor&quot;: _sensorList&#x5B;index]},\n                      );\n                    },\n                    title: Text(_sensorList&#x5B;index].sensorName),\n                  );\n                },\n                separatorBuilder: (context, index) {\n                  return const Divider();\n                },\n                itemCount: _sensorList.length,\n              );\n            } else {\n              return const Center(child: Text(&quot;No data&quot;));\n            }\n          },\n        ),\n      ),\n    );\n  }\n}\n<\/pre><\/div>\n\n\n<p>La pagina contiene una&nbsp;<em>ListView<\/em>&nbsp;popolata dal&nbsp;<em>Future<\/em>&nbsp;<strong>initSensorList()<\/strong>.<\/p>\n\n\n\n<p>Alla pressione di un elemento in lista viene caricata tramite&nbsp;<em>Navigator<\/em>&nbsp;la pagina di dettaglio con come argomento il sensore selezionato.<\/p>\n\n\n\n<p>Procediamo con la pagina di dettaglio&nbsp;<code>lib\/sensor_detail.dart<\/code><\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nimport &#039;dart:convert&#039;;\nimport &#039;package:client\/models\/payload.dart&#039;;\nimport &#039;package:flutter\/material.dart&#039;;\nimport &#039;package:intl\/intl.dart&#039;;\nimport &#039;package:mqtt_client\/mqtt_client.dart&#039;;\nimport &#039;package:mqtt_client\/mqtt_server_client.dart&#039;;\n\nimport &#039;models\/sensor.dart&#039;;\n\nclass SensorDetailPage extends StatefulWidget {\n  const SensorDetailPage(this.sensor, {super.key});\n\n  final SensorVM sensor;\n\n  @override\n  State&amp;lt;StatefulWidget&gt; createState() {\n    return _SensorDetailPageState();\n  }\n}\n\nclass _SensorDetailPageState extends State&amp;lt;SensorDetailPage&gt; {\n  late MqttServerClient client;\n\n  final ValueNotifier&amp;lt;PayloadVM?&gt; _latest = ValueNotifier(null);\n  final ValueNotifier&amp;lt;List&amp;lt;PayloadVM&gt;&gt; _history = ValueNotifier(&#x5B;]);\n\n  final DateFormat formatter = DateFormat(&#039;yyyy-MM-dd HH:mm:ss&#039;);\n\n  @override\n  void initState() {\n    initMqtt();\n    super.initState();\n  }\n\n  @override\n  void dispose() {\n    client.disconnect();\n    super.dispose();\n  }\n\n  initMqtt() async {\n    client = MqttServerClient(&#039;10.0.2.2&#039;, UniqueKey().toString());\n    client.setProtocolV311();\n\n    await client.connect();\n\n    client.subscribe(widget.sensor.latestValueTopic, MqttQos.atMostOnce);\n    client.subscribe(widget.sensor.historyTopic, MqttQos.atMostOnce);\n\n    client.updates!.listen((List&amp;lt;MqttReceivedMessage&amp;lt;MqttMessage?&gt;&gt;? c) {\n      final receivedMessage = c!&#x5B;0];\n      final message = receivedMessage.payload as MqttPublishMessage;\n      if (receivedMessage.topic == widget.sensor.latestValueTopic) {\n        final payload = PayloadVM.fromMap(\n          jsonDecode(\n                MqttPublishPayload.bytesToStringAsString(\n                  message.payload.message,\n                ),\n              )\n              as Map&amp;lt;String, dynamic&gt;,\n        );\n        _latest.value = payload;\n      } else if (receivedMessage.topic == widget.sensor.historyTopic) {\n        final decodedPayload =\n            jsonDecode(\n                  MqttPublishPayload.bytesToStringAsString(\n                    message.payload.message,\n                  ),\n                )\n                as List&amp;lt;dynamic&gt;;\n        final payload = List.generate(decodedPayload.length, (index) {\n          return PayloadVM.fromMap(decodedPayload&#x5B;index]);\n        });\n        _history.value = payload;\n      }\n    });\n  }\n\n  @override\n  Widget build(BuildContext context) {\n    return Scaffold(\n      appBar: AppBar(title: Text(&quot;Sensor ${widget.sensor.sensorName}&quot;)),\n      body: SizedBox(\n        height: double.infinity,\n        child: Column(\n          children: &#x5B;\n            Expanded(\n              child: ValueListenableBuilder(\n                valueListenable: _history,\n                builder: (context, value, child) {\n                  final reversedList = value.reversed.toList();\n                  return ListView.separated(\n                    itemBuilder: (context, index) {\n                      return ListTile(\n                        title: Text(\n                          &quot;${formatter.format(reversedList&#x5B;index].timestampUtc)} | value: ${reversedList&#x5B;index].value.toString()}&quot;,\n                        ),\n                      );\n                    },\n                    separatorBuilder: (context, index) =&gt; const Divider(),\n                    itemCount: value.length,\n                    reverse: true,\n                  );\n                },\n              ),\n            ),\n            ValueListenableBuilder(\n              valueListenable: _latest,\n              builder: (context, value, child) {\n                if (value != null) {\n                  return SizedBox(\n                    width: double.infinity,\n                    child: Padding(\n                      padding: EdgeInsets.all(16.0),\n                      child: Column(\n                        crossAxisAlignment: CrossAxisAlignment.start,\n                        spacing: 4.0,\n                        children: &#x5B;\n                          Text(&quot;latest&quot;),\n                          Text(\n                            formatter.format(value.timestampUtc),\n                            style: TextStyle(fontSize: 20),\n                          ),\n                          Text(\n                            value.value.toString(),\n                            style: TextStyle(fontSize: 80),\n                          ),\n                        ],\n                      ),\n                    ),\n                  );\n                }\n                return Text(&quot;---&quot;, style: TextStyle(fontSize: 80));\n              },\n            ),\n          ],\n        ),\n      ),\n    );\n  }\n}\n<\/pre><\/div>\n\n\n<p>All&#8217;avvio della pagina viene inizializzato il client MQTT (come gi\u00e0 indicato in riferimento al provider API, viene sempre utilizzato l&#8217;indirizzo&nbsp;<code>10.0.2.2<\/code>&nbsp;che rimanda all&#8217;indirizzo&nbsp;<code>localhost<\/code>&nbsp;della macchina che esegue l&#8217;emulatore).<\/p>\n\n\n\n<p>La porta&nbsp;<code>1883<\/code>&nbsp;\u00e8 preimpostata e non \u00e8 quindi necessario indicarla.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nclient = MqttServerClient(&#039;10.0.2.2&#039;, UniqueKey().toString());\nclient.setProtocolV311();\n\nawait client.connect();\n\nclient.subscribe(widget.sensor.latestValueTopic, MqttQos.atMostOnce);\nclient.subscribe(widget.sensor.historyTopic, MqttQos.atMostOnce);\n<\/pre><\/div>\n\n\n<p>Lo&nbsp;<em>StreamSubscription<\/em>&nbsp;<strong>listen<\/strong>&nbsp;viene invocato al ricevimento di messaggi nei&nbsp;<em>topic<\/em>&nbsp;a cui siamo sottoscritti<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nclient.updates!.listen((List&amp;lt;MqttReceivedMessage&amp;lt;MqttMessage?&gt;&gt;? c) {\n  final receivedMessage = c!&#x5B;0];\n  final message = receivedMessage.payload as MqttPublishMessage;\n  if (receivedMessage.topic == widget.sensor.latestValueTopic) {\n    final payload = PayloadVM.fromMap(\n      jsonDecode(\n            MqttPublishPayload.bytesToStringAsString(\n              message.payload.message,\n            ),\n          )\n          as Map&amp;lt;String, dynamic&gt;,\n    );\n    _latest.value = payload;\n  } else if (receivedMessage.topic == widget.sensor.historyTopic) {\n    final decodedPayload =\n        jsonDecode(\n              MqttPublishPayload.bytesToStringAsString(\n                message.payload.message,\n              ),\n            )\n            as List&amp;lt;dynamic&gt;;\n    final payload = List.generate(decodedPayload.length, (index) {\n      return PayloadVM.fromMap(decodedPayload&#x5B;index]);\n    });\n    _history.value = payload;\n  }\n});\n<\/pre><\/div>\n\n\n<p>I due&nbsp;<em>ValueNotifier<\/em>&nbsp;_<strong>latest<\/strong>&nbsp;ed _<strong>history<\/strong>&nbsp;vengono utilizzati per aggiornare gli elementi mostrati a schermo.<\/p>\n\n\n\n<p>La pagina presenta due componenti principali: un&nbsp;<em>widget<\/em>&nbsp;<strong>ListView<\/strong>&nbsp;popolato con lo storico valori ed in fondo, alla schermata, due&nbsp;<em>widget<\/em>&nbsp;che mostrato la data e ora dell&#8217;ultima temperatura rilevata insieme al suo valore.<\/p>\n\n\n\n<p>Per concludere non resta che modificare il file d&#8217;ingresso&nbsp;<code>lib\/main.dart<\/code>&nbsp;come segue per impostare il&nbsp;<em>routing<\/em>&nbsp;alle due pagine create sopra.<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nimport &#039;dart:io&#039;;\n\nimport &#039;package:client\/sensor_detail.dart&#039;;\nimport &#039;package:client\/sensor_list.dart&#039;;\nimport &#039;package:flutter\/material.dart&#039;;\n\nimport &#039;providers\/navigation.dart&#039;;\n\nclass DevHttpOverrides extends HttpOverrides {\n  @override\n  HttpClient createHttpClient(SecurityContext? context) {\n    return super.createHttpClient(context)\n      ..badCertificateCallback = (X509Certificate cert, String host, int port) {\n        return true;\n      };\n  }\n}\n\nFuture&amp;lt;void&gt; main() async {\n  WidgetsFlutterBinding.ensureInitialized();\n  HttpOverrides.global = DevHttpOverrides();\n  runApp(const MainApp());\n}\n\nclass MainApp extends StatelessWidget {\n  const MainApp({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      navigatorKey: NavigationProvider.instance.navigatorKey,\n      initialRoute: &#039;\/sensors&#039;,\n      onGenerateRoute: (settings) {\n        final args = (settings.arguments ?? {}) as Map&amp;lt;dynamic, dynamic&gt;;\n        Widget? pageWidget;\n        switch (settings.name) {\n          case &#039;\/sensors&#039;:\n            pageWidget = SensorListPage();\n            break;\n          case &#039;\/sensors\/detail&#039;:\n            pageWidget = SensorDetailPage(args&#x5B;&quot;sensor&quot;]);\n          default:\n            pageWidget = null;\n            break;\n        }\n        return MaterialPageRoute(builder: (context) =&gt; pageWidget!);\n      },\n    );\n  }\n}\n<\/pre><\/div>\n\n\n<p>L&#8217;estensione di&nbsp;<em>HttpOverrides<\/em>&nbsp;<strong>DevHttpOverrides<\/strong>&nbsp;\u00e8 necessaria per non avere errori in merito al certificato SSL di sviluppo Kestrel sull&#8217;emulatore Android.<\/p>\n\n\n\n<p><em>In produzione questa estensione \u00e8 da evitare per questioni di sicurezza, consiglio di abilitarla o meno in base all&#8217;ambiente in cui ci si trova, sviluppo o produzione.<\/em><\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"risultato-finale\">Risultato finale<\/h3>\n\n\n\n<p>In primis lanciamo il broker posizionandoci sulla cartella&nbsp;<code>broker<\/code>&nbsp;appunto ed eseguiamo il comando&nbsp;<code>dotnet run<\/code>.<\/p>\n\n\n\n<p>Come gi\u00e0 visto al paragrafo dedicato, se tutto funziona, otterremo un risultato simile al seguente<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"687\" height=\"147\" src=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2025\/06\/image-7.png\" alt=\"\" class=\"wp-image-20201\" srcset=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2025\/06\/image-7.png 687w, https:\/\/cloudsurfers.it\/wp-content\/uploads\/2025\/06\/image-7-300x64.png 300w\" sizes=\"auto, (max-width: 687px) 100vw, 687px\" \/><\/figure>\n<\/div>\n\n\n<p>Lanciamo l&#8217;applicazione Flutter sull&#8217;emulatore Android selezionando il profilo di lancio&nbsp;<strong>client<\/strong>&nbsp;e premendo F5<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"268\" height=\"214\" src=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2025\/06\/image-8.png\" alt=\"\" class=\"wp-image-20202\"\/><\/figure>\n<\/div>\n\n\n<p>Ci troveremo davanti la lista dei sensori disponibili. Selezioniamone uno per visualizzarne il dettaglio<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"269\" height=\"601\" src=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2025\/06\/image-9.png\" alt=\"\" class=\"wp-image-20203\" srcset=\"https:\/\/cloudsurfers.it\/wp-content\/uploads\/2025\/06\/image-9.png 269w, https:\/\/cloudsurfers.it\/wp-content\/uploads\/2025\/06\/image-9-134x300.png 134w\" sizes=\"auto, (max-width: 269px) 100vw, 269px\" \/><\/figure>\n<\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"github\">GitHub<\/h3>\n\n\n\n<p>Il progetto realizzato in questo articolo \u00e8 disponibile su <strong><a href=\"https:\/\/github.com\/Cloudsurfers-Dev\/cloudsurfers_sensors-example\" target=\"_blank\" rel=\"noreferrer noopener\">GitHub<\/a><\/strong>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>In questa guida realizzeremo passo passo un&#8217;applicazione console\u00a0.NET\u00a0(il nostro server) che invier\u00e0 la temperatura attuale e le ultime rilevazioni effettuate di una serie di sensori ad un&#8217;applicazione\u00a0Flutter\u00a0Android (il nostro client).<\/p>\n<p>L&#8217;applicazione Flutter permetter\u00e0 di selezionare il sensore da un&#8217;elenco e visualizzarne le temperature rilevate. La comunicazione verr\u00e0 effettuata tramite protocollo\u00a0MQTT\u00a0e come ambiente di sviluppo utilizzeremo\u00a0VSCode.<\/p>\n","protected":false},"author":3,"featured_media":20183,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"wds_primary_category":36,"footnotes":""},"categories":[90,152,181,36],"tags":[94,96,111,93,182,183,190,191,110],"class_list":["post-20182","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-net-core","category-android","category-flutter","category-guide","tag-net-core","tag-app","tag-c","tag-dotnet-core","tag-flutter","tag-mobile","tag-mqtt","tag-mqtt-broker","tag-tutorial"],"_links":{"self":[{"href":"https:\/\/cloudsurfers.it\/index.php\/wp-json\/wp\/v2\/posts\/20182","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cloudsurfers.it\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/cloudsurfers.it\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/cloudsurfers.it\/index.php\/wp-json\/wp\/v2\/users\/3"}],"replies":[{"embeddable":true,"href":"https:\/\/cloudsurfers.it\/index.php\/wp-json\/wp\/v2\/comments?post=20182"}],"version-history":[{"count":0,"href":"https:\/\/cloudsurfers.it\/index.php\/wp-json\/wp\/v2\/posts\/20182\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cloudsurfers.it\/index.php\/wp-json\/wp\/v2\/media\/20183"}],"wp:attachment":[{"href":"https:\/\/cloudsurfers.it\/index.php\/wp-json\/wp\/v2\/media?parent=20182"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cloudsurfers.it\/index.php\/wp-json\/wp\/v2\/categories?post=20182"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cloudsurfers.it\/index.php\/wp-json\/wp\/v2\/tags?post=20182"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}