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;
///
/// 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)
{
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 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(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(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 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 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 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 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 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);
}
}