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); } }