Added Mqtt and SignalR

This commit is contained in:
kunkliricsi 2020-11-09 18:12:07 +01:00
parent b87d90e5a4
commit 3632e56dc4
10 changed files with 312 additions and 5 deletions

View File

@ -0,0 +1,33 @@
using Birdmap.API.Options;
using Birdmap.API.Services.Mqtt;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
namespace Birdmap.API.Extensions
{
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddMqttClientServiceWithConfig(this IServiceCollection services, Action<AspCoreMqttClientOptions> configureOptions)
{
services.AddSingleton(serviceProvider =>
{
var optionBuilder = new AspCoreMqttClientOptions(serviceProvider);
configureOptions(optionBuilder);
return optionBuilder.Build();
});
services.AddSingleton<MqttClientService>();
services.AddSingleton<IHostedService>(serviceProvider =>
{
return serviceProvider.GetService<MqttClientService>();
});
services.AddSingleton(serviceProvider =>
{
var mqttClientService = serviceProvider.GetService<MqttClientService>();
var mqttClientServiceProvider = new MqttClientServiceProvider(mqttClientService);
return mqttClientServiceProvider;
});
return services;
}
}
}

View File

@ -0,0 +1,22 @@
using MQTTnet.Client.Options;
using System;
namespace Birdmap.API.Options
{
public class AspCoreMqttClientOptions : MqttClientOptionsBuilder
{
public IServiceProvider ServiceProvider { get; }
public AspCoreMqttClientOptions(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
}
public AspCoreMqttClientOptions WithTopic(string topic)
{
WithUserProperty("Topic", topic);
return this;
}
}
}

View File

@ -0,0 +1,36 @@
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;
namespace Birdmap.API.Services.Hubs
{
public class DevicesHub : Hub<IDevicesHubClient>
{
private readonly ILogger<DevicesHub> _logger;
public DevicesHub(ILogger<DevicesHub> logger)
{
_logger = logger;
}
public override Task OnConnectedAsync()
{
_logger.LogInformation("Client connected.");
return base.OnConnectedAsync();
}
public override Task OnDisconnectedAsync(Exception exception)
{
_logger.LogInformation("Client disconnected.");
return base.OnDisconnectedAsync(exception);
}
public Task UserJoinedAsync(Guid deviceId, DateTime date, double probability)
{
return Clients.All.NotifyDeviceAsync(deviceId, date, probability);
}
}
}

View File

@ -0,0 +1,10 @@
using System;
using System.Threading.Tasks;
namespace Birdmap.API.Services.Hubs
{
public interface IDevicesHubClient
{
Task NotifyDeviceAsync(Guid deviceId, DateTime date, double probability);
}
}

View File

@ -0,0 +1,15 @@
using Microsoft.Extensions.Hosting;
using MQTTnet.Client.Connecting;
using MQTTnet.Client.Disconnecting;
using MQTTnet.Client.Receiving;
namespace Birdmap.API.Services
{
public interface IMqttClientService : IHostedService,
IMqttClientConnectedHandler,
IMqttClientDisconnectedHandler,
IMqttApplicationMessageReceivedHandler
{
}
}

View File

@ -0,0 +1,134 @@
using Birdmap.API.Services.Hubs;
using Birdmap.BLL.Interfaces;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using MQTTnet;
using MQTTnet.Client;
using MQTTnet.Client.Connecting;
using MQTTnet.Client.Disconnecting;
using MQTTnet.Client.Options;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Birdmap.API.Services.Mqtt
{
public class MqttClientService : IMqttClientService
{
private readonly IMqttClient _mqttClient;
private readonly IMqttClientOptions _options;
private readonly ILogger<MqttClientService> _logger;
private readonly IInputService _inputService;
private readonly IHubContext<DevicesHub, IDevicesHubClient> _hubContext;
public MqttClientService(IMqttClientOptions options, ILogger<MqttClientService> logger, IInputService inputService, IHubContext<DevicesHub, IDevicesHubClient> hubContext)
{
_options = options;
_logger = logger;
_inputService = inputService;
_hubContext = hubContext;
_mqttClient = new MqttFactory().CreateMqttClient();
ConfigureMqttClient();
}
private void ConfigureMqttClient()
{
_mqttClient.ConnectedHandler = this;
_mqttClient.DisconnectedHandler = this;
_mqttClient.ApplicationMessageReceivedHandler = this;
}
private class Payload
{
[JsonProperty("tag")]
public Guid TagID { get; set; }
[JsonProperty("probability")]
public double Probability { get; set; }
}
public async Task HandleApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs eventArgs)
{
var message = eventArgs.ApplicationMessage.ConvertPayloadToString();
_logger.LogInformation($"Recieved [{eventArgs.ClientId}] " +
$"Topic: {eventArgs.ApplicationMessage.Topic} | Payload: {message} | QoS: {eventArgs.ApplicationMessage.QualityOfServiceLevel} | Retain: {eventArgs.ApplicationMessage.Retain}");
var payload = JsonConvert.DeserializeObject<Payload>(message);
var inputResponse = await _inputService.GetInputAsync(payload.TagID);
await _hubContext.Clients.All.NotifyDeviceAsync(inputResponse.Message.Device_id, inputResponse.Message.Date.UtcDateTime, payload.Probability);
}
public async Task HandleConnectedAsync(MqttClientConnectedEventArgs eventArgs)
{
try
{
var topic = _options.UserProperties.SingleOrDefault(up => up.Name == "Topic")?.Value;
_logger.LogInformation($"Connected. Auth result: {eventArgs.AuthenticateResult}. Subscribing to topic: {topic}");
await _mqttClient.SubscribeAsync(topic);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Cannot subscribe...");
}
}
public async Task HandleDisconnectedAsync(MqttClientDisconnectedEventArgs eventArgs)
{
_logger.LogWarning(eventArgs.Exception, $"Disconnected. Reason {eventArgs.ReasonCode}. Auth result: {eventArgs.AuthenticateResult}. Reconnecting...");
await Task.Delay(TimeSpan.FromSeconds(5));
try
{
await _mqttClient.ConnectAsync(_options, CancellationToken.None);
}
catch (Exception ex)
{
_logger.LogError(ex, $"Reconnect failed...");
}
}
public async Task StartAsync(CancellationToken cancellationToken)
{
try
{
await _mqttClient.ConnectAsync(_options);
if (!_mqttClient.IsConnected)
{
await _mqttClient.ReconnectAsync();
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Cannot connect...");
}
}
public async Task StopAsync(CancellationToken cancellationToken)
{
try
{
if (cancellationToken.IsCancellationRequested)
{
var disconnectOption = new MqttClientDisconnectOptions
{
ReasonCode = MqttClientDisconnectReason.NormalDisconnection,
ReasonString = "NormalDiconnection"
};
await _mqttClient.DisconnectAsync(disconnectOption, cancellationToken);
}
await _mqttClient.DisconnectAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, $"Cannot disconnect...");
}
}
}
}

View File

@ -0,0 +1,12 @@
namespace Birdmap.API.Services.Mqtt
{
public class MqttClientServiceProvider
{
public IMqttClientService MqttClientService { get; }
public MqttClientServiceProvider(IMqttClientService mqttClientService)
{
MqttClientService = mqttClientService;
}
}
}

View File

@ -1,5 +1,7 @@
using AutoMapper; using AutoMapper;
using Birdmap.API.Extensions;
using Birdmap.API.Middlewares; using Birdmap.API.Middlewares;
using Birdmap.API.Services.Hubs;
using Birdmap.BLL; using Birdmap.BLL;
using Birdmap.DAL; using Birdmap.DAL;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
@ -39,6 +41,8 @@ namespace Birdmap.API
services.AddAutoMapper(typeof(Startup)); services.AddAutoMapper(typeof(Startup));
services.AddSignalR();
var key = Encoding.ASCII.GetBytes(Configuration["Secret"]); var key = Encoding.ASCII.GetBytes(Configuration["Secret"]);
services.AddAuthentication(opt => services.AddAuthentication(opt =>
{ {
@ -59,6 +63,33 @@ namespace Birdmap.API
}; };
}); });
services.AddMqttClientServiceWithConfig(opt =>
{
var mqtt = Configuration.GetSection("Mqtt");
var mqttClient = mqtt.GetSection("ClientSettings");
var clientSettings = new
{
Id = mqttClient.GetValue<string>("Id"),
Username = mqttClient.GetValue<string>("Username"),
Password = mqttClient.GetValue<string>("Password"),
Topic = mqttClient.GetValue<string>("Topic"),
};
var mqttBrokerHost = mqtt.GetSection("BrokerHostSettings");
var brokerHostSettings = new
{
Host = mqttBrokerHost.GetValue<string>("Host"),
Port = mqttBrokerHost.GetValue<int>("Port"),
};
opt
.WithTopic(clientSettings.Topic)
.WithCredentials(clientSettings.Username, clientSettings.Password)
.WithClientId(clientSettings.Id)
.WithTcpServer(brokerHostSettings.Host, brokerHostSettings.Port);
});
// In production, the React files will be served from this directory // In production, the React files will be served from this directory
services.AddSpaStaticFiles(configuration => services.AddSpaStaticFiles(configuration =>
{ {
@ -91,6 +122,7 @@ namespace Birdmap.API
{ {
endpoints.MapHealthChecks("/health"); endpoints.MapHealthChecks("/health");
endpoints.MapControllers(); endpoints.MapControllers();
endpoints.MapHub<DevicesHub>("/hubs/devices");
}); });
app.UseSpa(spa => app.UseSpa(spa =>

View File

@ -28,5 +28,18 @@
} }
] ]
}, },
"UseDummyServices": true "UseDummyServices": true,
"Mqtt": {
"BrokerHostSettings": {
"Host": "localhost",
"Port": 1883
},
"ClientSettings": {
"Id": "ASP.NET Core client",
"Username": "username",
"Password": "password",
"Topic": "devices/output"
}
}
} }

View File

@ -15,14 +15,14 @@
<targets async="true"> <targets async="true">
<default-target-parameters xsi:type="File" keepFileOpen="false" maxArchiveFiles="10" archiveAboveSize="1048576"/> <default-target-parameters xsi:type="File" keepFileOpen="false" maxArchiveFiles="10" archiveAboveSize="1048576"/>
<target xsi:type="File" name="allFile" fileName="${basedir}Log/birdmap-all-${shortdate}.log" <target xsi:type="File" name="allFile" fileName="${basedir}Log/birdmap-all-${shortdate}.log"
layout="${longdate} [${event-properties:item=EventId_Id}] ${uppercase:${level}} ${logger} - ${message} ${exception:format=tostring}" /> layout="${longdate} [${threadname:whenEmpty=${threadid}}] ${uppercase:${level}} ${logger} - ${message} ${exception:format=tostring}" />
<target xsi:type="File" name="mqttFile" fileName="${basedir}Log/birdmap-mqtt-${shortdate}.log" <target xsi:type="File" name="mqttFile" fileName="${basedir}Log/birdmap-mqtt-${shortdate}.log"
layout="${longdate} [${event-properties:item=EventId_Id}] ${uppercase:${level}} ${logger} - ${message} ${exception:format=tostring}" /> layout="${longdate} [${threadname:whenEmpty=${threadid}}] ${uppercase:${level}} ${logger} - ${message} ${exception:format=tostring}" />
<!-- another file log, only own logs. Uses some ASP.NET core renderers --> <!-- another file log, only own logs. Uses some ASP.NET core renderers -->
<target xsi:type="File" name="ownFile" fileName="${basedir}Log/birdmap-own-${shortdate}.log" <target xsi:type="File" name="ownFile" fileName="${basedir}Log/birdmap-own-${shortdate}.log"
layout="${longdate} [${event-properties:item=EventId_Id}] ${uppercase:${level}} ${callsite} - ${message} ${exception:format=tostring} (url: ${aspnet-request-url})(action: ${aspnet-mvc-action})" /> layout="${longdate} [${threadname:whenEmpty=${threadid}}] ${uppercase:${level}} ${callsite} - ${message} ${exception:format=tostring} (url: ${aspnet-request-url})(action: ${aspnet-mvc-action})" />
</targets> </targets>
<!-- rules to map from logger name to target +--> <!-- rules to map from logger name to target +-->
@ -31,7 +31,7 @@
<logger name="*" minlevel="Trace" writeTo="allFile" /> <logger name="*" minlevel="Trace" writeTo="allFile" />
<!--Skip non-critical Mqtt logs--> <!--Skip non-critical Mqtt logs-->
<logger name="*.MqttDevicesController.*" minlevel="Trace" maxlevel="Warning" writeTo="mqttFile"/> <logger name="*.*Mqtt*.*" minlevel="Trace" maxlevel="Warning" writeTo="mqttFile" final="true"/>
<!--Skip non-critical Microsoft logs--> <!--Skip non-critical Microsoft logs-->
<logger name="Microsoft.*" maxlevel="Info" final="true" /> <logger name="Microsoft.*" maxlevel="Info" final="true" />