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.Visualization;
using RobotNet10.FleetManager.Events;
using RobotNet10.FleetManager.Services.ConfigManager;
using RobotNet10.MqttConnection;
using System.Text;
using System.Text.Json;
namespace RobotNet10.FleetManager.Services.RobotConnections;
///
/// Service implementation for managing MQTT connections to robots via VDA5050 protocol
///
public class RobotConnectionsService(
IConnectionConfig configManager,
IRobotEventBus eventBus,
IServiceProvider serviceProvider,
Logger logger,
ILogger mqttLogger) : IRobotConnectionsService
{
private readonly IConnectionConfig _configManager = configManager;
private readonly IRobotEventBus _eventBus = eventBus;
private readonly IServiceProvider _serviceProvider = serviceProvider;
private readonly Logger _logger = logger;
private readonly ILogger _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)
{
try
{
await StopAsync();
if (!_connectionSemaphore.Wait(1000)) return;
var mqttConfig = _configManager.GetMqttConfig();
var vdaConfig = _configManager.GetVDA5050Config();
MqttTopicFilter[] topics = [
new MqttTopicFilterBuilder().WithTopic($"{vdaConfig.TopicPrefix}/{vdaConfig.Manufacturer}/+/{VDA5050Topic.STATE.ToJsonString()}")
.WithAtMostOnceQoS()
.Build(),
new MqttTopicFilterBuilder().WithTopic($"{vdaConfig.TopicPrefix}/{vdaConfig.Manufacturer}/+/{VDA5050Topic.VISUALIZATION.ToJsonString()}")
.WithAtMostOnceQoS()
.Build(),
new MqttTopicFilterBuilder().WithTopic($"{vdaConfig.TopicPrefix}/{vdaConfig.Manufacturer}/+/{VDA5050Topic.FACTSHEET.ToJsonString()}")
.WithAtMostOnceQoS()
.Build(),
new MqttTopicFilterBuilder().WithTopic($"{vdaConfig.TopicPrefix}/{vdaConfig.Manufacturer}/+/{VDA5050Topic.CONNECTION.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);
_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))
{
using var scope = _serviceProvider.CreateAsyncScope();
var _robotService = scope.ServiceProvider.GetRequiredService();
if (await _robotService.ExistsAsync(robotId))
{
if (messageType == VDA5050Topic.STATE.ToJsonString())
{
HandleStateMessageAsync(robotId, payload);
}
else if (messageType == VDA5050Topic.VISUALIZATION.ToJsonString())
{
HandleVisualizationMessageAsync(robotId, payload);
}
else if (messageType == VDA5050Topic.FACTSHEET.ToJsonString())
{
HandleFactsheetMessageAsync(robotId, payload);
}
else if (messageType == VDA5050Topic.CONNECTION.ToJsonString())
{
HandleConnectionMessageAsync(robotId, 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 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);
}
}
public async Task PublishOrderAsync(string robotId, OrderMsg order, CancellationToken cancellationToken = default)
{
if (_mqttClient is null)
{
_logger.Warning("Mqtt Client not initialized");
return false;
}
if (string.IsNullOrEmpty(robotId))
{
_logger.Warning("Cannot publish order: robotId is null or empty");
return false;
}
if (order == null)
{
_logger.Warning("Cannot publish order: order message is null");
return false;
}
if (string.IsNullOrEmpty(order.OrderId))
{
_logger.Warning("Cannot publish order: orderId is null or empty");
return false;
}
if (!IsConnected)
{
_logger.Warning($"Cannot publish order to robot {robotId}: MQTT client is not connected");
return false;
}
try
{
var topic = BuildPublishTopic(robotId, VDA5050Topic.ORDER);
var data = JsonSerializer.Serialize(order, JsonOptionExtends.Write);
await _mqttClient.PublishAsync(topic, data);
return true;
}
catch (OperationCanceledException)
{
_logger.Warning($"Publish order to robot {robotId} was cancelled");
return false;
}
catch (Exception ex)
{
_logger.Error($"Error publishing order to robot {robotId}: {ex.Message}");
return false;
}
}
public async Task PublishInstantActionsAsync(string robotId, InstantActionsMsg instantActions, CancellationToken cancellationToken = default)
{
if (_mqttClient is null)
{
_logger.Warning("Mqtt Client not initialized");
return false;
}
if (string.IsNullOrEmpty(robotId))
{
_logger.Warning("Cannot publish instantActions: robotId is null or empty");
return false;
}
if (instantActions == null)
{
_logger.Warning("Cannot publish instantActions: instantActions message is null");
return false;
}
if (instantActions.Actions == null || instantActions.Actions.Length == 0)
{
_logger.Warning("Cannot publish instantActions: actions array is null or empty");
return false;
}
if (!IsConnected)
{
_logger.Warning($"Cannot publish instantActions to robot {robotId}: MQTT client is not connected");
return false;
}
try
{
var topic = BuildPublishTopic(robotId, VDA5050Topic.INSTANTACTIONS);
var data = JsonSerializer.Serialize(instantActions, JsonOptionExtends.Write);
await _mqttClient.PublishAsync(topic, data);
return true;
}
catch (OperationCanceledException)
{
_logger.Warning($"Publish instantActions to robot {robotId} was cancelled");
return false;
}
catch (Exception ex)
{
_logger.Error($"Error publishing instantActions to robot {robotId}: {ex.Message}");
return false;
}
}
private void HandleStateMessageAsync(string serialNumber, string payload)
{
try
{
var stateMsg = JsonSerializer.Deserialize(payload, JsonOptionExtends.Read);
if (stateMsg == null || stateMsg.SerialNumber != serialNumber) return;
_eventBus.PublishStateMessageReceived(serialNumber, stateMsg);
}
catch (Exception ex)
{
_logger.Error($"Error handling state message from robot {serialNumber}: {ex.Message}");
}
}
private void HandleConnectionMessageAsync(string serialNumber, string payload)
{
try
{
var connectionMsg = JsonSerializer.Deserialize(payload, JsonOptionExtends.Read);
if (connectionMsg == null || connectionMsg.SerialNumber != serialNumber) return;
_eventBus.PublishConnectionStateChanged(serialNumber, connectionMsg.ConnectionState);
}
catch (Exception ex)
{
_logger.Error($"Error handling connection message from robot {serialNumber}: {ex.Message}");
}
}
private void HandleVisualizationMessageAsync(string serialNumber, string payload)
{
try
{
var visualizationMsg = JsonSerializer.Deserialize(payload, JsonOptionExtends.Read);
if (visualizationMsg == null || visualizationMsg.SerialNumber != serialNumber) return;
_eventBus.PublishVisualizationMessageReceived(serialNumber, visualizationMsg);
}
catch (Exception ex)
{
_logger.Error($"Error handling visualization message from robot {serialNumber}: {ex.Message}");
}
}
private void HandleFactsheetMessageAsync(string serialNumber, string payload)
{
try
{
var factsheetMsg = JsonSerializer.Deserialize(payload, JsonOptionExtends.Read);
if (factsheetMsg == null || factsheetMsg.SerialNumber != serialNumber) return;
_eventBus.PublishFactsheetMessageReceived(serialNumber, factsheetMsg);
}
catch (Exception ex)
{
_logger.Error($"Error handling factsheet message from robot {serialNumber}: {ex.Message}");
}
}
private string BuildPublishTopic(string robotId, VDA5050Topic topic)
{
var vdaConfig = _configManager.GetVDA5050Config();
return $"{vdaConfig.TopicPrefix}/{vdaConfig.Manufacturer}/{robotId}/{topic.ToJsonString()}";
}
}