using RobotNet10.CustomConfiguration.Events; using RobotNet10.CustomConfiguration.Models; using RobotNet10.CustomConfiguration.Services; using RobotNet10.MqttConnection; using RobotNet10.RobotApp.Services.Robot.Connection.Models; using System.Reflection; using System.Text.Json; namespace RobotNet10.RobotApp.Services.ConfigManager; /// /// Service implementation for managing application configurations /// public class ConnectionConfig : IConnectionConfig { private readonly IConfigManager _configManager; private readonly Logger _logger; private readonly Lock _lockObject = new(); private VDA5050ProtocolConfig? _vda5050Config; private MQTTConfig? _mqttConfig; private bool _vda5050ConfigLoaded = false; private bool _mqttConfigLoaded = false; private const string VDA5050_PROTOCOL_CONFIG_TYPE = "VDA5050ProtocolConfig"; private const string MQTT_CONFIG_TYPE = "MQTTConfig"; private const string VDA5050_CONFIG_FILE_NAME = "VDA5050ProtocolConfig.config.json"; private const string MQTT_CONFIG_FILE_NAME = "MQTTConfig.config.json"; public ConnectionConfig(IConfigManager configManager, Logger logger) { _configManager = configManager ?? throw new ArgumentNullException(nameof(configManager)); _logger = logger; // Subscribe to config changes _configManager.ConfigChanged += OnConfigChanged; } public VDA5050ProtocolConfig GetVDA5050Config() { if (!_vda5050ConfigLoaded) { lock (_lockObject) { if (!_vda5050ConfigLoaded) { LoadVDA5050ConfigAsync().GetAwaiter().GetResult(); } } } return _vda5050Config ?? throw new InvalidOperationException("VDA5050 configuration not loaded"); } public MQTTConfig GetMqttConfig() { if (!_mqttConfigLoaded) { lock (_lockObject) { if (!_mqttConfigLoaded) { LoadMqttConfigAsync().GetAwaiter().GetResult(); } } } return _mqttConfig ?? throw new InvalidOperationException("MQTT configuration not loaded"); } private async Task LoadVDA5050ConfigAsync() { try { var configFile = await _configManager.GetConfigByTypeAsync(VDA5050_PROTOCOL_CONFIG_TYPE); if (configFile == null) { if (!TryLoadVda5050ConfigFromLocalFile(out var vdaConfig)) { _logger.Warning("VDA5050 Protocol configuration not found, using defaults"); _vda5050Config = new VDA5050ProtocolConfig { Manufacturer = "RobotNet", Version = "2.1.0", TopicPrefix = "uagv/v2", SerialNumber = "Robot-Dev" }; } else { _vda5050Config = vdaConfig; } } else { _vda5050Config = MapConfigVariablesToObject(configFile.Variables); _logger.Info("VDA5050 Protocol configuration loaded successfully"); } } catch (Exception ex) { _logger.Error($"Error loading VDA5050 Protocol configuration, using defaults: {ex.Message}"); _vda5050Config = new VDA5050ProtocolConfig { Manufacturer = "RobotNet", Version = "2.1.0", TopicPrefix = "uagv/v2" }; } finally { _vda5050ConfigLoaded = true; } } private async Task LoadMqttConfigAsync() { var configFile = await _configManager.GetConfigByTypeAsync(MQTT_CONFIG_TYPE); if (configFile is not null) { _mqttConfig = MapConfigVariablesToObject(configFile.Variables); } else if (TryLoadMqttConfigFromLocalFile(out var localMqttConfig)) { _mqttConfig = localMqttConfig; } else { throw new InvalidOperationException($"MQTT configuration (ConfigType: {MQTT_CONFIG_TYPE}) not found"); } if (string.IsNullOrEmpty(_mqttConfig?.Host)) { throw new InvalidOperationException("MQTT configuration: Host is required"); } if (string.IsNullOrEmpty(_mqttConfig.ClientId)) { throw new InvalidOperationException("MQTT configuration: ClientId is required"); } _mqttConfigLoaded = true; _logger.Info("MQTT configuration loaded successfully"); } private bool TryLoadVda5050ConfigFromLocalFile(out VDA5050ProtocolConfig config) { config = new VDA5050ProtocolConfig(); foreach (var path in GetLocalConfigCandidates(VDA5050_CONFIG_FILE_NAME)) { try { if (!File.Exists(path)) { continue; } using var doc = JsonDocument.Parse(File.ReadAllText(path)); config = new VDA5050ProtocolConfig { Manufacturer = GetStringVariable(doc.RootElement, "Manufacturer", "RobotNet"), Version = GetStringVariable(doc.RootElement, "Version", "2.1.0"), TopicPrefix = GetStringVariable(doc.RootElement, "TopicPrefix", "uagv/v2"), SerialNumber = GetStringVariable(doc.RootElement, "SerialNumber", "Robot-Dev") }; _logger.Info($"VDA5050 Protocol configuration loaded from local file: {path}"); return true; } catch (Exception ex) { _logger.Warning($"Failed reading VDA5050 config file '{path}': {ex.Message}"); } } return false; } private bool TryLoadMqttConfigFromLocalFile(out MQTTConfig config) { config = new MQTTConfig(); foreach (var path in GetLocalConfigCandidates(MQTT_CONFIG_FILE_NAME)) { try { if (!File.Exists(path)) { continue; } using var doc = JsonDocument.Parse(File.ReadAllText(path)); config = new MQTTConfig { Host = GetStringVariable(doc.RootElement, "Host", string.Empty), Port = GetIntVariable(doc.RootElement, "Port", 1883), ClientId = GetStringVariable(doc.RootElement, "ClientId", string.Empty), Username = GetStringVariable(doc.RootElement, "Username", string.Empty), Password = GetStringVariable(doc.RootElement, "Password", string.Empty), EnablePassword = GetBoolVariable(doc.RootElement, "EnablePassword", false), EnableTls = GetBoolVariable(doc.RootElement, "EnableTls", false), EnableCA = GetBoolVariable(doc.RootElement, "EnableCA", false), CaCertificatesPath = GetStringVariable(doc.RootElement, "CaCertificatesPath", string.Empty), ClientCertificatePath = GetStringVariable(doc.RootElement, "ClientCertificatePath", string.Empty), ClientKeyPath = GetStringVariable(doc.RootElement, "ClientKeyPath", string.Empty), PublishRepeat = GetIntVariable(doc.RootElement, "PublishRepeat", 2) }; _logger.Info($"MQTT configuration loaded from local file: {path}"); return true; } catch (Exception ex) { _logger.Warning($"Failed reading MQTT config file '{path}': {ex.Message}"); } } return false; } private static IEnumerable GetLocalConfigCandidates(string fileName) { yield return Path.Combine(Directory.GetCurrentDirectory(), "RobotAppCustomConfigs", "configs", fileName); yield return Path.Combine(AppContext.BaseDirectory, "RobotAppCustomConfigs", "configs", fileName); } private static string GetStringVariable(JsonElement root, string variableName, string fallback) { if (!root.TryGetProperty("variables", out var variables) || variables.ValueKind != JsonValueKind.Array) { return fallback; } foreach (var variable in variables.EnumerateArray()) { if (!variable.TryGetProperty("name", out var nameElement) || !string.Equals(nameElement.GetString(), variableName, StringComparison.Ordinal)) { continue; } if (variable.TryGetProperty("value", out var valueElement) && valueElement.ValueKind != JsonValueKind.Null) { return valueElement.ToString() ?? fallback; } } return fallback; } private static int GetIntVariable(JsonElement root, string variableName, int fallback) { var rawValue = GetStringVariable(root, variableName, fallback.ToString()); return int.TryParse(rawValue, out var value) ? value : fallback; } private static bool GetBoolVariable(JsonElement root, string variableName, bool fallback) { var rawValue = GetStringVariable(root, variableName, fallback.ToString()); return bool.TryParse(rawValue, out var value) ? value : fallback; } private void OnConfigChanged(object? sender, ConfigChangedEventArgs e) { // Reload config if it's one of our config types if (e.ConfigType == VDA5050_PROTOCOL_CONFIG_TYPE) { lock (_lockObject) { _vda5050ConfigLoaded = false; _vda5050Config = null; } _logger.Info("VDA5050 Protocol configuration changed, will reload on next access"); } else if (e.ConfigType == MQTT_CONFIG_TYPE) { lock (_lockObject) { _mqttConfigLoaded = false; _mqttConfig = null; } _logger.Info("MQTT configuration changed, will reload on next access"); } } private T MapConfigVariablesToObject(List variables) where T : new() { var obj = new T(); var properties = typeof(T).GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.SetProperty); foreach (var property in properties) { // Find variable by exact name match (case-sensitive) var variable = variables.FirstOrDefault(v => v.Name == property.Name); if (variable != null && variable.Value != null) { try { var convertedValue = ConvertValue(variable.Value, property.PropertyType, variable.Type); property.SetValue(obj, convertedValue); } catch (Exception ex) { _logger.Warning($"Failed to set property {property.Name} from variable {variable.Name}: {ex.Message}"); } } } return obj; } private object? ConvertValue(object? value, Type targetType, ConfigVariableType variableType) { if (value == null) return targetType.IsValueType ? Activator.CreateInstance(targetType) : null; // If value is already of the correct type, return it if (targetType.IsInstanceOfType(value)) return value; try { // Handle nullable types var underlyingType = Nullable.GetUnderlyingType(targetType) ?? targetType; // Convert based on target type if (underlyingType == typeof(string)) { return value.ToString() ?? string.Empty; } else if (underlyingType == typeof(int)) { if (value is int i) return i; if (int.TryParse(value.ToString(), out var parsed)) return parsed; if (value is double d) return (int)d; throw new InvalidCastException($"Cannot convert {value.GetType()} to int"); } else if (underlyingType == typeof(double)) { if (value is double d) return d; if (double.TryParse(value.ToString(), out var parsed)) return parsed; if (value is int i) return i; throw new InvalidCastException($"Cannot convert {value.GetType()} to double"); } else if (underlyingType == typeof(bool)) { if (value is bool b) return b; if (bool.TryParse(value.ToString(), out var parsed)) return parsed; // Handle numeric values: 0/1, "0"/"1", etc. if (value.ToString()?.Trim() == "0" || value.ToString()?.Trim().ToLower() == "false") return false; if (value.ToString()?.Trim() == "1" || value.ToString()?.Trim().ToLower() == "true") return true; throw new InvalidCastException($"Cannot convert {value.GetType()} to bool"); } else if (underlyingType.IsEnum) { var enumValue = Enum.Parse(underlyingType, value.ToString() ?? "", true); return enumValue; } else { // Try standard conversion return Convert.ChangeType(value, underlyingType); } } catch (Exception ex) { _logger.Warning($"Error converting value {value} of type {value.GetType()} to {targetType}: {ex.Message}"); throw; } } }