diff --git a/Birdmap.API/Extensions/ServiceCollectionExtensions.cs b/Birdmap.API/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..a765a80 --- /dev/null +++ b/Birdmap.API/Extensions/ServiceCollectionExtensions.cs @@ -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 configureOptions) + { + services.AddSingleton(serviceProvider => + { + var optionBuilder = new AspCoreMqttClientOptions(serviceProvider); + configureOptions(optionBuilder); + return optionBuilder.Build(); + }); + services.AddSingleton(); + services.AddSingleton(serviceProvider => + { + return serviceProvider.GetService(); + }); + services.AddSingleton(serviceProvider => + { + var mqttClientService = serviceProvider.GetService(); + var mqttClientServiceProvider = new MqttClientServiceProvider(mqttClientService); + return mqttClientServiceProvider; + }); + return services; + } + } +} diff --git a/Birdmap.API/Options/AspCoreMqttClientOptions.cs b/Birdmap.API/Options/AspCoreMqttClientOptions.cs new file mode 100644 index 0000000..02ae31e --- /dev/null +++ b/Birdmap.API/Options/AspCoreMqttClientOptions.cs @@ -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; + } + } +} diff --git a/Birdmap.API/Services/Hubs/DevicesHub.cs b/Birdmap.API/Services/Hubs/DevicesHub.cs new file mode 100644 index 0000000..76505a3 --- /dev/null +++ b/Birdmap.API/Services/Hubs/DevicesHub.cs @@ -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 + { + private readonly ILogger _logger; + + public DevicesHub(ILogger 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); + } + } +} diff --git a/Birdmap.API/Services/Hubs/IDevicesHubClient.cs b/Birdmap.API/Services/Hubs/IDevicesHubClient.cs new file mode 100644 index 0000000..58cb188 --- /dev/null +++ b/Birdmap.API/Services/Hubs/IDevicesHubClient.cs @@ -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); + } +} diff --git a/Birdmap.API/Services/IMqttClientService.cs b/Birdmap.API/Services/IMqttClientService.cs new file mode 100644 index 0000000..a8e4676 --- /dev/null +++ b/Birdmap.API/Services/IMqttClientService.cs @@ -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 + { + + } +} diff --git a/Birdmap.API/Services/Mqtt/MqttClientService.cs b/Birdmap.API/Services/Mqtt/MqttClientService.cs new file mode 100644 index 0000000..fcb1c87 --- /dev/null +++ b/Birdmap.API/Services/Mqtt/MqttClientService.cs @@ -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 _logger; + private readonly IInputService _inputService; + private readonly IHubContext _hubContext; + + public MqttClientService(IMqttClientOptions options, ILogger logger, IInputService inputService, IHubContext 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(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..."); + } + } + } +} diff --git a/Birdmap.API/Services/Mqtt/MqttClientServiceProvider.cs b/Birdmap.API/Services/Mqtt/MqttClientServiceProvider.cs new file mode 100644 index 0000000..a1f9aea --- /dev/null +++ b/Birdmap.API/Services/Mqtt/MqttClientServiceProvider.cs @@ -0,0 +1,12 @@ +namespace Birdmap.API.Services.Mqtt +{ + public class MqttClientServiceProvider + { + public IMqttClientService MqttClientService { get; } + + public MqttClientServiceProvider(IMqttClientService mqttClientService) + { + MqttClientService = mqttClientService; + } + } +} diff --git a/Birdmap.API/Startup.cs b/Birdmap.API/Startup.cs index c4f49aa..464c842 100644 --- a/Birdmap.API/Startup.cs +++ b/Birdmap.API/Startup.cs @@ -1,5 +1,7 @@ using AutoMapper; +using Birdmap.API.Extensions; using Birdmap.API.Middlewares; +using Birdmap.API.Services.Hubs; using Birdmap.BLL; using Birdmap.DAL; using Microsoft.AspNetCore.Authentication.JwtBearer; @@ -39,6 +41,8 @@ namespace Birdmap.API services.AddAutoMapper(typeof(Startup)); + services.AddSignalR(); + var key = Encoding.ASCII.GetBytes(Configuration["Secret"]); 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("Id"), + Username = mqttClient.GetValue("Username"), + Password = mqttClient.GetValue("Password"), + Topic = mqttClient.GetValue("Topic"), + }; + + var mqttBrokerHost = mqtt.GetSection("BrokerHostSettings"); + var brokerHostSettings = new + { + Host = mqttBrokerHost.GetValue("Host"), + Port = mqttBrokerHost.GetValue("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 services.AddSpaStaticFiles(configuration => { @@ -91,6 +122,7 @@ namespace Birdmap.API { endpoints.MapHealthChecks("/health"); endpoints.MapControllers(); + endpoints.MapHub("/hubs/devices"); }); app.UseSpa(spa => diff --git a/Birdmap.API/appsettings.json b/Birdmap.API/appsettings.json index fe9b2e1..c742aab 100644 --- a/Birdmap.API/appsettings.json +++ b/Birdmap.API/appsettings.json @@ -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" + } + } } diff --git a/Birdmap.API/nlog.config b/Birdmap.API/nlog.config index 3028859..ff4ba76 100644 --- a/Birdmap.API/nlog.config +++ b/Birdmap.API/nlog.config @@ -15,14 +15,14 @@ + layout="${longdate} [${threadname:whenEmpty=${threadid}}] ${uppercase:${level}} ${logger} - ${message} ${exception:format=tostring}" /> + layout="${longdate} [${threadname:whenEmpty=${threadid}}] ${uppercase:${level}} ${logger} - ${message} ${exception:format=tostring}" /> + layout="${longdate} [${threadname:whenEmpty=${threadid}}] ${uppercase:${level}} ${callsite} - ${message} ${exception:format=tostring} (url: ${aspnet-request-url})(action: ${aspnet-mvc-action})" /> @@ -31,7 +31,7 @@ - +