Compare commits

..

No commits in common. "master" and "feature/thesis" have entirely different histories.

33 changed files with 2968 additions and 3346 deletions

View File

@ -1,45 +0,0 @@
kind: pipeline
type: docker
name: default
steps:
- name: code-analysis
image: aosapps/drone-sonar-plugin
settings:
sonar_host:
from_secret: SONAR_HOST
sonar_token:
from_secret: SONAR_CODE
- name: kaniko
image: banzaicloud/drone-kaniko
settings:
registry: registry.kmlabz.com
repo: birbnetes/${DRONE_REPO_NAME}
username:
from_secret: DOCKER_USERNAME
password:
from_secret: DOCKER_PASSWORD
tags:
- latest
- ${DRONE_BUILD_NUMBER}
- name: dockerhub
image: plugins/docker
settings:
repo: birbnetes/${DRONE_REPO_NAME}
username:
from_secret: DOCKERHUB_USER
password:
from_secret: DOCKERHUB_PASSWORD
tags:
- latest
- ${DRONE_BUILD_NUMBER}
- name: ms-teams
image: kuperiu/drone-teams
settings:
webhook:
from_secret: TEAMS_WEBHOOK
when:
status: [ failure ]

View File

@ -2,6 +2,7 @@
using Birdmap.API.DTOs;
using Birdmap.BLL.Interfaces;
using Birdmap.BLL.Services.CommunicationServices.Hubs;
using Birdmap.BLL.Services.CommunicationServices.Mqtt;
using Birdmap.DAL.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
@ -24,16 +25,16 @@ namespace Birdmap.API.Controllers
{
private readonly IServiceService _service;
private readonly IMapper _mapper;
private readonly ICommunicationService _communicationService;
private readonly IMqttClientService _mqttClientService;
private readonly IHubContext<ServicesHub, IServicesHubClient> _hubContext;
private readonly ILogger<ServicesController> _logger;
public ServicesController(IServiceService service, IMapper mapper, ICommunicationServiceProvider communicationServiceProvider,
public ServicesController(IServiceService service, IMapper mapper, MqttClientServiceProvider mqttClientProvider,
IHubContext<ServicesHub, IServicesHubClient> hubContext, ILogger<ServicesController> logger)
{
_service = service;
_mapper = mapper;
_communicationService = communicationServiceProvider.Service;
_mqttClientService = mqttClientProvider.MqttClientService;
_hubContext = hubContext;
_logger = logger;
}
@ -82,11 +83,11 @@ namespace Birdmap.API.Controllers
Service = new()
{
Id = 0,
Name = "Message Queue Service",
Name = "Mqtt Client Service",
Uri = "localhost",
},
Response = $"IsConnected: {_communicationService.IsConnected}",
StatusCode = _communicationService.IsConnected ? HttpStatusCode.OK : HttpStatusCode.ServiceUnavailable,
Response = $"IsConnected: {_mqttClientService.IsConnected}",
StatusCode = _mqttClientService.IsConnected ? HttpStatusCode.OK : HttpStatusCode.ServiceUnavailable,
});
return serviceInfos.ToList();

View File

@ -12,7 +12,6 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
using NSwag.Generation.Processors.Security;
using System;
using System.Text;
namespace Birdmap.API
@ -72,7 +71,7 @@ namespace Birdmap.API
{
opt.Title = "Birdmap";
opt.OperationProcessors.Add(new OperationSecurityScopeProcessor("Jwt Token"));
opt.AddSecurity("Jwt Token", Array.Empty<string>(),
opt.AddSecurity("Jwt Token", new string[] { },
new NSwag.OpenApiSecurityScheme
{
Type = NSwag.OpenApiSecuritySchemeType.ApiKey,
@ -96,9 +95,11 @@ namespace Birdmap.API
app.UseOpenApi();
app.UseSwaggerUi3();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseAuthentication();
app.UseRouting();
app.UseAuthorization();

View File

@ -38,7 +38,6 @@
},
"UseDummyServices": true,
"ServicesBaseUrl": "https://birb.k8s.kmlabz.com/",
"UseRabbitMq": false,
"Mqtt": {
"BrokerHostSettings": {
"Host": "localhost",

View File

@ -24,28 +24,12 @@
},
"UseDummyServices": false,
"ServicesBaseUrl": "https://birb.k8s.kmlabz.com/",
"UseRabbitMq": false,
"Mqtt": {
"BrokerHostSettings": {
"VirtualHost": "",
"Host": "",
"Port": 1883
},
"ExchangeSettings": {
"Name": "",
"Type": "",
"Durable": false,
"AutoDelete": false
},
"QueueSettings": {
"Name": "",
"Durable": false,
"Exclusive": false,
"AutoDelete": false
},
"ClientSettings": {
"Id": "ASP.NET Core client",
"Username": "",

View File

@ -35,8 +35,6 @@
<!--Skip non-critical Mqtt logs-->
<logger name="*.*Mqtt*.*" minlevel="Trace" maxlevel="Warning" writeTo="mqttFile" final="true"/>
<logger name="*.*RabbitMq*.*" minlevel="Trace" maxlevel="Warning" writeTo="mqttFile" final="true"/>
<logger name="*.*CommunicationServiceBase*.*" minlevel="Trace" maxlevel="Warning" writeTo="mqttFile" final="true"/>
<!--Skip non-critical Hub logs-->
<logger name="*.*Hubs*.*" minlevel="Trace" maxlevel="Warning" writeTo="hubsFile" final="true"/>

View File

@ -9,7 +9,6 @@
<PackageReference Include="Microsoft.AspNetCore.SignalR.Core" Version="1.1.0" />
<PackageReference Include="MQTTnet" Version="3.0.13" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="RabbitMQ.Client" Version="6.2.1" />
</ItemGroup>
<ItemGroup>

View File

@ -1,9 +0,0 @@
using Microsoft.Extensions.Hosting;
namespace Birdmap.BLL.Interfaces
{
public interface ICommunicationService : IHostedService
{
public bool IsConnected { get; }
}
}

View File

@ -1,7 +0,0 @@
namespace Birdmap.BLL.Interfaces
{
public interface ICommunicationServiceProvider
{
public ICommunicationService Service { get; }
}
}

View File

@ -5,9 +5,11 @@ using MQTTnet.Client.Receiving;
namespace Birdmap.BLL.Interfaces
{
public interface IMqttClientService : IMqttClientConnectedHandler,
public interface IMqttClientService : IHostedService,
IMqttClientConnectedHandler,
IMqttClientDisconnectedHandler,
IMqttApplicationMessageReceivedHandler
{
public bool IsConnected { get; }
}
}

View File

@ -3,16 +3,16 @@ using System;
namespace Birdmap.BLL.Options
{
public class MqttClientOptions : MqttClientOptionsBuilder
public class AspCoreMqttClientOptions : MqttClientOptionsBuilder
{
public IServiceProvider ServiceProvider { get; }
public MqttClientOptions(IServiceProvider serviceProvider)
public AspCoreMqttClientOptions(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
}
public MqttClientOptions WithTopic(string topic)
public AspCoreMqttClientOptions WithTopic(string topic)
{
WithUserProperty("Topic", topic);

View File

@ -1,11 +0,0 @@
namespace Birdmap.BLL.Options
{
public record RabbitMqClientOptions(
string Hostname, int Port, string VirtualHost,
string Username, string Password,
string ExchangeName, string ExchangeType,
bool ExchangeDurable, bool ExchangeAutoDelete,
string QueueName,
bool QueueDurable, bool QueueAutoDelete, bool QueueExclusive,
string Topic);
}

View File

@ -1,92 +0,0 @@
using Birdmap.BLL.Interfaces;
using Birdmap.BLL.Services.CommunicationServices.Hubs;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Timer = System.Timers.Timer;
namespace Birdmap.BLL.Services.CommunationServices
{
internal class Payload
{
[JsonProperty("tag")]
public Guid TagID { get; set; }
[JsonProperty("probability")]
public double Probability { get; set; }
}
internal abstract class CommunicationServiceBase : ICommunicationService
{
protected readonly ILogger _logger;
protected readonly IInputService _inputService;
protected readonly IHubContext<DevicesHub, IDevicesHubClient> _hubContext;
private readonly Timer _hubTimer;
private readonly List<Message> _messages = new();
private readonly object _messageLock = new();
public abstract bool IsConnected { get; }
public CommunicationServiceBase(ILogger logger, IInputService inputService, IHubContext<DevicesHub, IDevicesHubClient> hubContext)
{
_logger = logger;
_inputService = inputService;
_hubContext = hubContext;
_hubTimer = new Timer()
{
AutoReset = true,
Interval = 1000,
};
_hubTimer.Elapsed += SendMqttMessagesWithSignalR;
}
protected async Task ProcessJsonMessageAsync(string json)
{
try
{
var payload = JsonConvert.DeserializeObject<Payload>(json);
var inputResponse = await _inputService.GetInputAsync(payload.TagID);
lock (_messageLock)
{
_messages.Add(new Message(inputResponse.Message.Device_id, inputResponse.Message.Date.UtcDateTime, payload.Probability));
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Could not handle application message.");
}
}
protected void StartMessageTimer()
{
_hubTimer.Start();
}
protected void StopMessageTimer()
{
_hubTimer.Stop();
}
private void SendMqttMessagesWithSignalR(object sender, System.Timers.ElapsedEventArgs e)
{
lock (_messageLock)
{
if (_messages.Any())
{
_logger.LogInformation($"Sending ({_messages.Count}) messages: {string.Join(" | ", _messages)}");
_hubContext.Clients.All.NotifyMessagesAsync(_messages);
_messages.Clear();
}
}
}
public abstract Task StartAsync(CancellationToken cancellationToken);
public abstract Task StopAsync(CancellationToken cancellationToken);
}
}

View File

@ -1,14 +0,0 @@
using Birdmap.BLL.Interfaces;
namespace Birdmap.BLL.Services.CommunicationServices
{
internal class CommunicationServiceProvider : ICommunicationServiceProvider
{
public ICommunicationService Service { get; }
public CommunicationServiceProvider(ICommunicationService service)
{
Service = service;
}
}
}

View File

@ -1,5 +1,4 @@
using Birdmap.BLL.Interfaces;
using Birdmap.BLL.Services.CommunationServices;
using Birdmap.BLL.Services.CommunicationServices.Hubs;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
@ -8,29 +7,59 @@ 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;
using Timer = System.Timers.Timer;
namespace Birdmap.BLL.Services.CommunicationServices.Mqtt
{
internal class MqttClientService : CommunicationServiceBase, IMqttClientService
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;
private readonly Timer _hubTimer;
private readonly List<Message> _messages = new();
private readonly object _messageLock = new();
public override bool IsConnected => _mqttClient.IsConnected;
public bool IsConnected => _mqttClient.IsConnected;
public MqttClientService(IMqttClientOptions options, ILogger<MqttClientService> logger, IInputService inputService, IHubContext<DevicesHub, IDevicesHubClient> hubContext)
: base(logger, inputService, hubContext)
{
_options = options;
_logger = logger;
_inputService = inputService;
_hubContext = hubContext;
_hubTimer = new Timer()
{
AutoReset = true,
Interval = 1000,
};
_hubTimer.Elapsed += SendMqttMessagesWithSignalR;
_mqttClient = new MqttFactory().CreateMqttClient();
ConfigureMqttClient();
}
private void SendMqttMessagesWithSignalR(object sender, System.Timers.ElapsedEventArgs e)
{
lock (_messageLock)
{
if (_messages.Any())
{
_logger.LogInformation($"Sending ({_messages.Count}) messages: {string.Join(" | ", _messages)}");
_hubContext.Clients.All.NotifyMessagesAsync(_messages);
_messages.Clear();
}
}
}
private void ConfigureMqttClient()
{
_mqttClient.ConnectedHandler = this;
@ -38,14 +67,36 @@ namespace Birdmap.BLL.Services.CommunicationServices.Mqtt
_mqttClient.ApplicationMessageReceivedHandler = this;
}
public Task HandleApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs eventArgs)
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.LogDebug($"Recieved [{eventArgs.ClientId}] " +
$"Topic: {eventArgs.ApplicationMessage.Topic} | Payload: {message} | QoS: {eventArgs.ApplicationMessage.QualityOfServiceLevel} | Retain: {eventArgs.ApplicationMessage.Retain}");
return ProcessJsonMessageAsync(message);
try
{
var payload = JsonConvert.DeserializeObject<Payload>(message);
var inputResponse = await _inputService.GetInputAsync(payload.TagID);
lock (_messageLock)
{
_messages.Add(new Message(inputResponse.Message.Device_id, inputResponse.Message.Date.UtcDateTime, payload.Probability));
}
}
catch (Exception ex)
{
_logger.LogError(ex, $"Could not handle application message.");
}
}
public async Task HandleConnectedAsync(MqttClientConnectedEventArgs eventArgs)
@ -56,7 +107,7 @@ namespace Birdmap.BLL.Services.CommunicationServices.Mqtt
_logger.LogInformation($"Connected. Auth result: {eventArgs.AuthenticateResult}. Subscribing to topic: {topic}");
await _mqttClient.SubscribeAsync(topic);
StartMessageTimer();
_hubTimer.Start();
}
catch (Exception ex)
{
@ -72,7 +123,7 @@ namespace Birdmap.BLL.Services.CommunicationServices.Mqtt
try
{
StopMessageTimer();
_hubTimer.Stop();
await _mqttClient.ConnectAsync(_options, CancellationToken.None);
}
catch (Exception ex)
@ -81,7 +132,7 @@ namespace Birdmap.BLL.Services.CommunicationServices.Mqtt
}
}
public override async Task StartAsync(CancellationToken cancellationToken)
public async Task StartAsync(CancellationToken cancellationToken)
{
try
{
@ -97,7 +148,7 @@ namespace Birdmap.BLL.Services.CommunicationServices.Mqtt
}
}
public override async Task StopAsync(CancellationToken cancellationToken)
public async Task StopAsync(CancellationToken cancellationToken)
{
try
{

View File

@ -0,0 +1,14 @@
using Birdmap.BLL.Interfaces;
namespace Birdmap.BLL.Services.CommunicationServices.Mqtt
{
public class MqttClientServiceProvider
{
public IMqttClientService MqttClientService { get; }
public MqttClientServiceProvider(IMqttClientService mqttClientService)
{
MqttClientService = mqttClientService;
}
}
}

View File

@ -1,114 +0,0 @@
using Birdmap.BLL.Interfaces;
using Birdmap.BLL.Options;
using Birdmap.BLL.Services.CommunicationServices.Hubs;
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Logging;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Birdmap.BLL.Services.CommunationServices.RabbitMq
{
internal class RabbitMqClientService : CommunicationServiceBase
{
private IConnection _connection;
private IModel _channel;
private readonly IConnectionFactory _factory;
private readonly RabbitMqClientOptions _options;
public override bool IsConnected => _connection.IsOpen;
public RabbitMqClientService(RabbitMqClientOptions options, ILogger<RabbitMqClientService> logger, IInputService inputService, IHubContext<DevicesHub, IDevicesHubClient> hubContext)
: base(logger, inputService, hubContext)
{
_options = options;
_factory = new ConnectionFactory()
{
HostName = options.Hostname,
Port = options.Port,
UserName = options.Username,
Password = options.Password,
AutomaticRecoveryEnabled = true,
};
}
private Task OnRecieved(object sender, BasicDeliverEventArgs eventArgs)
{
var props = eventArgs.BasicProperties;
var body = Encoding.UTF8.GetString(eventArgs.Body.ToArray());
_logger.LogDebug($"Recieved [{props.UserId}] " +
$"ConsumerTag: {eventArgs.ConsumerTag} | DeliveryTag: {eventArgs.DeliveryTag} | Payload: {body} | Priority: {props.Priority}");
return ProcessJsonMessageAsync(body);
}
public override async Task StartAsync(CancellationToken cancellationToken)
{
try
{
Connect();
}
catch (Exception ex)
{
_logger.LogError(ex, $"Cannot connect. Reconnecting...");
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
await StartAsync(cancellationToken);
}
}
public override Task StopAsync(CancellationToken cancellationToken)
{
try
{
StopMessageTimer();
_channel?.Close();
_connection?.Close();
return Task.CompletedTask;
}
catch (Exception ex)
{
_logger.LogError(ex, $"Cannot disconnect...");
return Task.FromException(ex);
}
}
private void Connect()
{
_connection = _factory.CreateConnection();
_channel = _connection.CreateModel();
_channel.ExchangeDeclare(
exchange: _options.ExchangeName,
type: _options.ExchangeType,
durable: _options.ExchangeDurable,
autoDelete: _options.ExchangeAutoDelete);
_channel.QueueDeclare(
queue: _options.QueueName,
durable: _options.QueueDurable,
exclusive: _options.QueueExclusive,
autoDelete: _options.QueueAutoDelete);
_channel.QueueBind(queue: _options.QueueName,
exchange: _options.ExchangeName,
routingKey: _options.Topic);
var consumer = new AsyncEventingBasicConsumer(_channel);
consumer.Received += OnRecieved;
_channel.BasicConsume(queue: _options.QueueName,
autoAck: true,
consumer: consumer);
StartMessageTimer();
}
}
}

View File

@ -1,8 +1,6 @@
using Birdmap.BLL.Interfaces;
using Birdmap.BLL.Options;
using Birdmap.BLL.Services;
using Birdmap.BLL.Services.CommunationServices.RabbitMq;
using Birdmap.BLL.Services.CommunicationServices;
using Birdmap.BLL.Services.CommunicationServices.Mqtt;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
@ -45,109 +43,54 @@ namespace Birdmap.BLL
services.AddSignalR();
var mqtt = configuration.GetSection("Mqtt");
var client = mqtt.GetSection("ClientSettings");
var clientSettings = new
{
Id = client.GetValue<string>("Id"),
Username = client.GetValue<string>("Username"),
Password = client.GetValue<string>("Password"),
Topic = client.GetValue<string>("Topic"),
};
var brokerHost = mqtt.GetSection("BrokerHostSettings");
var brokerHostSettings = new
{
Host = brokerHost.GetValue<string>("Host"),
Port = brokerHost.GetValue<int>("Port"),
VirtualHost = brokerHost.GetValue<string>("VirtualHost"),
};
var exchange = mqtt.GetSection("ExchangeSettings");
var exchangeSettings = new
{
Name = exchange.GetValue<string>("Name"),
Type = exchange.GetValue<string>("Type"),
Durable = exchange.GetValue<bool>("Durable"),
AutoDelete = exchange.GetValue<bool>("AutoDelete"),
};
var queue = mqtt.GetSection("QueueSettings");
var queueSettings = new
{
Name = queue.GetValue<string>("Name"),
Durable = exchange.GetValue<bool>("Durable"),
Exclusive = exchange.GetValue<bool>("Exclusive"),
AutoDelete = exchange.GetValue<bool>("AutoDelete"),
};
if (configuration.GetValue<bool>("UseRabbitMq"))
{
services.AddRabbitMqClientServiceWithConfig(new RabbitMqClientOptions(
Hostname: brokerHostSettings.Host,
Port: brokerHostSettings.Port,
VirtualHost: brokerHostSettings.VirtualHost,
Username: clientSettings.Username,
Password: clientSettings.Password,
ExchangeName: exchangeSettings.Name,
ExchangeType: exchangeSettings.Type,
ExchangeDurable: exchangeSettings.Durable,
ExchangeAutoDelete: exchangeSettings.AutoDelete,
QueueName: queueSettings.Name,
QueueDurable: queueSettings.Durable,
QueueExclusive: queueSettings.Exclusive,
QueueAutoDelete: queueSettings.AutoDelete,
Topic: clientSettings.Topic));
}
else
{
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);
});
}
return services;
}
private static IServiceCollection AddMqttClientServiceWithConfig(this IServiceCollection services, Action<MqttClientOptions> configureOptions)
private static IServiceCollection AddMqttClientServiceWithConfig(this IServiceCollection services, Action<AspCoreMqttClientOptions> configureOptions)
{
services.AddSingleton(serviceProvider =>
{
var optionBuilder = new MqttClientOptions(serviceProvider);
var optionBuilder = new AspCoreMqttClientOptions(serviceProvider);
configureOptions(optionBuilder);
return optionBuilder.Build();
});
services.AddClientServiceWithProvider<MqttClientService>();
return services;
}
private static IServiceCollection AddRabbitMqClientServiceWithConfig(this IServiceCollection services, RabbitMqClientOptions options)
{
services.AddSingleton(options);
services.AddClientServiceWithProvider<RabbitMqClientService>();
return services;
}
private static IServiceCollection AddClientServiceWithProvider<T>(this IServiceCollection services) where T : class, ICommunicationService
{
services.AddSingleton<T>();
services.AddSingleton<MqttClientService>();
services.AddSingleton<IHostedService>(serviceProvider =>
{
return serviceProvider.GetService<T>();
return serviceProvider.GetService<MqttClientService>();
});
services.AddSingleton<ICommunicationServiceProvider>(serviceProvider =>
services.AddSingleton(serviceProvider =>
{
var clientService = serviceProvider.GetService<T>();
var clientServiceProvider = new CommunicationServiceProvider(clientService);
return clientServiceProvider;
var mqttClientService = serviceProvider.GetService<MqttClientService>();
var mqttClientServiceProvider = new MqttClientServiceProvider(mqttClientService);
return mqttClientServiceProvider;
});
return services;
}

View File

@ -267,7 +267,7 @@
this.trackBar1.LargeChange = 500;
this.trackBar1.Location = new System.Drawing.Point(180, 162);
this.trackBar1.Maximum = 5050;
this.trackBar1.Minimum = 1;
this.trackBar1.Minimum = 50;
this.trackBar1.Name = "trackBar1";
this.trackBar1.Size = new System.Drawing.Size(247, 45);
this.trackBar1.SmallChange = 100;

View File

@ -17,7 +17,7 @@ services:
- ${APPDATA}/ASP.NET/Https:/root/.aspnet/https:ro
build:
context: .
dockerfile: Dockerfile
dockerfile: Birdmap.API/Dockerfile
depends_on:
- db
environment:
@ -38,18 +38,8 @@ services:
- Birdmap_Defaults__Services__KMLabz-Service=https://birb.k8s.kmlabz.com/devices
- Birdmap_UseDummyServices=true
- Birdmap_ServicesBaseUrl=https://birb.k8s.kmlabz.com/
- Birdmap_UseRabbitMq=false
- Birdmap_Mqtt__BrokerHostSettings__Host=localhost
- Birdmap_Mqtt__BrokerHostSettings__Port=1883
- Birdmap_Mqtt__BrokerHostSettings__VirtualHost=/
- Birdmap_Mqtt__ExchangeSettings__Name=birbmapexchange
- Birdmap_Mqtt__ExchangeSettings__Type=fanout
- Birdmap_Mqtt__ExchangeSettings__Durable=true
- Birdmap_Mqtt__ExchangeSettings__AutoDelete=true
- Birdmap_Mqtt__QueueSettings__Name=birbmapqueue
- Birdmap_Mqtt__QueueSettings__Durable=true
- Birdmap_Mqtt__QueueSettings__Exclusive=true
- Birdmap_Mqtt__QueueSettings__AutoDelete=true
- Birdmap_Mqtt__ClientSettings__Id=ASP.NET Core client
- Birdmap_Mqtt__ClientSettings__Username=username
- Birdmap_Mqtt__ClientSettings__Password=password

Binary file not shown.

View File

@ -9,10 +9,10 @@
\chapter*{Kivonat}\addcontentsline{toc}{chapter}{Kivonat}
Adott egy tanszéken fejlesztett felhő alapú elosztott rendszer, melynek eszközei madárhangok azonosítására képesek.
Ha a rendszer úgy észleli, hogy az egyik általa vezérelt eszköz mikrofonja felvételén madárhang található,
Ha a rendszer úgy észleli, hogy az egyik álatala vezérelt eszköz mikrofonja felvételén madárhang található,
akkor riasztást kezdeményez az eszközön ezzel elijesztve a madarat ezáltal megóvva a növényzetet.
A rendszernek több kisebb komponense van, amelyek rengeteg adatot dolgoznak fel és nincs jelenleg egy olyan egységes grafikus felület, ahol a rendszer teljes állapotát
A rendszernek több kisebb komponense van, amelyek rengeteg adatot dolgoznak fel és nincs jelenleg egy olyan egységes grafikus felület ahol a rendszer teljes állapotát
át lehetne tekinteni, ahol a feldolgozott adatokat vizualizálni lehetne.
A piacon létezik már több olyan szoftver csomag, amely hasonló problémákra próbál megoldást nyújtani, de ezek sem mindig
@ -20,7 +20,7 @@ tudják kielégíteni azokat a speciális igényeket, amelyek egy ilyen rendszer
Jelen szakdolgozat célja egy olyan vizualizációs megoldás bemutatása, amelynek segítségével a rendszer könnyedén áttekinthető
és kezelhető. A tanszéki rendszer által kezelt eszközök a felületen is vezérelhetők
és azok működéséről különböző statisztikákat felhasználva egyszerűen értelmezhető diagramok generálódnak.
és azok működéséről különböző statisztikákat felhasználva egyszerűen értelmezhető diagrammok generálódnak.
A backend megvalósítására az ASP.NET Core-t választottam, mely platformfüggetlen megoldást nyújt a web kérések kiszolgálására.
A frontend-et a React.js használatával készítettem, mely segítségével egyszerűen és gyorsan lehet reszponzív felhasználói felületeket készíteni.

View File

@ -10,8 +10,8 @@ Ebben a fejezetben bemutatom a szerveroldal architektúráját, felépítését.
A szerveroldal fejlesztésénél a háromrétegú architektúrát alkalmaztam, melynek lényege, hogy az alkalmazást logikailag három elkülönülő részre bontjuk:
\begin{itemize}
\item \textbf{Adat elérési réteg}. Ez a rész felel a tárolt entitások modell definícióiért, illetve azoknak a kiolvasásáért, tárolásáért egy adatbázisból vagy fájlrendszerből.
\item \textbf{Megjelenítési réteg}. Ezen réteg feladata a kliensoldal közvetlen kiszolgálása. Bármilyen irányú kommunikáció a kliensek felé ezen a rétegen keresztül történik.
\item \textbf{Üzleti logikai réteg}. Minden, ami nem a közvetlen kommunikációért, megjelenítésért vagy adat elérésért, tárolásért felel, az ide kerül.
\item \textbf{Megjelenítési réteg}. Ezen réteg feladata a kliensoldal közvetlek kiszolgálása. Bármilyen irányú kommunikáció a kliensek felé ezen a rétegen keresztül történik.
\item \textbf{Üzleti logikai réteg}. Minden ami nem a közvetlen kommunikációért, megjelenítésért vagy adat elérésért, tárolásért felel, az ide kerül.
A fenti két réteg között helyezkedik el és feladata a különböző folyamatok értékelése és futtatása, valamint az adatok feldolgozása.
\end{itemize}
@ -79,15 +79,15 @@ melyeknek szintén van egy modellje \verb+Sensor+ néven. Ennek szintén van azo
amely a hangüzenetek metaadatait reprezentálja. Többek között tartalmazza a kihelyezett eszköz egyedi azonosítóját és a hangüzenet keltének dátumát.
Ugyan itt található meg a \verb+User+ és \verb+Service+ entitások létrehozásáért, olvasásáért, szerkesztéséért és törléséért felelős szolgáltatások is.
Valamint itt található még az autentikációért felelős szolgáltatás is. A felhasználók jelszavainak tárolására a HMAC (Hash-based Message Authentication Code) algoritmust,
Valamint itt található még az autentikációért felelős szolgáltatás is. A felhasználók jelszavainak tárolására a HMAC (Hash-based Message Authentication Code) algorithmust,
pontosabban annak a \verb+HMACSHA512+ \cite{hmacsha512} C\# implementációját használtam.
Minden jelszóhoz generálok egy egyedi kulcsot és azzal egy hash-t, majd ezeket tárolom a \verb+User+ modell \verb+PasswordSalt+ és \verb+PasswordHash+ mezőiben.
Amikor egy felhasználó be akar jelentkezni először megvizsgálom, hogy egyáltalán létezik-e az adatbázisban az adott nevű felhasználó,
ha igen, akkor a megadott jelszóból az imént említett folyamattal generált kulcsot és hash-t összehasonlítom az adatbázisban tárolttal.
Azért hasznos ily módon, és nem mondjuk egyszerű szöveges formában tárolni a felhasználók jelszavát, mert így a felhasználón kívül senki sem tudja, hogy mi volt az eredeti jelszava,
az algoritmus egyirányú volta miatt\footnotemark. Ha véletlenül rossz kezekbe kerülne az adatbázis tartalma, akkor sem fognak tudni bejeletkezni a felhasználók adataival.
Azért hasznos íly módon, és nem mondjuk egyszerű szöveges formában tárolni a felhasználók jelszavát, mert így a felhasználón kívül senki sem tudja, hogy mi volt az eredeti jelszava,
az algorithmus egyirányú volta miatt\footnotemark. Ha véletlenül rossz kezekbe kerülne az adatbázis tartalma, akkor sem fognak tudni bejeletkezni a felhasználók adataival.
\footnotetext{Generálni egyszerű és gyors. Visszafejteni közel lehetetlen.}
%----------------------------------------------------------------------------
@ -100,10 +100,10 @@ frissüljön a felület.
Egy másik szerveroldalon használt szolgáltatás a Birbnetes MQTT kommunikációért felelős szolgáltatás,
mely felregisztrál a \ref{subsect:birdnetes-ai-service}-as alfejezetben bemutatott AI Service által publikált üzenetekre.
Ezekben az üzenetekben található a hanganyagok egyedi azonosítója, illetve azok seregélytől való származásának valószínűsége.
Ezekben az üzenetekben található a hanganyagok egyedi azonosítója, illetve azok seregélytől való származásának valószínüsége.
Ha a szolgáltatás kap egy ilyen üzenetet akkor lekérdezi a \ref{subsect:birdnetes-input-service}-es alfejezetben bemutatott Input Service-től
a hanganyag azonosítójához tartozó metaadatokat.
Ezekből felhasználva a kihelyezett eszköz azonosítóját, a hanganyag beérkezésének dátumát és az említett valószínűséget új üzenetek készülnek, melyeket egy pufferben tárolódnak.
Ezekből felhasználva a kihelyezett eszköz azonosítóját, a hanganyag beérkezésének dátumát és az említett valószínüséget új üzenetek készülnek, melyeket egy pufferben tárolódnak.
Ezt a folyamatot a \ref{fig:birdmap-mqtt-service}-es ábra szemlélteti.
\begin{figure}[!ht]
@ -114,7 +114,7 @@ Ezt a folyamatot a \ref{fig:birdmap-mqtt-service}-es ábra szemlélteti.
\end{figure}
A puffer tartalmát másodperces gyakorisággal elküldöm a klienseknek a SignalR segítségével.
Azért van szükség a puffer használatára, mert az MQTT-n érkezett üzenetek gyakorisága akár milliszekundum nagyságrendű is lehet.
Azért van szükség a puffer használatára, mert az MQTT-n érkezett üzenetek gyakorisága akár miliszekundum nagyságrendű is lehet.
Míg a szerver képes is az üzeneteket feldolgozni, ha ezeket rögtön tovább küldeném a kliensek felé, azok nem biztos, hogy képesek lennének rá.
%----------------------------------------------------------------------------
@ -125,8 +125,8 @@ Itt történik a \ref{subsect:seeding} fejezetben leírt adatbázis seedelése i
Többek között a naplózás is itt kerül inicializálásra, mely az NLog saját konfigurációs fájljával történik.
Meg lehet adni különböző szűrőket és kimeneteket, amellyel szelektálni lehet, hogy az egyes naplózott események hova kerüljenek.
Például az MQTT szolgáltatás napló bejegyzéseit a \ref{lst:nlog-config} lista alapján szűrtem.
Minden \verb+Debug+ szinttől nagyobb és \verb+Error+ szinttől kisebb bejegyzés, mely tartalmazza az \verb+Mqtt+ kulcsszót az \verb+mqttFile+ azonosítójú fájlba kerül.
Például az MQTT szolgáltalás napló bejegyzéseit a \ref{lst:nlog-config} lista alapján szűrtem.
Minden \verb+Debug+ szintől nagyobb és \verb+Error+ szinttől kisebb bejegyzés, mely tartalmazza az \verb+Mqtt+ kulcsszót az \verb+mqttFile+ azonosítójú fájlba kerül.
\begin{lstlisting}[style=xml, caption=Az NLog.config fájl egy részlete, label=lst:nlog-config]
<targets>
@ -184,14 +184,14 @@ A kontrollerek határozzák meg, hogy a szerveroldalon milyen végpontokat, mily
}
\end{lstlisting}
A jogosultságok kezelését a JSON Web Token-ekkel oldottam meg. A felhasználó bejelentkezéskor kap egy ilyen token-t,
A jogosultságok kezelését a JSON Web Token-ekkel oldottam meg. A fejlasználó bejelentkezéskor kap egy ilyen token-t,
amelyben tárolom a hozzá tartozó szerepet. A \ref{lst:devices-controller}-as listában látszik, hogy hogyan használom ezeket a szerepeket.
A \verb+DevicesController+ végpontjait alapértelmezetten \verb+User+ és \verb+Admin+ jogosultságú felhasználó hívhatja, az "api/devices/online" végpontot azonban csak \verb+Admin+ jogosultságú.
Hasonlóképpen oldottam meg ezt a többi kontrollernél is. A \verb+User+ felhasználók csak olyan végpontokat hívhat, mely kizárólag az állapotok olvasásával jár.
Hasonló képpen oldottam meg ezt a többi kontrollernél is. A \verb+User+ felhasználók csak olyan végpontokat hívhat, mely kizárolag az állapotok olvasásával jár.
Az \verb+Admin+ felhasználók hívhatnak bármilyen végpontot.
A szerveroldalon négy különböző kontroller található, melyek mindegyikének alapvető feladata az üzleti logikát megvalósító szolgáltatások használata, a működés naplózás,
illetve az imént említett végpontok autorizálása és kiszolgálása. Ezeken kívül a kontrollerek speciális feladata a következő:
illetve az imént említett végpontok authorizálása és kiszolgálása. Ezeken kívül a kontrollerek speciális feladata a következő:
\begin{itemize}
\item Az \textbf{AuthController} felel a felhasználók bejelentkezésének lebonyolításáért, a JSON Web Token elkészítéséért. Az \verb+[Authorize]+ helyett itt az \verb+[AllowAnonymous]+ attribútum van használva, mellyel azt lehet jelezni, hogy a végpont bejelentkezés nélkül is hívható.
\item A \textbf{ServiceController} felel az alkalmazás által használt külső szolgáltatások állapotának lekérdezhetőségéért. Ilyenek például a Birbnetes rendszer vagy az MQTT szolgáltatás állapota.
@ -200,7 +200,7 @@ illetve az imént említett végpontok autorizálása és kiszolgálása. Ezeken
\end{itemize}
Az adatbázisból érkező adatok gyakran túl sok vagy túl kevés információt tartalmaznak ahhoz, hogy kiolvasás után rögtön elküldjem a kliensoldalnak.
Például amikor a felhasználó bejelentkezik a kiolvasott \verb+User+ objektum tartalmazza annak jelszavát (hash-elt formában), viszont nem tartalmazza az autorizációhoz használt token adatait.
Például amikor a felhasználó bejelentkezik a kiolvasott \verb+User+ objektum tartalmazza annak jelszavát (hash-elt formában), viszont nem tartalmazza az authorizációhoz használt token adatait.
Ennek a megoldására adatátviteli objektumokat hoztam létre, melyek csak azokat a mezőket tartalmazzák amelyekre a felhasználónak szüksége van.
Az adatbázisból kiolvasott objektum hasznos részeit és egyéb használni kívánt információt átmásolom az átviteli objektumba. Majd ezt küldöm el a kliensoldal felé.

View File

@ -9,7 +9,7 @@ Ebben a fejezetben bemutatom a kliensoldal architektúráját. Ismertetem a kül
%----------------------------------------------------------------------------
Az alkalmazásnak minden oldala egy külön React komponens, mely mindegyikének saját mappája van a főkönyvtár alatt,
ahol az egyes oldalak által használt szolgáltatások és egyéb komponensek találhatóak.
A közösen használt szolgáltatások és komponensek a common mappába kerültek.
A közöses használt szolgáltatások és komponensek a common mappába kerültek.
A kliensoldal belépési pontja az \verb+App.js+ fájlban található \verb+App+ komponens.
Itt egy React \verb+Switch+-ben fel van sorolva az összes oldal komponense azok elérési útvonalai szerint.
@ -40,10 +40,10 @@ Paraméterében át lehet adni egy másik megjeleníteni kívánt komponenst, me
Mivel minden komponens ebbe az bázis komponensbe van csomagolva, így akárhova navigálunk az oldalon a felület mindig egységes marad.
A másik komponens a \verb+PredicateRoute+, melynek paraméterében meg lehet adni egy feltételt, illetve egy másik komponenst.
Ha a feltétel hamis akkor átirányítja a felhasználót a bejelentkező oldalra, ha igaz akkor megjeleníti a \verb+DefaultLayout+-ba csomagolt komponenst.
Ha a feltétel hamis akkor átírányítja a felhasználót a bejelentkező oldalra, ha igaz akkor megjeleníti a \verb+DefaultLayout+-ba csomagolt komponenst.
Publikus oldalnál a feltétel mindig igaz.
Privátnál a feltétel a bejelentkezéshez van kötve.
Az admin oldal feltétele egyrészt szintén a bejelentkezés, másrészt a felhasználó \verb+Admin+ jogosultsága.
Az admin oldal feltétele egyrészt szintén a bejelentkezés, másrészt a felhasználó \verb+Admin+ jogolsultsága.
Ezt a folyamatot próbálja szemléltetni a \ref{fig:birdmap-frontend-architecture}-es ábra.
Legfelül sárgával vannak feltüntetve a hívható végpontok, alattuk a hozzájuk kapcsolt megjelenítendő komponensek, azok alatt pedig a hozzáférést szabályozó komponensek.
@ -59,13 +59,13 @@ Legfelül sárgával vannak feltüntetve a hívható végpontok, alattuk a hozz
%----------------------------------------------------------------------------
A szerveroldallal való kommunikációt rendkívül egyszerűen tudtam implementálni köszönhetően a \ref{subsect:backend-swagger}-as fejezetben bemutatott Swagger oldalnak
és annak, hogy az NSwag Studio-val \cite{nswag-studio} a C\#-on kívül lehet TypeScript\footnotemark klienseket is generálni a leíró fájlból.
Így készültek el a komponensek kommunikációért felelős szolgáltatásai.
Így készültek el a kommponensek kommunikációért felelős szolgáltatásai.
\footnotetext{JavaScript-re épített statikus típusdefiníciókat tartalmazó nyelv. JavaScript és TypeScript együtt is használható.}
%----------------------------------------------------------------------------
\section{Komponensek}
%----------------------------------------------------------------------------
Ebben a szakaszban ismertetem az egyes oldalak komponenseit és azok alkomponenseit,
Ebben a szakaszban ismertete az egyes oldalak komponenseit és azok alkomponenseit,
illetve a navigációért felelős fejlécet.
%----------------------------------------------------------------------------
\subsection{Navigáció}
@ -100,7 +100,7 @@ A generált szerverrel kommunikáló szolgáltatás be van csomagolva egy közö
Ennek célja, hogy a bejelentkezés eredményét több komponens is olvashassa, hiszen az alkalmazás felületét alapvetően megkülönbözteti,
egyrészt a bejelentkezés sikeressége, másrészt a bejelentkezett felhasználó jogosultsági köre.
Sikeres bejelentkezés után a szerver elküldi a felhasználó szerepét, illetve a hozzáférési token-t, amelyre a kliens többi szolgáltatásának is szüksége lesz a kommunikációhoz.
Sikeres bejelentkezés után a szerver elküldi a felhasználó szerepét, illetve a hozzáférési token-t, amelyre a kliens többi szolgáltatásának is szüksége lesz a kommunkációhoz.
Ezeket az oldal \verb+sessionStorage+-ában\footnotemark tárolom és a becsomagolt szolgáltatáson keresztül elérhetőek.
Kijelentkezni a navigációs fejlécben található profil ikonra való kattintással lehet.
@ -120,7 +120,7 @@ Komponense a \ref{fig:birdmap-logs}-es ábrán látható.
\label{fig:birdmap-logs}
\end{figure}
%----------------------------------------------------------------------------
\subsection{Eszközállapot- és hangüzenet-kezelő szolgáltatás}
\subsection{Eszköz állapot és hangüzenet kezelő szolgáltatás}
%----------------------------------------------------------------------------
A szakasz további komponenseinek van egy közös ismertetője. Mégpedig, hogy mindegyiknek szüksége van a kihelyezett eszközök adataira
és az azok által publikált hangüzenetekből képzett valószínűségre.
@ -133,13 +133,13 @@ A \ref{lst:react-switch}-es listában látható, hogy a \verb+DevicesContextProv
\subsection{Dashboard}
%----------------------------------------------------------------------------
A Dashboard az alkalmazás kezdő oldala. Itt található meg a külső szolgáltatások állapotát vizsgáló komponens,
illetve a kihelyezett eszközök működési folyamatában áttekintést nyújtó diagramok mindegyike.
illetve a kihelyezett eszközök működési folyamatában áttekintést nyújtó diagrammok mindegyike.
Az oldal megjelenítésekor elindul egy másodpercenként ismétlődő folyamat,
mely a \verb+DevicesContext+-ből kiolvasott értékekből legenerálja a diagramokon megjelenítendő összes adatot.
mely a \verb+DevicesContext+-ből kiolvasott értékekből legenerálja a diagrammokon megjelenítendő összes adatot.
Ez azonban az adat mennyiségétől függően akár egy-két másodpercig is eltarthat, ami rendkívül lassúvá és használhatatlanná tenné a felületet.
Ennek elkerülése érdekében az adatfeldolgozó folyamat egyszerre csak egy pár elemet dolgoz fel, mely alfolyamatok között 20 milliszekundum szüneteket iktattam be.
Továbbá hogy a különböző diagramok animációi is zökkenőmentesek legyenek, azok adatai cserélése között is van 300 milliszekundum szünet.
Továbbá hogy a különböző diagrammok animációi is zökkenőmentesek legyenek, azok adatai cserélése között is van 300 milliszekundum szünet.
Így valamivel lasabb az adatfeldolgozás, de a felület használható marad.
%----------------------------------------------------------------------------
\subsubsection{Külső szolgáltatások}
@ -176,38 +176,38 @@ A felhasználói élmény maximalizálása érdekében a frissítés előtt lek
\subsubsection{Eszközök és szenzorok állapota}
%----------------------------------------------------------------------------
Ennek a komponensnek a szerepe, hogy áttekintést nyújtson az eszközök és szenzorok állapotáról.
Úgy gondoltam, hogy erre a legcélravezetőbb eszköz a \ref{fig:dashboard-donut}-es ábrán is látható Apexcharts fánk diagramja.
Úgy gondoltam, hogy erre a legcélravezetőbb eszköz a \ref{fig:dashboard-donut}-es ábrán is látható Apexcharts fánk diagrammja.
Látható, hogy hány darab eszköz és szenzor van bekapcsolt, kikapcsolt, illetve hibás állapotban.
Az állapotok változása esetén a \verb+DevicesContextProvider+-nek köszönhetően az adatok automatikusan frissülnek.
\begin{figure}[!ht]
\centering
\includegraphics[width=150mm, keepaspectratio]{figures/dashboard-donut-devices.png}
\caption{A Dashboard eszköz- és szenzor állapotok diagramja}
\caption{A Dashboard eszköz- és szenzor állapotok diagrammja}
\label{fig:dashboard-donut}
\end{figure}
%----------------------------------------------------------------------------
\subsubsection{Hőtérkép diagramok}
\subsubsection{Hőtérkép diagrammok}
%----------------------------------------------------------------------------
Ezekkel a diagramokkal az a célom, hogy az eszközök által küldött észleléseket időrendben vizualizáljam.
Megvalósításukhoz az Apexcharts Heatmap típusú diagramját használtam.
A \ref{fig:dashboard-heatmap-second}-as ábrán látható diagram az elmúlt egy percben küldött, másodpercenként a legnagyobb, hangüzenetekből képzett valószínűségeket ábrázolja.
Ezekkel a diagrammokkal az a célom, hogy az eszközök által küldött észleléseket időrendben vizualizáljam.
Megvalósításukhoz az Apexcharts Heatmap típusú diagrammját használtam.
A \ref{fig:dashboard-heatmap-second}-as ábrán látható diagram az elmúlt egy percben küldött, másodpercenként a legnagyobb, hangüzenetekből képzett valószínűségeket ábrozolja.
A \ref{fig:dashboard-heatmap-minute}-es ábrán látható diagram pedig az elmúlt egy órában percenként a legnagyobbakat.
\begin{figure}[!ht]
\centering
\includegraphics[width=150mm, keepaspectratio]{figures/second-heatmap.png}
\caption{Másodperc alapú hőtérképes diagram}
\caption{Másodperc alapú hőtérképes diagramm}
\label{fig:dashboard-heatmap-second}
\end{figure}
\begin{figure}[!ht]
\centering
\includegraphics[width=150mm, keepaspectratio]{figures/minute-heatmap.png}
\caption{Perc alapú hőtérképes diagram}
\caption{Perc alapú hőtérképes diagramm}
\label{fig:dashboard-heatmap-minute}
\end{figure}
A függőleges tengelyen a rendszer eszközei vannak dinamikusan megjelenítve.
A vízszintes tengelyen pedig az említett időtartományok.
A diagramokon látható négyzetek a valószínűség nagyságától függően sötétebbek vagy világosabbak.
A diagrammokon látható négyzetek a valószínűség nagyságától függően sötétebbek vagy világosabbak.
\newpage
%----------------------------------------------------------------------------
\subsubsection{Riasztás számláló}
@ -217,7 +217,7 @@ Segítségével megvizsgálható, hogy mely eszközök riasztanak a legtöbbet a
\begin{figure}[!ht]
\centering
\includegraphics[width=150mm, keepaspectratio]{figures/dashboard-column-devices.png}
\caption{Eszközönkénti riasztásokat számláló diagram}
\caption{Eszközönkénti riasztásokat számláló diagramm}
\label{fig:dashboard-devices-column}
\end{figure}
@ -226,12 +226,12 @@ Az egyes oszlopok három részre vannak bontva az üzenetek öt tized, hét tize
%----------------------------------------------------------------------------
\subsubsection{Üzenetek gyakorisága}
%----------------------------------------------------------------------------
Az oldalon található utolsó diagram egy vonal diagram, melynek célja, hogy ábrázolja a rendszer által küldött üzenetek számát másodpercenként.
Az oldalon található utolsó diagramm egy vonal diagammn, melynek célja, hogy ábrázolja a rendszer által küldött üzenetek számát másodpercenként.
A \ref{fig:dashboard-messages-line}-es ábrán látható a komponens.
A vízszintes tengelyen a legelső érték az alkalmazás által először észlelt üzenet időpontja.
Az utolsó érték a legutoljára észlelt időpontja.
A függőleges tengelyen az adott másodpercben érkező üzenetek száma van ábrázolva.
Az előzőkkel ellentétben itt az adatok nincsennek szűrve a hangüzenet valószínűsége alapján,
Az előzőekkel ellentétben itt az adatok nincsennek szűrve a hangüzenet valószínűsége alapján,
tehát a rendszer által küldött összes üzenet látható.
\begin{figure}[!ht]
\centering
@ -243,11 +243,11 @@ tehát a rendszer által küldött összes üzenet látható.
%----------------------------------------------------------------------------
\subsection{Devices}
%----------------------------------------------------------------------------
Ez az oldal lehetővé teszi a felhasználók számára az eszközök állapotának áttekintését, \verb+Admin+ felhasználók számára azok menedzselését is.
Ez az oldal lehetővé teszi a felhasználók számára az eszközök állapotának áttekintését, \verb+Admin+ felhasználók számára azok menedszelését is.
Az eszközök dinamikusan jelennek meg a \verb+DevicesContextProvider+ adatai alapján, melyek megjelenítésére a Material UI \verb+Accrordion+ komponensét használom.
Ennek fejlécében az eszköz neve, egyedi azonosítója és státusza található. A lenyíló részben pedig az eszköz által használt szenzorok neve, azonosítója és státusza.
\verb+Admin+ felhasználók számára a felület két fajta gombbal bővül, melyekkel be és ki lehet kapcsolni az egyes eszközöket, szenzorokat.
Az \verb+Accordion+-ok felett található egy külön panel, mellyel egyszerre lehet kezelni az összes eszközt és azok szenzorjait.
\verb+Admin+ felhasználók számára a felület két fajta gombbal bővül, mellyekkel be és ki lehet kapcsolni az egyes eszközöket, szenzorokat.
Az \verb+Accordion+-ok felett található egy külön panel, mellyel egyszerre lehet kezelni az összes eszközt és azok szenzorait.
A Devices oldal felülete a \ref{fig:frontend-devices}-es ábrán,
az \verb+Admin+ felhasználók számára nyújtott plusz funkciók a \ref{fig:frontend-devices-admin}-as ábrán láthatók.
\begin{figure}[!ht]
@ -272,14 +272,14 @@ Ezt használva megjelenítem a rendszer összes eszközét azok koordinátái sz
A kék színű ikonok jelölik a bekapcsolt állapotban lévő, a sárga a kikapcsolt állapotban lévő,
a piros pedig a hibás állapotban lévő eszközöket.
Ha a felhasználó az egerét az ikonok fölé helyezi, megjelenik egy szövegdoboz, melyben az eszköz azonosítója és státusza látható.
Az ikonra kattintva a felhasználó a Devices oldalra kerül, ahol megnyílik a kattintott eszköz \verb+Accordion+-ja.
Az ikonra kattinta a felhasználó a Devices oldalra kerül, ahol megnyílik a kattintott eszköz \verb+Accordion+-ja.
A \verb+DevicesContext+ tartalmazza az eszközök által küldött üzenetek adatait,
melyeknek a 0.5 valószínűségtől nagyobb részhalmazát a hőtérkép által kezelhető adatokká konvertálok.
Egyrészt szükség van az előbb is említett földrajzi koordinátákra, melyeket az üzenetek eszköz azonosítója alapján határozok meg.
Másrészt szükség van egy súly értékre, mely a pont színezésének pirosságát határozza meg.
Ezt az értéket az üzenetek valószínűség értékével tettem egyenlővé.
Minél több magasabb valószínűségű riasztás érkezik egy adott eszköztől, a körülötte lévő terület annál pirosabb lesz.
Minnél több magasabb valószínűségű riasztás érkezik egy adott eszköztől, a körülötte lévő terület annál pirosabb lesz.
A \ref{fig:frontend-heatmap}-ös ábra mutatja a térkép működését miközben 4 eszköz is seregélyeket észelt.

View File

@ -13,7 +13,7 @@ A jellemző adatvizualizációs megoldások közül az alábbi hármat találtam
\begin{itemize}
\item \textbf{Hőtérkép}. Hasznos lenne egy olyan felület, ahol az eszközök GPS koordinátái és a seregély detektálást jelző üzenetek alapján, meg lehetne jeleníteni a seregélyek hozzávetőleges előfordulásának helyeit és gyakoriságát egy térképen, hőtérképes formában.
\item \textbf{Eszköz állapotok}. Jelenleg a Command and Control mikroszolgáltatás felé indított kéréseken kívül, nincs lehetőség a kihelyezett eszközök állapotának vizsgálatára. Szükség lenne egy olyan felületre, ahol ezek állapotai láthatóak, esetleg dinamikusan is frissülnek.
\item \textbf{Diagramok}. A hőtérképen kívül egyéb olyan diagramok is hasznosak lehetnek, ahol látható például, hogy melyik eszköz melyik percben észlelt madárhangot vagy, hogy egy eszköz összesen hány madárhangot észlelt. Minél több információ, annál jobb.
\item \textbf{Diagrammok}. A hőtérképen kívül egyéb olyan diagrammok is hasznosak lehetnek, ahol látható például, hogy melyik eszköz melyik percben észlelt madárhangot vagy, hogy egy eszköz összesen hány madárhangot észelt. Minnél több információ, annál jobb.
\end{itemize}
Ezeken kívül fontos követelmény volt még, hogy az alkalmazásom futtatható legyen Linux környezetben is, hogy az telepíthető legyen a Birbnetes Kubernetes \cite{kubernetes} klaszterébe.
@ -28,7 +28,7 @@ Az imént vázolt igények kielégítésére sok, széles körben alkalmazott me
\subsection{Grafana}
%----------------------------------------------------------------------------
A Grafana \cite{grafana} az egy nyílt forráskódú platformfüggetlen vizualizációs web alkalmazás.
Egy támogatott adatbázishoz csatlakoztatva különféle interaktív gráfokat és diagramokat generál.
Egy támogatott adatbázishoz csatlakoztatva különféle interaktív gráfokat és diagrammokat generál.
A testreszabhatóság maximalizásának érdekében különböző, akár harmadik fél által készített, bővítmények használatát is támogatja,
melyekkel új adatforrások és panel típusok integrálhatók.
A \ref{fig:grafana}-es ábra egy jó példa arra, hogy hogyan néz ki egy általános Grafana felület.

View File

@ -1,12 +1,12 @@
%----------------------------------------------------------------------------
\chapter{Docker image készítés}
\label{chapt:birdmap-kubernetes}
\label{chapt:birdnetes-kubernetes}
%----------------------------------------------------------------------------
Az éles rendszerrel való kommunikáció megvalósításához készítenem kell egy Docker image-et, melyet telepíteni lehet a Birbnetes Kubernetes klaszterébe.
Ehhez először készítettem egy Dockerfile-t \cite{dockerfile}, mely az image-ek automatikus elkészítését teszi lehetővé.
Utasításokat lehet benne felsorolni, melyekkel a konténer környezetét kell felépíteni.
Meg lehet adni kiindulópontokat, mely az image alapjául szolgál.
Erre a célra én az ASP.NET futtatókörnyeztét használtam, mely tartalmazza az alkalmazás futtatásához szükséges parancsokat.
Erre a célra én az ASP.NET futtatokörnyeztét használtam, mely tartalmazza az alkalmazás futtatásához szükséges parancsokat.
Ezek után a Dockerfile utasításait használva bemásolom a \verb+Release+ konfigurációval fordított alkalmazásomat a konténer egy mappájába,
majd a belépési pont utasítással megadom az alkalmazás indításához szükséges parancsot.
Ezt futtatva sikeresen elkészül a Docker image.

View File

@ -38,7 +38,7 @@ A Visual Studio \cite{vs} a Microsoft fejlesztőkörnyezete. Jól alkalmazható
%----------------------------------------------------------------------------
\subsection{Visual Studio Code}
%----------------------------------------------------------------------------
Egy másik Microsoft termék, viszont a fentivel ellentétben a Visual Studio Code \cite{vs-code} inkább szövegszerkesztő, mint fejlesztőkörnyezet.
Egy másik Microsoft termék, viszont a fentivel ellentétben a Visual Studio Code \cite{vs-code} inkább szövegszerkeztő, mint fejlesztőkörnyezet.
Ennek köszönhetően jelentősen gyorsabb és egyszerűbb a használata. Különféle bővítmények használatával nagyon jó program nyelv támogatottságot lehet elérni.
Többek között ezen okok miatt preferáltam a kliensoldal fejlesztésére.
@ -53,20 +53,20 @@ amivel már foglalkoztam korábban, amivel gyorsabban és rutinosabban megy a fe
Másrészt nemrég jelent meg a .NET új 5-ös verziója, melynek használatával jelentős teljesítmény javulást ígértek több területen is, és úgy gondoltam, hogy ez a projekt tökéletes lenne
ennek próbatételére.
Mindemellett a .NET teljesen platformfüggetlen, mely az egyik legfontosabb követelmény volt az alkalmazással szemben.
Mindemellett a .NET teljesen platformüggetlen, mely az egyik legfontosabb követelmény volt az alkalmazással szemben.
%----------------------------------------------------------------------------
\subsection{ASP.NET Core}
%----------------------------------------------------------------------------
Az ASP.NET Core a .NET család ingyenes, nyílt forráskódú webes keretrendszere. Gyors és moduláris fejlesztést tesz lehetővé,
mely főként a csomagkezelő rendszerének, a NuGet-nek \cite{nuget} köszönhető.
Használatának egyik előnye, hogy ugyan az a C\# kód tud futni a szerver és a kliens oldalon, de támogat más kliens oldali keretrendszereket is, mint például az Angular-t, a Vue.js-t
Használatána egyik előnye, hogy ugyan az a C\# kód tud futni a szerver éa a kliens oldalon, de támogat más kliens oldali keretrendszereket is, mint például az Angular-t, a Vue.js-t
vagy a React.js-t.
%----------------------------------------------------------------------------
\subsection{Entity Framework Core}
%----------------------------------------------------------------------------
Az Entity Framework Core (röviden EF Core) egy objektum-relációs leképző keretrendszer a .NET-hez. Az adatbázissal való kommunikációt könnyítését szolgálja.
Az Entity Framework Core (röviden EF Core) egy objektum-relációs leképező keretrendszer a .NET-hez. Az adatbázissal való kommunikációt könnyítését szolgálja.
Használatával C\#-ban lehet adatbázis lekérdezéseket írni a LINQ (Language-Integrated Query) szoftvercsomag segítségével.
%----------------------------------------------------------------------------
@ -117,12 +117,12 @@ Használatának egyik előnye, hogy automatizált az állapot kezelés, tehát h
A Material \cite{material} elsősorban egy kezelőfelület tervezési útmutató a Google által, melyet követve szép és minőségi felületeket lehet készíteni.
A Material UI \cite{material-ui} egy szoftvercsomag, mely ezeket az útmutatásokat követő egyszerű React komponenseket tartalmaz.
Alkalmazásával könnyű esztétikus felhasználói felületeket készíteni, minimalizált a CSS használattal.
Alkalmazásával könnyő esztétikus felhasználói felületeket készíteni, minimalizált a CSS használattal.
%----------------------------------------------------------------------------
\subsection{Apexcharts}
%----------------------------------------------------------------------------
Az Apexcharts \cite{apexcharts} egy nyílt forráskódú JavaScript szoftvercsomag, amellyel könnyen konfigurálható, modern kinézetű diagramokat lehet készíteni.
Az Apexcharts \cite{apexcharts} egy nyílt forráskódú JavaScript szoftvercsomag, amellyel könnyen konfigurálható, modern kinézetű diagrammokat lehet készíteni.
Sokféle kliensoldali (és szerveroldali) technológiát támogat, köztük a React-et is. A kezelőfelületen található vizualizációk szinte összes elemét ennek használatával csináltam.
%----------------------------------------------------------------------------
@ -132,5 +132,5 @@ A Google szinte összes termékének van API-ja, ami lehetővé teszi a programo
A Google Maps sincs másképp és mivel ennek interfésze külön támogatja a hőtérképes réteg használatát is, nem gondoltam, hogy ettől jobb eszközt tudnék találni a feladat megvalósítására.
A Google Maps API-t, ami alapvetően csak egy JavaScript csomag, rengetegen újracsomagolják, hogy különböző részét, különböző keretrendszerekben is lehessen használni.
Ezek közül én a Google Map React-et \cite{google-map-react} választottam, egyrészt mert támogatja a hőtérképes réteg használatát,
Ezek közül én a Google Map React \cite{google-map-react}-et választottam, egyrészt mert támogatja a hőtérképes réteg használatát,
másrészt mert lehetővé teszi a térképen való React komponensek renderelését az alapértelmezett markerek helyett.

View File

@ -14,7 +14,7 @@ melyeket az alábbi szekciókban ismertetek.
\section{Helyettesítő szolgáltatások}
%----------------------------------------------------------------------------
Az alkalmazásom szerver oldali szolgáltatásai a Birbnetes Command and Control (a kódban Device) és Input Service-ekkel azok OpenAPI leíróiból generált interfészein keresztül kommunikál.
Ezen interfészek mögé bármilyen implementáció regisztrálható, mely helyettesíti az éles rendszer működését.
Ezen intefészek mögé bármilyen implementáció regisztrálható, mely helyettesíti az éles rendszer működését.
Készítettem egy osztályt \verb+DummyDeviceAndInputService+ néven, mely a szerver indulásakor mű eszközadatokat generál egy lokális változóval állítható darabszámban,
majd ezeket egy belső listában tárolja. Az eszközök státuszát és koordinátáit egy véletlenszám generátor segítségével határozom meg.
@ -23,7 +23,7 @@ azok státuszát olvassák és módosítják.
Illetve implementálja az Input Service interfészét,
melynek metódusa bármilyen paraméterből kapott egyedi azonosító esetén visszaad egy véletlenszerűen kiválasztott bekapcsolt státuszú eszközt a listából.
Az alkalmazás által regisztrált és ezáltal használt interfész implementációi a konfigurációs fájl egy logikai értéke alapján cserélhető az éles és a helyettesítő között,
Az alkalmazás által regisztrált és ezáltal használt intefész implementációi a konfigurációs fájl egy logikai értéke alapján cserélhető az éles és a helyettesítő között,
a \ref{lst:dummy-service-registration}-es listában látható módon.
\newpage
\begin{lstlisting}[style=csharp, caption=A helyettesítő és az éles szolgáltatások regisztrálásának logikája, label=lst:dummy-service-registration]
@ -50,7 +50,7 @@ Indítható vele MQTT szerver, feliratkozó kliens és publikáló kliens is.
Ezek meglétével az alkalmazás képes az üzenetek manuális publikálására egy a felületen beállítható témában.
Én azonban szerettem volna az üzeneteket automatikusan bizonyos időközönként küldeni,
ezért átalakítottam az alkalmazást az igényeimnek megfelelően a \ref{fig:mqtt-tester}-es ábrán látható módon.
Elhelyeztem a felületen egy csúszkát, mellyel az üzenet küldés intervalluma állítható, illetve két új gombot,
Elhelyeztem a fejlületen egy csúszkát, mellyel az üzenet küldés intervalluma állítható, illetve két új gombot,
melyekkel az üzenet küldő időzítő indítható és megállítható.
Az alkalmazás képes üzenetek adatainak generálására, mellyel az AI Service által publikált üzenetek modelljeivel azonos adatokat generálok.
\begin{figure}[!ht]

View File

@ -33,8 +33,8 @@ Ez több okból is hasznos:
A mikroszolgáltatások (microservices) nem sok mindenben különböznek egy általános szolgáltatástól.
Ugyan úgy valamilyen kéréseket kiszolgáló egységek, legyen az web kérések kiszolgálása HTTP-n keresztül
vagy akár parancssori utasítások feldolgozása. Az egyetlen fő különbség az a szolgáltatások felelősségköre.
A mikroszolgáltatások fejlesztésénél a fejlesztők elsősorban arra törekednek, hogy egy komponensnek minél kevesebb feladata és függősége legyen,
ezzel megnő a tesztelhetőség és könnyebb a skálázhatóság.
A mikroszolgáltatások fejlesztésénél a fejlesztők elsősorban arra törekednek, hogy egy komponensnek minnél kevesebb feladata és függősége legyen,
ezzel megnő a tesztelhetőség és könyebb a skálázhatóság.
%----------------------------------------------------------------------------
\subsubsection{Konténerek}
@ -52,7 +52,7 @@ Kihasználja és ötvözi az imént említett technológiák előnyeit, hogy egy
Használatával felgyorsulhat és automatizált lehet az egyes konténerek telepítése, futtatása, de talán a legfőbb előnye,
hogy segítségével könnyedén megoldható a rendszert ért terhelési igények szerinti dinamikus skálázódás.
Azok a mikroszolgáltatások, amikre a rendszernek épp nincs szüksége, minimális erőforrást igényelnek a szerveren,
így nem kell utánuk annyit fizetni sem. Ezzel ellentétben, ha valamely szolgáltatás után hirtelen megnő az igény,
így nem kell utánnuk annyit fizetni sem. Ezzel ellentétben, ha valamely szolgáltatás után hirtelen megnő az igény,
akkor az könnyedén duplikálható.
%----------------------------------------------------------------------------
@ -68,7 +68,7 @@ Működéséhez szükség van egy szerverre, amelynek feladata a beérkező üze
\subsection{OpenAPI}
%----------------------------------------------------------------------------
Az OpenAPI egy nyilvános alkalmazás-programozási leíró, amely a fejlesztők számára hozzáférést biztosít egy másik alkalmazáshoz.
Az API-k leírják és meghatározzák, hogy egy alkalmazás hogyan kommunikálhat egy másikkal,
Az API-k lírják és meghatározzák, hogy egy alkalmazás hogyan kommunikálhat egy másikkal,
melyet használva a fejlesztők könnyedén képesek a kommunikációra képes kódot írni vagy generálni.
%----------------------------------------------------------------------------
@ -109,7 +109,7 @@ Tartalmaznak még egy hangszórót is, mely a madarak elijesztését szolgálja.
A kihelyezett IoT eszközök által felvett hangfájlok ezen a komponensen keresztül érkeznek be a rendszerbe.
Itt történik a hanganyaghoz tartozó metaadatok lementése az Input Service saját relációs adatbázisába.
Ilyenek például a beküldő eszköz azonosítója, a beérkezés dátuma vagy a hangüzenet rendszerszintű egyedi azonosítója.
Amint a szolgáltatás a beérkezett üzenettel kapcsolatban elvégezte az összes feladatát,
Amint a szolgáltatás a berékezett üzenettel kapcsolatban elvégezte az összes feladatát,
publikál egy üzenetet egy másik üzenetsorra a többi kliensnek feldolgozásra.
%----------------------------------------------------------------------------
@ -130,6 +130,6 @@ Ha igen, akkor az üzenetsoron küld egy riasztás parancsot a hanganyagot küld
%----------------------------------------------------------------------------
\subsubsection{Command and Control Service}
%----------------------------------------------------------------------------
A Command and Control Service az előzőkkel ellentétben egyáltalán nem vesz részt a minták fogadásában, feldolgozásában vagy kezelésében.
Felelősége az eszközök és azok szenzorjai állapotának menedzselése és követése.
A Command and Control Service az előzőekkel ellentétben egyáltalán nem vesz részt a minták fogadásában, feldolgozásában vagy kezelésében.
Felelősége az eszközök és azok szenzorai állapotának menedzselése és követése.
Ezen keresztül lehet az egyes eszközöket ki- és bekapcsolni.

View File

@ -3,7 +3,7 @@
%----------------------------------------------------------------------------
Szőlőtulajdonosoknak éves szinten jelentős kárt okoznak a seregélyek, akik előszeretettel választják táplálékul a megtermelt szőlőt.
Erre a problémára dolgoztak ki a tanszéken diáktársaim egy felhő alapú konténerizált rendszert, a Birbnetes-t
mely a természetben elhelyezett eszközökkel kommunikál, azokat vezérli.
mely a természetben elkelyezett eszközökkel kommunikál, azokat vezérli.
Az eszközök bizonyos időközönként hangfelvételt készítenek a környezetükről,
majd valamilyen formában elküldik ezeket a felvételeket a központi rendszernek,
amely egy erre a célra kifejlesztett mesterséges intelligenciát használva eldönti
@ -12,19 +12,19 @@ Ha igen akkor jelez a felvételt küldő eszköznek, hogy szólaltassa meg a ria
berendezését, hogy elijessze a madarakat.
%----------------------------------------------------------------------------
\section{Probléma}
\section{A probléma}
%----------------------------------------------------------------------------
A jelen rendszer használata során nincs vizuális visszacsatolás az esetleges riasztásokról azok gyakoriságáról
és a rendszer állapotáról sem. Különböző diagnosztikai eszközök ugyan implementálva lettek, mint például
és a rendszer állapotáról sem. Különböző diagnosztikai eszközök ugyan implementálva lettek mint például
a naplózás vagy a hiba bejelentés, de ezek használata nehézkes, nem kézenfekvő.
Szükség van egy olyan megoldásra, amivel egy helyen és egyszerűen lehet kezelni és felügyelni a rendszer egyes elemeit.
Szükség van egy olyan megoldásra amivel egy helyen és egyszerűen lehet kezelni és felügyelni a rendszer egyes elemeit.
%----------------------------------------------------------------------------
\section{Megoldás}
\section{A megoldás}
%----------------------------------------------------------------------------
A jelen szakdolgozat egy olyan webes alkalmazás elkészítését dokumentálja, mellyel a felhasználók képesek
A jelen szakdolgozat egy olyan webes alkalmazás elkészítését dokumentálja, melyel a felhasználók képesek
a természetben elhelyezett eszközök állapotát vizsgálni, azokat akár ki és bekapcsolni igény szerint.
Az egyes rendszer eseményeket vizsgálva a szoftver statisztikákat készít, melyeket különböző diagramokon ábrázolok.
Az egyes rendszer eseményeket vizsgálva a szoftver statisztikákat készít, melyeket különböző diagrammokon ábrázolok.
Ilyen statisztikák például, hogy időben melyik eszköz mikor észlelt madár hangot, vagy hogy hány hang üzenet érkezik
az eszközöktől másodpercenként.
@ -33,8 +33,7 @@ az eszközöktől másodpercenként.
%----------------------------------------------------------------------------
A szakdolgozatom első részében, a \ref{chapt:birdnetes-introduction}. fejezetben, bemutatom a vizualizálni kívánt rendszer felépítését, az egyes komponensek közötti kapcsolatokat,
valamint a vizualizációs szempontból releváns technológiákat, amire a rendszer épült.
A \ref{chapt:birdmap-introduction}. fejezetben ismertetem a jelenleg az iparban is használt mikroszolgáltatás működését vizualizáló alternatívákat, majd a saját megoldásom tervezetét, az arra vonatkozó elvárásokat.
A \ref{chapt:birdmap-technologies}. fejezetben az alkalmazásom által használt technológiákat mutatom be,
ezzel előkészítve az \ref{chapt:birdmap-backend}. és \ref{chapt:birdmap-frontend}. fejezetet, ahol ismertetem a szerver- és kliensalkalmazások felépítését.
A \ref{chapt:birdmap-test}. és \ref{chapt:birdmap-kubernetes}. fejezet az alkalmazás teszteléséről és telepítéséről szól.
A 3. fejezetben ismertetem a jelenleg az iparban is használt mikroszolgáltatás működését vizualizáló alternatívákat, majd a saját megoldásom tervezetét, az arra vonatkozó elvárásokat.
A 4. fejezetben az alkalmazásom által használt technológiákat mutatom be, ezzel előkészítve az 5. és 6. fejezetet, ahol ismertetem a szerver- és kliensalkalmazások felépítését.
A 7. és 8. fejezet az alkalmazás teszteléséről és telepítéséről szól.
Az utolsó fejezetben értékelem a munkám eredményét, levonom a tapasztalatokat és bemutatok néhány továbbfejlesztési lehetőséget.

View File

@ -5,11 +5,11 @@
Úgy gondolom, hogy az alkalmazásom elérte a célját.
Egy használható felületet nyújt a Birbnetes mikroszolgáltatás rendszere működésének vizualizálására.
A fejlesztés közben jelentős figyelmet fordítottam arra, hogy az alkalmazás felületi és kód komponensei között is
minimalizáltak legyenek a függőségek, így a rendszerben történő változások esetén azok könnyen cserélhetőek, bővíthetőek.
minimalizáltak legyenek a függőségek, így a rendszerben történő változások esetén azok könnyen cseréhetőek, bővíthetőek.
%----------------------------------------------------------------------------
\section{Továbbfejlesztési lehetőségek}
%----------------------------------------------------------------------------
Az kliens oldalon történő diagramok adatainak generálása hamar túl nagy falatnak bizonyult.
Az kliens oldalon történő diagrammok adatainak generálása hamar túl nagy falatnak bizonyult.
A bevetett optimalizációk ellenére sem lett hatványozottan gyorsabb a felület.
Így az első és legfontosabb továbbfejlesztési teendő az adatok szerveroldalon történő generálása lenne.

File diff suppressed because it is too large Load Diff