Initial commit
This commit is contained in:
@@ -0,0 +1,369 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Service implementation for managing application configurations
|
||||
/// </summary>
|
||||
public class ConnectionConfig : IConnectionConfig
|
||||
{
|
||||
private readonly IConfigManager _configManager;
|
||||
private readonly Logger<ConnectionConfig> _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<ConnectionConfig> 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<VDA5050ProtocolConfig>(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<MQTTConfig>(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<string> 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<T>(List<ConfigVariable> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using RobotNet10.MqttConnection;
|
||||
using RobotNet10.RobotApp.Services.Robot.Connection.Models;
|
||||
|
||||
namespace RobotNet10.RobotApp.Services.ConfigManager;
|
||||
|
||||
/// <summary>
|
||||
/// Service for managing application configurations
|
||||
/// </summary>
|
||||
public interface IConnectionConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Get VDA5050 Protocol configuration
|
||||
/// </summary>
|
||||
VDA5050ProtocolConfig GetVDA5050Config();
|
||||
|
||||
/// <summary>
|
||||
/// Get MQTT configuration
|
||||
/// </summary>
|
||||
MQTTConfig GetMqttConfig();
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
using RobotNet10.RobotApp.Services.Navigation.CSharp;
|
||||
|
||||
namespace RobotNet10.RobotApp.Services.ConfigManager;
|
||||
public interface INavigationConfig
|
||||
{
|
||||
PurePursuitConfig GetPurepursuitConfig();
|
||||
VelocitySignalProcessingConfig GetVelocitySignalProcessingConfig();
|
||||
VelocityEstimatorConfig GetVelocityEstimatorConfig();
|
||||
MotorDynamicsConfig GetMotorDynamicsConfig();
|
||||
PIDConfig GetMovePidConfig();
|
||||
PIDConfig GetRotatePidConfig();
|
||||
Navigation.NavigationConfig GetNavigationConfig();
|
||||
NavigationTune.Shared.Models.StanleyConfig GetStanleyConig();
|
||||
DockToConfig GetDockToConfig();
|
||||
DockToConfig GetMoveStraightConfig();
|
||||
LocalPlannerConfig GetLocalPlannerConfig();
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using RobotNet10.RobotApp.Services.Robot;
|
||||
using RobotNet10.RobotApp.Services.Simulation;
|
||||
|
||||
namespace RobotNet10.RobotApp.Services.ConfigManager;
|
||||
|
||||
/// <summary>
|
||||
/// Service for managing robot configurations
|
||||
/// </summary>
|
||||
public interface IRobotConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Get Robot Physical configuration
|
||||
/// </summary>
|
||||
RobotPhysicalConfig GetRobotPhysicalConfig();
|
||||
|
||||
/// <summary>
|
||||
/// Get Simulation configuration
|
||||
/// </summary>
|
||||
SimulationConfig GetSimulationConfig();
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,456 @@
|
||||
using RobotNet10.CustomConfiguration.Events;
|
||||
using RobotNet10.CustomConfiguration.Models;
|
||||
using RobotNet10.CustomConfiguration.Services;
|
||||
using RobotNet10.RobotApp.Services.Robot;
|
||||
using RobotNet10.RobotApp.Services.Simulation;
|
||||
using RobotNet10.RobotApp.Shared.Enums;
|
||||
using System.Reflection;
|
||||
|
||||
namespace RobotNet10.RobotApp.Services.ConfigManager;
|
||||
|
||||
/// <summary>
|
||||
/// Service implementation for managing robot configurations
|
||||
/// </summary>
|
||||
public class RobotConfiguration : IRobotConfiguration
|
||||
{
|
||||
private readonly IConfigManager _configManager;
|
||||
private readonly Logger<RobotConfiguration> _logger;
|
||||
|
||||
private readonly Lock _lockObject = new();
|
||||
private RobotPhysicalConfig? _robotPhysicalConfig;
|
||||
private SimulationConfig? _simulationConfig;
|
||||
private bool _robotPhysicalConfigLoaded = false;
|
||||
private bool _simulationConfigLoaded = false;
|
||||
|
||||
private const string ROBOT_PHYSICAL_CONFIG_TYPE = "RobotPhysicalConfig";
|
||||
private const string SIMULATION_CONFIG_TYPE = "SimulationConfig";
|
||||
|
||||
public RobotConfiguration(IConfigManager configManager, Logger<RobotConfiguration> logger)
|
||||
{
|
||||
_configManager = configManager ?? throw new ArgumentNullException(nameof(configManager));
|
||||
_logger = logger;
|
||||
|
||||
// Subscribe to config changes
|
||||
_configManager.ConfigChanged += OnConfigChanged;
|
||||
}
|
||||
|
||||
public RobotPhysicalConfig GetRobotPhysicalConfig()
|
||||
{
|
||||
if (!_robotPhysicalConfigLoaded)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
if (!_robotPhysicalConfigLoaded)
|
||||
{
|
||||
LoadRobotPhysicalConfigAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
return _robotPhysicalConfig ?? throw new InvalidOperationException("Robot Physical configuration not loaded");
|
||||
}
|
||||
|
||||
public SimulationConfig GetSimulationConfig()
|
||||
{
|
||||
if (!_simulationConfigLoaded)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
if (!_simulationConfigLoaded)
|
||||
{
|
||||
LoadSimulationConfigAsync().GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
return _simulationConfig ?? throw new InvalidOperationException("Simulation configuration not loaded");
|
||||
}
|
||||
|
||||
private async Task LoadRobotPhysicalConfigAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var configFile = await _configManager.GetConfigByTypeAsync(ROBOT_PHYSICAL_CONFIG_TYPE);
|
||||
|
||||
if (configFile == null)
|
||||
{
|
||||
_logger.Warning("Robot Physical configuration not found, using defaults");
|
||||
_robotPhysicalConfig = new RobotPhysicalConfig
|
||||
{
|
||||
WheelRadius = 0.1,
|
||||
WheelBase = 0.6,
|
||||
Width = 0.606,
|
||||
Length = 1.106,
|
||||
Height = 0.5,
|
||||
NavigationType = NavigationType.Differential
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
_robotPhysicalConfig = MapConfigVariablesToRobotPhysicalConfig(configFile.Variables);
|
||||
_logger.Info("Robot Physical configuration loaded successfully");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error loading Robot Physical configuration, using defaults: {ex.Message}");
|
||||
_robotPhysicalConfig = new RobotPhysicalConfig
|
||||
{
|
||||
WheelRadius = 0.1,
|
||||
WheelBase = 0.6,
|
||||
Width = 0.606,
|
||||
Length = 1.106,
|
||||
Height = 0.5,
|
||||
NavigationType = NavigationType.Differential
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
_robotPhysicalConfigLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadSimulationConfigAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var configFile = await _configManager.GetConfigByTypeAsync(SIMULATION_CONFIG_TYPE);
|
||||
|
||||
if (configFile == null)
|
||||
{
|
||||
_logger.Warning("Simulation configuration not found, using defaults");
|
||||
_simulationConfig = new SimulationConfig
|
||||
{
|
||||
IsEnable = false,
|
||||
MaxVelocity = 1.5,
|
||||
MaxAngularVelocity = 0.5,
|
||||
Acceleration = 2.0,
|
||||
Deceleration = 10.0
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
_simulationConfig = MapConfigVariablesToObject<SimulationConfig>(configFile.Variables);
|
||||
_logger.Info("Simulation configuration loaded successfully");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error($"Error loading Simulation configuration, using defaults: {ex.Message}");
|
||||
_simulationConfig = new SimulationConfig
|
||||
{
|
||||
IsEnable = false,
|
||||
MaxVelocity = 1.5,
|
||||
MaxAngularVelocity = 0.5,
|
||||
Acceleration = 2.0,
|
||||
Deceleration = 10.0
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
_simulationConfigLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnConfigChanged(object? sender, ConfigChangedEventArgs e)
|
||||
{
|
||||
// Reload config if it's one of our config types
|
||||
if (e.ConfigType == ROBOT_PHYSICAL_CONFIG_TYPE)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
_robotPhysicalConfigLoaded = false;
|
||||
_robotPhysicalConfig = null;
|
||||
}
|
||||
_logger.Info("Robot Physical configuration changed, will reload on next access");
|
||||
}
|
||||
else if (e.ConfigType == SIMULATION_CONFIG_TYPE)
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
_simulationConfigLoaded = false;
|
||||
_simulationConfig = null;
|
||||
}
|
||||
_logger.Info("Simulation configuration changed, will reload on next access");
|
||||
}
|
||||
}
|
||||
|
||||
private RobotPhysicalConfig MapConfigVariablesToRobotPhysicalConfig(List<ConfigVariable> variables)
|
||||
{
|
||||
var config = new RobotPhysicalConfig();
|
||||
|
||||
// Get properties with reflection to handle private setters
|
||||
// Use BindingFlags to get properties with private setters
|
||||
var properties = typeof(RobotPhysicalConfig).GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
||||
|
||||
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(config, convertedValue);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning($"Failed to set property {property.Name} from variable {variable.Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
private T MapConfigVariablesToObject<T>(List<ConfigVariable> 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;
|
||||
}
|
||||
// Handle Dictionary types (e.g., Dictionary<SafetySpeed, double>)
|
||||
else if (targetType.IsGenericType && targetType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
|
||||
{
|
||||
return ConvertToDictionary(value, targetType);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert value to Dictionary<TKey, TValue>
|
||||
/// Handles conversion from Dictionary<string, object> (JSON) to typed dictionaries
|
||||
/// Supports enum keys (e.g., Dictionary<SafetySpeed, double>)
|
||||
/// </summary>
|
||||
private object ConvertToDictionary(object value, Type targetDictionaryType)
|
||||
{
|
||||
// Get key and value types from Dictionary<TKey, TValue>
|
||||
var genericArgs = targetDictionaryType.GetGenericArguments();
|
||||
var keyType = genericArgs[0];
|
||||
var valueType = genericArgs[1];
|
||||
|
||||
// Create dictionary instance
|
||||
var dictionaryType = typeof(Dictionary<,>).MakeGenericType(keyType, valueType);
|
||||
var dictionary = Activator.CreateInstance(dictionaryType);
|
||||
var addMethod = dictionaryType.GetMethod("Add") ?? throw new InvalidOperationException("Cannot find Add method on Dictionary");
|
||||
|
||||
// Handle source Dictionary<string, object> from JSON
|
||||
if (value is Dictionary<string, object> sourceDict)
|
||||
{
|
||||
foreach (var kvp in sourceDict)
|
||||
{
|
||||
// Convert key (usually from string to enum)
|
||||
object convertedKey;
|
||||
if (keyType.IsEnum)
|
||||
{
|
||||
// Parse enum from string
|
||||
convertedKey = Enum.Parse(keyType, kvp.Key, ignoreCase: true);
|
||||
}
|
||||
else if (keyType == typeof(string))
|
||||
{
|
||||
convertedKey = kvp.Key;
|
||||
}
|
||||
else
|
||||
{
|
||||
convertedKey = Convert.ChangeType(kvp.Key, keyType);
|
||||
}
|
||||
|
||||
// Convert value
|
||||
object? convertedValue = ConvertDictionaryValue(kvp.Value, valueType);
|
||||
|
||||
// Add to dictionary
|
||||
addMethod.Invoke(dictionary, [convertedKey, convertedValue]);
|
||||
}
|
||||
|
||||
return dictionary ?? new Dictionary<object, object>();
|
||||
}
|
||||
// Handle JsonElement (alternative JSON representation)
|
||||
else if (value is System.Text.Json.JsonElement jsonElement && jsonElement.ValueKind == System.Text.Json.JsonValueKind.Object)
|
||||
{
|
||||
foreach (var property in jsonElement.EnumerateObject())
|
||||
{
|
||||
// Convert key
|
||||
object convertedKey;
|
||||
if (keyType.IsEnum)
|
||||
{
|
||||
convertedKey = Enum.Parse(keyType, property.Name, ignoreCase: true);
|
||||
}
|
||||
else if (keyType == typeof(string))
|
||||
{
|
||||
convertedKey = property.Name;
|
||||
}
|
||||
else
|
||||
{
|
||||
convertedKey = Convert.ChangeType(property.Name, keyType);
|
||||
}
|
||||
|
||||
// Convert value from JsonElement
|
||||
object? convertedValue = ConvertJsonElementValue(property.Value, valueType);
|
||||
|
||||
// Add to dictionary
|
||||
addMethod.Invoke(dictionary, [convertedKey, convertedValue]);
|
||||
}
|
||||
|
||||
return dictionary ?? new Dictionary<object, object>();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidCastException($"Cannot convert {value.GetType()} to {targetDictionaryType}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert dictionary value to target type
|
||||
/// </summary>
|
||||
private object? ConvertDictionaryValue(object? value, Type valueType)
|
||||
{
|
||||
if (value == null)
|
||||
return valueType.IsValueType ? Activator.CreateInstance(valueType) : null;
|
||||
|
||||
if (valueType == 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 (double)i;
|
||||
return Convert.ChangeType(value, valueType);
|
||||
}
|
||||
else if (valueType == 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;
|
||||
return Convert.ChangeType(value, valueType);
|
||||
}
|
||||
else if (valueType == typeof(string))
|
||||
{
|
||||
return value.ToString() ?? string.Empty;
|
||||
}
|
||||
else if (valueType == typeof(bool))
|
||||
{
|
||||
if (value is bool b) return b;
|
||||
if (bool.TryParse(value.ToString(), out var parsed)) return parsed;
|
||||
return Convert.ChangeType(value, valueType);
|
||||
}
|
||||
else if (valueType.IsEnum)
|
||||
{
|
||||
return Enum.Parse(valueType, value.ToString() ?? "", ignoreCase: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Convert.ChangeType(value, valueType);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert JsonElement value to target type
|
||||
/// </summary>
|
||||
private object? ConvertJsonElementValue(System.Text.Json.JsonElement element, Type valueType)
|
||||
{
|
||||
if (element.ValueKind == System.Text.Json.JsonValueKind.Null)
|
||||
return valueType.IsValueType ? Activator.CreateInstance(valueType) : null;
|
||||
|
||||
if (valueType == typeof(double))
|
||||
{
|
||||
return element.GetDouble();
|
||||
}
|
||||
else if (valueType == typeof(int))
|
||||
{
|
||||
return element.GetInt32();
|
||||
}
|
||||
else if (valueType == typeof(string))
|
||||
{
|
||||
return element.GetString();
|
||||
}
|
||||
else if (valueType == typeof(bool))
|
||||
{
|
||||
return element.GetBoolean();
|
||||
}
|
||||
else if (valueType.IsEnum)
|
||||
{
|
||||
return Enum.Parse(valueType, element.GetString() ?? "", ignoreCase: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
var rawValue = element.GetRawText();
|
||||
return System.Text.Json.JsonSerializer.Deserialize(rawValue, valueType);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user