Initial commit
This commit is contained in:
@@ -0,0 +1,424 @@
|
||||
using MQTTnet;
|
||||
using MQTTnet.Packets;
|
||||
using RobotNet.VDA5050;
|
||||
using RobotNet.VDA5050.Connection;
|
||||
using RobotNet.VDA5050.Factsheet;
|
||||
using RobotNet.VDA5050.InstantAction;
|
||||
using RobotNet.VDA5050.Order;
|
||||
using RobotNet.VDA5050.State;
|
||||
using RobotNet.VDA5050.Type;
|
||||
using RobotNet.VDA5050.Visualization;
|
||||
using RobotNet10.MqttConnection;
|
||||
using RobotNet10.RobotApp.Events;
|
||||
using RobotNet10.RobotApp.Services.ConfigManager;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace RobotNet10.RobotApp.Services.Robot.Connection;
|
||||
|
||||
/// <summary>
|
||||
/// Service implementation for managing MQTT connections to robots via VDA5050 protocol
|
||||
/// </summary>
|
||||
public class RobotConnectionsService(
|
||||
IConnectionConfig configManager,
|
||||
IRobotEventBus eventBus,
|
||||
IServiceProvider serviceProvider,
|
||||
Logger<RobotConnectionsService> logger,
|
||||
ILogger<MQTTClient> mqttLogger) : IRobotConnectionsService
|
||||
{
|
||||
private readonly IConnectionConfig _configManager = configManager;
|
||||
private readonly IRobotEventBus _eventBus = eventBus;
|
||||
private readonly IServiceProvider _serviceProvider = serviceProvider;
|
||||
private readonly Logger<RobotConnectionsService> _logger = logger;
|
||||
private readonly ILogger<MQTTClient> _mqttLogger = mqttLogger;
|
||||
|
||||
private MQTTClient? _mqttClient;
|
||||
private readonly SemaphoreSlim _connectionSemaphore = new(1, 1);
|
||||
|
||||
public bool IsConnected => _mqttClient is not null && _mqttClient.IsConnected;
|
||||
|
||||
public async Task StartAsync(CancellationToken? cancellationToken)
|
||||
{
|
||||
if (!_connectionSemaphore.Wait(1000)) return;
|
||||
try
|
||||
{
|
||||
if (IsConnected) return;
|
||||
|
||||
await StopAsync();
|
||||
|
||||
var mqttConfig = _configManager.GetMqttConfig();
|
||||
var vdaConfig = _configManager.GetVDA5050Config();
|
||||
|
||||
MqttTopicFilter[] topics = [
|
||||
new MqttTopicFilterBuilder().WithTopic($"{vdaConfig.TopicPrefix}/{vdaConfig.Manufacturer}/+/{VDA5050Topic.ORDER.ToJsonString()}")
|
||||
.WithAtMostOnceQoS()
|
||||
.Build(),
|
||||
new MqttTopicFilterBuilder().WithTopic($"{vdaConfig.TopicPrefix}/{vdaConfig.Manufacturer}/+/{VDA5050Topic.INSTANTACTIONS.ToJsonString()}")
|
||||
.WithAtMostOnceQoS()
|
||||
.Build()
|
||||
];
|
||||
|
||||
_mqttClient = new MQTTClient(mqttConfig, topics, _mqttLogger);
|
||||
_mqttClient.MessageUpdated += MessageUpdated;
|
||||
if (_mqttClient is not null) await _mqttClient.ConnectAsync(cancellationToken);
|
||||
if (_mqttClient is not null) await _mqttClient.SubscribeAsync(cancellationToken);
|
||||
|
||||
// Publish ONLINE once broker connection and subscriptions are ready.
|
||||
await PublishConnectionStateAsync(ConnectionState.ONLINE);
|
||||
|
||||
_logger.Info("RobotConnectionsService started successfully");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"Connection broker is failed: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_connectionSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StopAsync()
|
||||
{
|
||||
if (_mqttClient is not null)
|
||||
{
|
||||
await _mqttClient.DisposeAsync();
|
||||
_mqttClient = null;
|
||||
_logger.Info("RobotConnectionsService stopped");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task MessageUpdated(MqttApplicationMessageReceivedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var topic = e.ApplicationMessage.Topic;
|
||||
var payload = Encoding.UTF8.GetString(e.ApplicationMessage.Payload);
|
||||
var (robotId, messageType) = ParseVDA5050Topic(topic);
|
||||
if (!string.IsNullOrEmpty(robotId) && !string.IsNullOrEmpty(messageType))
|
||||
{
|
||||
var vdaConfig = _configManager.GetVDA5050Config();
|
||||
if (robotId == vdaConfig.SerialNumber)
|
||||
{
|
||||
if (messageType == VDA5050Topic.ORDER.ToJsonString())
|
||||
{
|
||||
HandleOrderMessageAsync(payload);
|
||||
}
|
||||
else if (messageType == VDA5050Topic.INSTANTACTIONS.ToJsonString())
|
||||
{
|
||||
HandleInstantActionMessageAsync(payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Warning("Failed to parse topic");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"Error processing message: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private (string? robotId, string? messageType) ParseVDA5050Topic(string topic)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(topic)) return (null, null);
|
||||
|
||||
var vdaConfig = _configManager.GetVDA5050Config();
|
||||
ReadOnlySpan<char> topicSpan = topic.AsSpan();
|
||||
var manufacturerSpan = $"/{vdaConfig.Manufacturer}/".AsSpan();
|
||||
int manufacturerIndex = topicSpan.IndexOf(manufacturerSpan);
|
||||
|
||||
if (manufacturerIndex == -1) return (null, null);
|
||||
|
||||
var remaining = topicSpan[(manufacturerIndex + manufacturerSpan.Length)..];
|
||||
int firstSlash = remaining.IndexOf('/');
|
||||
if (firstSlash == -1) return (null, null);
|
||||
|
||||
var robotId = remaining[..firstSlash].ToString();
|
||||
var messageType = remaining[(firstSlash + 1)..].ToString();
|
||||
|
||||
return (robotId, messageType);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"Parse VDA5050 Topic failed: {ex}");
|
||||
return (null, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleOrderMessageAsync(string payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
var orderMsg = JsonSerializer.Deserialize<OrderMsg>(payload, JsonOptionExtends.Read);
|
||||
if (orderMsg is null) return;
|
||||
_eventBus.PublishOrderMessageReceived(orderMsg);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error handling order message: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleInstantActionMessageAsync(string payload)
|
||||
{
|
||||
try
|
||||
{
|
||||
var instantActionMsg = JsonSerializer.Deserialize<InstantActionsMsg>(payload, JsonOptionExtends.Read);
|
||||
if (instantActionMsg is null) return;
|
||||
|
||||
_eventBus.PublishInstantActionMessageReceived(instantActionMsg);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error handling instant action message: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private string BuildPublishTopic(string robotId, VDA5050Topic topic)
|
||||
{
|
||||
var vdaConfig = _configManager.GetVDA5050Config();
|
||||
return $"{vdaConfig.TopicPrefix}/{vdaConfig.Manufacturer}/{robotId}/{topic.ToJsonString()}";
|
||||
}
|
||||
|
||||
private async Task<bool> EnsureMqttClientReadyAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_mqttClient is not null && IsConnected)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Startup can publish before the async connection task finishes.
|
||||
_logger.Info("Mqtt Client not initialized yet, attempting to connect...");
|
||||
await StartAsync(cancellationToken);
|
||||
|
||||
return _mqttClient is not null && IsConnected;
|
||||
}
|
||||
|
||||
public async Task<bool> PublishStateAsync(StateMsg state, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!await EnsureMqttClientReadyAsync(cancellationToken))
|
||||
{
|
||||
_logger.Warning("Cannot publish state: MQTT client is not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (state == null)
|
||||
{
|
||||
_logger.Warning("Cannot publish state: state message is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(state.SerialNumber))
|
||||
{
|
||||
_logger.Warning("Cannot publish state: SerialNumber is null or empty");
|
||||
return false;
|
||||
}
|
||||
|
||||
var vdaConfig = _configManager.GetVDA5050Config();
|
||||
if (state.SerialNumber != vdaConfig.SerialNumber)
|
||||
{
|
||||
_logger.Warning("Cannot publish state: state.SerialNumber is diffirent SerialNumber setting");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsConnected)
|
||||
{
|
||||
_logger.Warning($"Cannot publish state: MQTT client is not connected");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var topic = BuildPublishTopic(state.SerialNumber, VDA5050Topic.STATE);
|
||||
var data = JsonSerializer.Serialize(state, JsonOptionExtends.Write);
|
||||
await _mqttClient.PublishAsync(topic, data);
|
||||
return true;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.Warning($"Publish state was cancelled");
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error publishing state: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> PublishVisualizationAsync(VisualizationMsg visualization, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!await EnsureMqttClientReadyAsync(cancellationToken))
|
||||
{
|
||||
_logger.Warning("Cannot publish visualization: MQTT client is not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (visualization == null)
|
||||
{
|
||||
_logger.Warning("Cannot publish visualization: visualization message is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(visualization.SerialNumber))
|
||||
{
|
||||
_logger.Warning("Cannot publish visualization: SerialNumber is null or empty");
|
||||
return false;
|
||||
}
|
||||
|
||||
var vdaConfig = _configManager.GetVDA5050Config();
|
||||
if (visualization.SerialNumber != vdaConfig.SerialNumber)
|
||||
{
|
||||
_logger.Warning("Cannot publish visualization: visualization.SerialNumber is diffirent SerialNumber setting");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsConnected)
|
||||
{
|
||||
_logger.Warning($"Cannot publish visualization: MQTT client is not connected");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var topic = BuildPublishTopic(visualization.SerialNumber, VDA5050Topic.VISUALIZATION);
|
||||
var data = JsonSerializer.Serialize(visualization, JsonOptionExtends.Write);
|
||||
await _mqttClient.PublishAsync(topic, data);
|
||||
return true;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.Warning($"Publish visualization was cancelled");
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error publishing visualization: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> PublishFactsheetAsync(FactSheetMsg factsheet, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!await EnsureMqttClientReadyAsync(cancellationToken))
|
||||
{
|
||||
_logger.Warning("Cannot publish factsheet: MQTT client is not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (factsheet == null)
|
||||
{
|
||||
_logger.Warning("Cannot publish factsheet: factsheet message is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(factsheet.SerialNumber))
|
||||
{
|
||||
_logger.Warning("Cannot publish factsheet: SerialNumber is null or empty");
|
||||
return false;
|
||||
}
|
||||
|
||||
var vdaConfig = _configManager.GetVDA5050Config();
|
||||
if (factsheet.SerialNumber != vdaConfig.SerialNumber)
|
||||
{
|
||||
_logger.Warning("Cannot publish factsheet: factsheet.SerialNumber is diffirent SerialNumber setting");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsConnected)
|
||||
{
|
||||
_logger.Warning($"Cannot publish factsheet: MQTT client is not connected");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var topic = BuildPublishTopic(factsheet.SerialNumber, VDA5050Topic.FACTSHEET);
|
||||
var data = JsonSerializer.Serialize(factsheet, JsonOptionExtends.Write);
|
||||
await _mqttClient.PublishAsync(topic, data);
|
||||
return true;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.Warning($"Publish factsheet was cancelled");
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error publishing factsheet: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> PublishConnectionAsync(ConnectionMsg connection, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!await EnsureMqttClientReadyAsync(cancellationToken))
|
||||
{
|
||||
_logger.Warning("Cannot publish connection: MQTT client is not initialized");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (connection == null)
|
||||
{
|
||||
_logger.Warning("Cannot publish connection: connection message is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(connection.SerialNumber))
|
||||
{
|
||||
_logger.Warning("Cannot publish connection: SerialNumber is null or empty");
|
||||
return false;
|
||||
}
|
||||
|
||||
var vdaConfig = _configManager.GetVDA5050Config();
|
||||
if (connection.SerialNumber != vdaConfig.SerialNumber)
|
||||
{
|
||||
_logger.Warning("Cannot publish connection: connection.SerialNumber is diffirent SerialNumber setting");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsConnected)
|
||||
{
|
||||
_logger.Warning($"Cannot publish connection: MQTT client is not connected");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var topic = BuildPublishTopic(connection.SerialNumber, VDA5050Topic.CONNECTION);
|
||||
var data = JsonSerializer.Serialize(connection, JsonOptionExtends.Write);
|
||||
await _mqttClient.PublishAsync(topic, data, retain: true);
|
||||
return true;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.Warning($"Publish connection was cancelled");
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error publishing connection: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task PublishConnectionStateAsync(ConnectionState state)
|
||||
{
|
||||
var vdaConfig = _configManager.GetVDA5050Config();
|
||||
var connectionMsg = new ConnectionMsg
|
||||
{
|
||||
HeaderId = 1,
|
||||
SerialNumber = vdaConfig.SerialNumber,
|
||||
Timestamp = DateTime.Now,
|
||||
Manufacturer = vdaConfig.Manufacturer,
|
||||
ConnectionState = state,
|
||||
Version = vdaConfig.Version
|
||||
};
|
||||
await PublishConnectionAsync(connectionMsg);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user