diff --git a/recipes/ros2_csharp/activate.bat b/recipes/ros2_csharp/activate.bat new file mode 100644 index 00000000..5cd8c17d --- /dev/null +++ b/recipes/ros2_csharp/activate.bat @@ -0,0 +1,6 @@ +@echo off +set AMENT_PREFIX_PATH_BACKUP=%AMENT_PREFIX_PATH% +set AMENT_PREFIX_PATH=%CONDA_PREFIX%\Library +set PATH=%CONDA_PREFIX%\Library\bin;%PATH% +set CMAKE_PREFIX_PATH=%CONDA_PREFIX%\Library;%CMAKE_PREFIX_PATH% +set PYTHONPATH=%CONDA_PREFIX%\Lib\site-packages;%PYTHONPATH% \ No newline at end of file diff --git a/recipes/ros2_csharp/deactivate.bat b/recipes/ros2_csharp/deactivate.bat new file mode 100644 index 00000000..b0b09a13 --- /dev/null +++ b/recipes/ros2_csharp/deactivate.bat @@ -0,0 +1,6 @@ +@echo off +set AMENT_PREFIX_PATH=%AMENT_PREFIX_PATH_BACKUP% +set AMENT_PREFIX_PATH_BACKUP= +set PATH=%PATH:%CONDA_PREFIX%\Library\bin;=% +set CMAKE_PREFIX_PATH=%CMAKE_PREFIX_PATH:%CONDA_PREFIX%\Library;=% +set PYTHONPATH=%PYTHONPATH:%CONDA_PREFIX%\Lib\site-packages;=% \ No newline at end of file diff --git a/recipes/ros2_csharp/meta.yaml b/recipes/ros2_csharp/meta.yaml new file mode 100644 index 00000000..d47566bc --- /dev/null +++ b/recipes/ros2_csharp/meta.yaml @@ -0,0 +1,42 @@ +package: + name: ros2_csharp + version: 0.0.1 + +source: + path: {{ 你的安装打包目录 }} + +build: + number: 0 + script: + # 创建基本目录 + - mkdir %PREFIX%\Library\bin 2>nul + - mkdir %PREFIX%\Library\include 2>nul + - mkdir %PREFIX%\Library\lib 2>nul + - mkdir %PREFIX%\Library\share 2>nul + # 复制文件 + - xcopy /E /I /Y %SRC_DIR%\bin %PREFIX%\Library\bin + - xcopy /E /I /Y %SRC_DIR%\include %PREFIX%\Library\include + - xcopy /E /I /Y %SRC_DIR%\lib %PREFIX%\Library\lib + - xcopy /E /I /Y %SRC_DIR%\share %PREFIX%\Library\share + + # 关键:注册ROS 2包 + - mkdir %PREFIX%\Library\share\ament_index\resource_index\packages 2>nul + - copy %RECIPE_DIR%\ros_driver %PREFIX%\Library\share\ament_index\resource_index\packages\ros_driver + + # 设置环境变量 + - mkdir %PREFIX%\etc\conda\activate.d 2>nul + - mkdir %PREFIX%\etc\conda\deactivate.d 2>nul + # activate脚本 + - copy %RECIPE_DIR%\activate.bat %PREFIX%\etc\conda\activate.d\ros_csharp.bat + - copy %RECIPE_DIR%\deactivate.bat %PREFIX%\etc\conda\deactivate.d\ros_csharp.bat + + +requirements: + host: + run: + +about: + home: https://www.ros.org/ + license: BSD-3-Clause + summary: | + Robot Operating System \ No newline at end of file diff --git a/recipes/ros2_csharp/ros_driver b/recipes/ros2_csharp/ros_driver new file mode 100644 index 00000000..e69de29b diff --git a/ros2_csharp/README.md b/ros2_csharp/README.md new file mode 100644 index 00000000..c632bef9 --- /dev/null +++ b/ros2_csharp/README.md @@ -0,0 +1,38 @@ +## ros2 C# 开发指南 + +## 背景 + 该项目是一个示例项目,帮您快速的配置 C# 开发环境。具体硬件设备驱动需要您自行编写。 + + +## C# 开发环境配置 +### 1. 安装 dotnet 环境 +dotnet [下载地址](https://dotnet.microsoft.com/zh-cn/download) +安装完成后执行一下命令,如果安装成功会有如下展示。 +``` + dotnet --version + 8.0.406 +``` + +### 2. ros2-humble 安装 + 请参照官方[安装文档](https://docs.ros.org/en/humble/Installation/Windows-Install-Binary.html) + +### 3. 开发配置 +1. ros2_dotnet 环境配置请参阅[配置文档](https://github.com/ros2-dotnet/ros2_dotnet/blob/main/README.md)。 +2. 参照项目下的 ros_driver 创建您自己的项目。 +3. 编写相应的代码。 +4. 构建流程 + ``` + 打开 windows 搜索 Developer Command Prompt 启动 visual studio + cd {本地ros2-humble} 目录 + call ./local_setup.bat + cd {Uni-Lab-OS 项目目录}/ros_csharp + vcs import ./src < ros2_humble.repos + colcon build --merge-install --packages-up-to {your package name} --event-handlers console_cohesion+ + ``` +5. 拷贝编译完成的包到conda 对应的虚拟环境目录下。 +6. 执行如下命令运行测试 + + ``` + 参照 ros_driver 项目,需要配置 CONFIG_FILE_PATH 环境变量,对应的值是 {Uni-Lab-OS 路径}/unilabos/registry/devices 下的 yaml 文件路径。 + ros2 run {项目包名} {可执行程序名} + ``` \ No newline at end of file diff --git a/ros2_csharp/ros2_humble.repos b/ros2_csharp/ros2_humble.repos new file mode 100644 index 00000000..d2828134 --- /dev/null +++ b/ros2_csharp/ros2_humble.repos @@ -0,0 +1,33 @@ +repositories: + ament_dotnet/ament_cmake_export_assemblies: + type: git + url: https://github.com/ros2-dotnet/ament_cmake_export_assemblies.git + version: main + ros2/common_interfaces: + type: git + url: https://github.com/ros2/common_interfaces.git + version: 4.2.4 + ros2/rcl_interfaces: + type: git + url: https://github.com/ros2/rcl_interfaces.git + version: 1.2.1 + ros2/rosidl_defaults: + type: git + url: https://github.com/ros2/rosidl_defaults.git + version: 1.2.0 + ros2/test_interface_files: + type: git + url: https://github.com/ros2/test_interface_files.git + version: humble + ros2/unique_identifier_msgs: + type: git + url: https://github.com/ros2/unique_identifier_msgs.git + version: 2.2.1 + ros2_dotnet/dotnet_cmake_module: + type: git + url: https://github.com/ros2-dotnet/dotnet_cmake_module.git + version: main + ros2_dotnet/ros2_dotnet: + type: git + url: https://github.com/ros2-dotnet/ros2_dotnet.git + version: main \ No newline at end of file diff --git a/ros2_csharp/src/ros_driver/CMakeLists.txt b/ros2_csharp/src/ros_driver/CMakeLists.txt new file mode 100644 index 00000000..ee0cf131 --- /dev/null +++ b/ros2_csharp/src/ros_driver/CMakeLists.txt @@ -0,0 +1,65 @@ +cmake_minimum_required(VERSION 3.5) + +project(ros_driver C) + +find_package(ament_cmake REQUIRED) +find_package(rcldotnet REQUIRED) + +find_package(rcldotnet_common REQUIRED) + +find_package(builtin_interfaces REQUIRED) +find_package(std_msgs REQUIRED) +find_package(std_srvs REQUIRED) +find_package(geometry_msgs REQUIRED) +find_package(test_msgs REQUIRED) +find_package(action_msgs REQUIRED) # Add this line +find_package(rcldotnet REQUIRED) +find_package(ilabos_msgs REQUIRED) + + + +find_package(dotnet_cmake_module REQUIRED) + +set(CSHARP_TARGET_FRAMEWORK "netcoreapp6.0") +find_package(DotNETExtra REQUIRED) + + +# 使用相对路径定位 YamlDotNet.dll +set(YAMLDOTNET_DLL "${CMAKE_CURRENT_SOURCE_DIR}/deps/YamlDotNet/YamlDotNet.dll") + +# 检查文件是否存在 +if(NOT EXISTS ${YAMLDOTNET_DLL}) + message(FATAL_ERROR "YamlDotNet.dll not found at: ${YAMLDOTNET_DLL}") +endif() + + +set(_assemblies_dep_dlls + ${rcldotnet_common_ASSEMBLIES_DLL} + ${rcldotnet_ASSEMBLIES_DLL} + ${builtin_interfaces_ASSEMBLIES_DLL} + ${std_msgs_ASSEMBLIES_DLL} + ${std_srvs_ASSEMBLIES_DLL} + ${geometry_msgs_ASSEMBLIES_DLL} + ${test_msgs_ASSEMBLIES_DLL} + ${action_msgs_ASSEMBLIES_DLL} + ${ilabos_msgs_ASSEMBLIES_DLL} + ${YAMLDOTNET_DLL} +) + +add_dotnet_executable(ros_driver + Program.cs + DeviceNode.cs + DeviceActionServer.cs + PropertyPublisher.cs + MockGripper.cs + utils/Yaml.cs + utils/MessageConverter.cs + INCLUDE_DLLS + ${_assemblies_dep_dlls} +) + + +install_dotnet(ros_driver DESTINATION lib/${PROJECT_NAME}/dotnet) + + +ament_package() \ No newline at end of file diff --git a/ros2_csharp/src/ros_driver/DeviceActionServer.cs b/ros2_csharp/src/ros_driver/DeviceActionServer.cs new file mode 100644 index 00000000..907f9ec8 --- /dev/null +++ b/ros2_csharp/src/ros_driver/DeviceActionServer.cs @@ -0,0 +1,133 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using ROS2; +using test_msgs.action; +using System.Collections.Generic; +using ilabos_msgs.action; +using RosDriver.Utils; + +namespace ros_driver +{ + /// + /// Generic action server that can be used with any ROS2 action type + /// + public class DeviceActionServer : IDisposable + where TAction : IRosActionDefinition + where TGoal : IRosMessage, new() + where TResult : IRosMessage, new() + where TFeedback : IRosMessage, new() + { + private readonly Node _node; + private readonly string _actionName; + private bool _disposed = false; + private ActionValueMapping _actionValue = null; + private readonly object _device = null; + + + public DeviceActionServer( + Node node, + object device, + string actionName, + ActionValueMapping actionValue) + { + _node = node; + _actionName = actionName; + _actionValue = actionValue; + _device = device; + + _node.CreateActionServer( + _actionName, + HandleAccepted, + cancelCallback: HandleCancel); + + Console.WriteLine($"Created Action Server: {_actionName}"); + } + + + private void HandleAccepted(ActionServerGoalHandle goalHandle) + { + // Don't block in the callback. + // -> Don't wait for the returned Task. + _ = DoWorkWithGoal(goalHandle); + } + + private CancelResponse HandleCancel(ActionServerGoalHandle goalHandle) + { + return CancelResponse.Accept; + } + + private async Task DoWorkWithGoal(ActionServerGoalHandle goalHandle) + { + + Console.WriteLine("Executing goal..."); + // 检查 TAction 是否是 Fibonacci 类型 + if (typeof(TAction) == typeof(Fibonacci)) + { + Console.WriteLine("确认 TAction 是 Fibonacci 类型"); + // 注意:这种转换是不安全的,因为泛型参数在编译时已确定 + // 这里仅作为示例,实际使用时需要谨慎 + var fibonacciGoalHandle = goalHandle as ActionServerGoalHandle; + if (fibonacciGoalHandle != null) + { + Console.WriteLine("成功转换为 Fibonacci 类型的 GoalHandle"); + var feedback = new Fibonacci_Feedback(); + + feedback.Sequence = new List { 0, 1 }; + + for (int i = 1; i < fibonacciGoalHandle.Goal.Order; i++) + { + if (fibonacciGoalHandle.IsCanceling) + { + var cancelResult = new Fibonacci_Result(); + cancelResult.Sequence = feedback.Sequence; + + Console.WriteLine($"Canceled Result: {string.Join(", ", cancelResult.Sequence)}"); + fibonacciGoalHandle.Canceled(cancelResult); + return; + } + + feedback.Sequence.Add(feedback.Sequence[i] + feedback.Sequence[i - 1]); + + Console.WriteLine($"Feedback: {string.Join(", ", feedback.Sequence)}"); + fibonacciGoalHandle.PublishFeedback(feedback); + + // NOTE: This causes the code to resume in an background worker Thread. + // Consider this when copying code from the example if additional synchronization is needed. + await Task.Delay(1000); + } + + var result = new Fibonacci_Result(); + result.Sequence = feedback.Sequence; + + Console.WriteLine($"Result: {string.Join(", ", result.Sequence)}"); + fibonacciGoalHandle.Succeed(result); + } + } + else + { + Console.WriteLine($"TAction 不是 Fibonacci 类型,而是 {typeof(TAction).Name}"); + return; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + // Release managed resources if any + } + + _disposed = true; + } + } + } +} diff --git a/ros2_csharp/src/ros_driver/DeviceNode.cs b/ros2_csharp/src/ros_driver/DeviceNode.cs new file mode 100644 index 00000000..aca65281 --- /dev/null +++ b/ros2_csharp/src/ros_driver/DeviceNode.cs @@ -0,0 +1,173 @@ +using System; +using System.Threading.Tasks; +using ROS2; +using test_msgs.action; +using System.Collections.Generic; +using RosDriver.Utils; +using System.Reflection; + + +namespace ros_driver +{ + class DeviceNode : IDisposable + { + private readonly Node _node; + private readonly object _device; + private readonly string _name; + private bool _disposed = false; + + private readonly Dictionary _propertyPublishers = new Dictionary(); + private readonly Dictionary _actionServers = new Dictionary(); + + public DeviceNode(Node node, string name, DeviceInfo deviceInfo) + { + _name = name; + _node = node; + + _device = new MockGripper(); + Type dType = _device.GetType(); + PropertyInfo[] properties = dType.GetProperties( + BindingFlags.Public | BindingFlags.Instance); + + var statusTypes = deviceInfo.StatusTypes; + var actionMappings = deviceInfo.ActionValueMappings; + foreach (var prop in properties) + { + MethodInfo setMethod = prop.GetSetMethod(nonPublic: true); + MethodInfo getMethod = prop.GetGetMethod(nonPublic: true); + + // 如果 setMethod 不是 null 打印方法名和返回值名 + if (setMethod != null) + { + ParameterInfo[] parameters = setMethod.GetParameters(); + Type parameterType = parameters[0].ParameterType; + + // 如果返回值是字符串,则actionMappings 添加一个 ActionValueMapping 对象 + if (setMethod.ReturnType == typeof(string)) + { + var actionValueMapping = new ActionValueMapping + { + Type = "SendCmd", + Goal = new Dictionary { { "command", parameterType.Name } }, + Feedback = new Dictionary { { "status", "status" } }, + Result = new Dictionary(), + }; + actionMappings.Add(prop.Name, new ActionValueMapping()); + } + Console.WriteLine($"Property: {prop.Name}, Setter: {setMethod.Name},Parameter Type: {parameterType.Name}, Return Type: {setMethod.ReturnType.Name}"); + } + + // 如果 getMethod 不是 null 打印方法名和返回值名 + if (getMethod != null) + { + // 判断 prop.Name 不在 statusTypes 中,则添加到 statusTypes 中 + if (!statusTypes.ContainsKey(prop.Name)) + { + statusTypes.Add(prop.Name, getMethod.ReturnType.Name); + } + Console.WriteLine($"Property: {prop.Name}, Getter: {getMethod.Name}, Return Type: {getMethod.ReturnType.Name}"); + } + } + + foreach (var kv in statusTypes) + { + Console.WriteLine($"create publisher: {kv.Key}:"); + CreatePublisher(kv.Key, kv.Value); + } + + foreach (var kv in actionMappings) + { + Console.WriteLine($"create action server: {kv.Key}:"); + CreateActionServer(kv.Key, kv.Value); + } + } + + private void CreatePublisher(string key, string msgType) + { + Type rosMessageType = Converter2RosMessage.GetRosMessageType(msgType); + + // Use reflection to create the appropriate publisher type + var publisherType = typeof(PropertyPublisher<>).MakeGenericType(rosMessageType); + var publisher = Activator.CreateInstance(publisherType, _node, _device, key, 1.0, true); + _propertyPublishers.Add(key, publisher); + } + + private void CreateActionServer(string key, ActionValueMapping actionValue) + { + try + { + List actionTypes = Converter2RosMessage.GetActionMessageType(actionValue.Type); + if (actionTypes.Count != 4) + { + throw new ArgumentException($"Expected 4 action types, but got {actionTypes.Count} for action {actionValue.Type}"); + } + + // 提取类型:Action, Goal, Result, Feedback + Type actionType = actionTypes[0]; + Type goalType = actionTypes[1]; + Type resultType = actionTypes[2]; + Type feedbackType = actionTypes[3]; + + // 使用反射创建正确泛型类型的DeviceActionServer + Type deviceActionServerType = typeof(DeviceActionServer<,,,>).MakeGenericType( + actionType, goalType, resultType, feedbackType); + + // 创建DeviceActionServer实例 + object actionServer = Activator.CreateInstance( + deviceActionServerType, + _node, // 节点 + _device, // 设备对象 + key, // 动作名称 + actionValue // 动作值映射 + ); + + // 将动作服务器存储在字典中 + _actionServers.Add(key, actionServer); + + Console.WriteLine($"成功创建了类型为 {actionValue.Type} 的动作服务器 {key}"); + } + catch (Exception ex) + { + Console.WriteLine($"创建动作服务器 {key} 时出错: {ex.Message}"); + if (ex.InnerException != null) + { + Console.WriteLine($"内部异常: {ex.InnerException.Message}"); + } + Console.WriteLine($"这可能是由于消息定义中缺少或不正确的类型支持。"); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + // Node doesn't have a Dispose method, so don't try to call it + // _node.Dispose(); + + // Handle other resources that need to be released + foreach (var publisher in _propertyPublishers.Values) + { + if (publisher is IDisposable disposable) + disposable.Dispose(); + } + + foreach (var server in _actionServers.Values) + { + if (server is IDisposable disposable) + disposable.Dispose(); + } + } + // Dispose unmanaged resources if any + _disposed = true; + } + } + } +} \ No newline at end of file diff --git a/ros2_csharp/src/ros_driver/MockGripper.cs b/ros2_csharp/src/ros_driver/MockGripper.cs new file mode 100644 index 00000000..e709d186 --- /dev/null +++ b/ros2_csharp/src/ros_driver/MockGripper.cs @@ -0,0 +1,73 @@ +using System; +using System.Threading.Tasks; + +public class MockGripper +{ + private double _position = 0.0; + public double Position + { + get + { + _position += 1.0; + return _position; + } + private set + { + _position = value; + } + } + + private double _velocity = 2.0; + public double Velocity + { + get + { + _velocity += 1.0; + return _velocity; + } + private set + { + _velocity = value; + } + } + + private double _torque = 0.0; + public double Torque + { + get + { + _torque += 1.0; + return _torque; + } + private set + { + _torque = value; + } + } + + public string Status { get; private set; } = "Idle"; + + public async Task PushToAsync(double position, double torque, double velocity = 0.0) + { + Status = "Running"; + double currentPos = Position; + + if (velocity == 0.0) + { + velocity = Velocity; + } + + double moveTime = Math.Abs(position - currentPos) / velocity; + + for (int i = 0; i < 20; i++) + { + Position = currentPos + (position - currentPos) / 20 * (i + 1); + Torque = torque / (20 - i); + Velocity = velocity; + await Task.Delay((int)(moveTime * 1000 / 20)); // Convert seconds to milliseconds + } + + Torque = torque; + Status = "Idle"; + } +} \ No newline at end of file diff --git a/ros2_csharp/src/ros_driver/Program.cs b/ros2_csharp/src/ros_driver/Program.cs new file mode 100644 index 00000000..487a54f5 --- /dev/null +++ b/ros2_csharp/src/ros_driver/Program.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using ROS2; +using RosDriver.Utils; + +namespace ros_driver +{ + class Program + { + private static void Main(string[] args) + { + RCLdotnet.Init(); + + // Get device configuration from environment variables + string configFilePath = Environment.GetEnvironmentVariable("CONFIG_FILE_PATH"); + if (string.IsNullOrEmpty(configFilePath)) + { + Console.WriteLine("Please set the CONFIG_FILE_PATH environment variable."); + return; + } + + var deviceDict = YamlConfigParser.GetDeviceInfoDict(configFilePath); + + // 创建列表存储所有节点和设备对象 + List nodes = new List(); + List deviceNodes = new List(); + + foreach (var device in deviceDict) + { + string deviceId = device.Key; + DeviceInfo deviceInfo = device.Value; + string nodeName = deviceId.Split('/').Last(); + string nodeNamespace = $"/devices/{deviceId}"; + + Console.WriteLine($"Starting node '{nodeName}' with namespace '{nodeNamespace}'"); + Console.WriteLine($"Using config file: {configFilePath}"); + + var node = RCLdotnet.CreateNode(nodeName, nodeNamespace); + var deviceNode = new DeviceNode(node, deviceId, deviceInfo); + + // 添加到列表中而不是立即spin + nodes.Add(node); + deviceNodes.Add(deviceNode); + } + + while (RCLdotnet.Ok()) + { + foreach (var node in nodes) + { + RCLdotnet.SpinOnce(node, 100); // 100ms超时 + } + } + + // 清理资源 + foreach (var deviceNode in deviceNodes) + { + deviceNode.Dispose(); + } + + RCLdotnet.Shutdown(); + } + } +} \ No newline at end of file diff --git a/ros2_csharp/src/ros_driver/PropertyPublisher.cs b/ros2_csharp/src/ros_driver/PropertyPublisher.cs new file mode 100644 index 00000000..12ca3286 --- /dev/null +++ b/ros2_csharp/src/ros_driver/PropertyPublisher.cs @@ -0,0 +1,217 @@ +using System; +using System.Threading.Tasks; +using ROS2; +using test_msgs.action; +using System.Reflection; +using RosDriver.Utils; + + + +namespace ros_driver +{ + /// + /// A publisher that periodically publishes property values as ROS messages + /// + public class PropertyPublisher where T : IRosMessage, new() + { + private readonly Node _node; + private readonly string _name; + private readonly bool _printPublish; + private readonly object _device = null; + + private T _value; + private Publisher _publisher; + private Timer _timer; + private TimeSpan _timerPeriod; + + + public PropertyPublisher( + Node node, + object device, + string name, + double initialPeriod = 1.0, + bool printPublish = true) + { + _node = node; + _device = device; + _name = name; + _timerPeriod = TimeSpan.FromSeconds(initialPeriod); + _printPublish = printPublish; + + _publisher = node.CreatePublisher(_name); + _timer = node.CreateTimer(_timerPeriod, PublishProperty); + + Console.WriteLine($"创建发布者 {name}, {typeof(T)}"); + + _value = new T(); + InitializeValue(); + } + + private void InitializeValue() + { + if (_value is std_msgs.msg.String strData) + { + strData.Data = "hello world"; + } + + if (_value is std_msgs.msg.Int32 intData) + { + intData.Data = 1; + } + + if (_value is std_msgs.msg.Float64 floatData) + { + floatData.Data = 1.00f; + } + + if (_value is std_msgs.msg.Float64MultiArray floatArrayData) + { + floatArrayData.Data = new System.Collections.Generic.List { 1.0, 2.0, 3.0, 4.0 }; + floatArrayData.Layout = new std_msgs.msg.MultiArrayLayout(); + } + } + + private bool HasMethod(object obj, string methodName) + { + if (obj == null) + return false; + + Type type = obj.GetType(); + return type.GetMethod(methodName) != null; + } + + // 检查对象是否有特定名称的属性 + private bool HasProperty(object obj, string propertyName) + { + if (obj == null) + return false; + + Type type = obj.GetType(); + return type.GetProperty(propertyName) != null; + } + + public object SafeGetPropertyValue(string propertyName) + { + if (!HasProperty(_device, propertyName)) + { + return null; // 或抛出异常,或返回默认值 + } + + PropertyInfo propInfo = _device.GetType().GetProperty(propertyName); + MethodInfo getMethod = propInfo.GetGetMethod(nonPublic: true); + + if (getMethod == null) + return null; // 没有 getter 方法 + + return getMethod.Invoke(_device, null); + } + + private T getProperty() + { + + var result = SafeGetPropertyValue(_name); + if (result != null) + { + Console.WriteLine($"level: info, Property '{_name}' not found in device '{_device.GetType().Name}', result: {result}"); + return Converter2RosMessage.ConvertToRosMessage(result); + } + + return mockData(); + } + + private T mockData() + { + // Get the full name with namespace + string typeName = typeof(T).FullName; + + if (typeName.EndsWith(".Float64")) + return Converter2RosMessage.ConvertToRosMessage(1.0); + else if (typeName.EndsWith(".String")) + return Converter2RosMessage.ConvertToRosMessage("hello world"); + else if (typeName.EndsWith(".Int32")) + return Converter2RosMessage.ConvertToRosMessage(1); + else if (typeName.EndsWith(".Float64MultiArray")) + return Converter2RosMessage.ConvertToRosMessage(new double[] { 1.0, 2.0, 3.0, 4.0 }); + else + throw new NotSupportedException($"Type {typeName} is not supported."); + } + + + private async Task getPropertyAsync() + { + await Task.Yield(); + var result = SafeGetPropertyValue(_name); + if (result != null) + { + // Console.WriteLine($"level: info, Property '{_name}' found in device '{_device.GetType().Name}', result: {result}"); + return Converter2RosMessage.ConvertToRosMessage(result); + } + return mockData(); + } + + /// + /// Gets the property value synchronously + /// + /// The property value + public T GetProperty() + { + + // If async method is available, run it on a task and wait for result + return Task.Run(async () => await getPropertyAsync()).Result; + + + // // Otherwise call the synchronous method + // return getProperty(); + } + + + /// + /// Publishes the property value + /// + private void PublishProperty(TimeSpan elapsed) + { + try + { + T value = GetProperty(); + if (_printPublish) + { + //_node.Logger.Info($"Publishing {typeof(T).Name}: {value}"); + Console.WriteLine($"level: info,prop name: {_name}, {typeof(T).Name}: {value} normal msg: {Converter2RosMessage.ConvertFromRosMessage(value)}"); + } + + if (value != null) + { + // Console.WriteLine($"level: info, Publishing type: {typeof(T)}, value: {value.Data}"); + _publisher.Publish(value); + } + } + catch (Exception ex) + { + //_node.Logger.Error($"Error in PublishProperty: {ex.Message}\n{ex.StackTrace}"); + Console.WriteLine($"level: error, PublishProperty: {ex.Message}\n{ex.StackTrace}"); + } + } + + + + + + + + /// + /// Changes the frequency of publication + /// + /// New period in seconds + public void ChangeFrequency(double period) + { + _timerPeriod = TimeSpan.FromSeconds(period); + // _node.Logger.Info($"Changing {_name} timer period to: {_timerPeriod.TotalSeconds} seconds"); + Console.WriteLine($"level: info, Changing {_name} timer period to: {_timerPeriod.TotalSeconds} seconds"); + + + // Reset timer + _timer.Cancel(); + _timer = _node.CreateTimer(_timerPeriod, PublishProperty); + } + } +} diff --git a/ros2_csharp/src/ros_driver/deps/YamlDotNet/YamlDotNet.dll b/ros2_csharp/src/ros_driver/deps/YamlDotNet/YamlDotNet.dll new file mode 100644 index 00000000..41ad7208 Binary files /dev/null and b/ros2_csharp/src/ros_driver/deps/YamlDotNet/YamlDotNet.dll differ diff --git a/ros2_csharp/src/ros_driver/package.xml b/ros2_csharp/src/ros_driver/package.xml new file mode 100644 index 00000000..b0f732c6 --- /dev/null +++ b/ros2_csharp/src/ros_driver/package.xml @@ -0,0 +1,38 @@ + + + ros_driver + 0.0.0 + Package containing examples of how to use the rcldotnet API. + Esteve Fernandez + Apache License 2.0 + + ament_cmake + ament_cmake_export_assemblies + + dotnet_cmake_module + rcldotnet_common + rosidl_cmake + + builtin_interfaces + example_interfaces + rcldotnet + std_msgs + std_srvs + sensor_msgs + test_msgs + ilabos_msgs + + rcldotnet_common + builtin_interfaces + example_interfaces + rcldotnet + std_msgs + std_srvs + sensor_msgs + test_msgs + ilabos_msgs + + + ament_cmake + + diff --git a/ros2_csharp/src/ros_driver/utils/MessageConverter.cs b/ros2_csharp/src/ros_driver/utils/MessageConverter.cs new file mode 100644 index 00000000..4afde2b7 --- /dev/null +++ b/ros2_csharp/src/ros_driver/utils/MessageConverter.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using ROS2; +using test_msgs.action; +using builtin_interfaces.msg; +using ilabos_msgs.action; + +namespace RosDriver.Utils +{ + // 静态方法1 C# 根据数据std_msg.msg 类型把数据转 std_msg.msg 类型数据。 + + public static class Converter2RosMessage + { + + // 帮我写一个静态函数,接受一个 string 返回一个 std_msgs.msg 类型,例如接受 String 返回 std_msgs.msg.String 类型,而不是具体的实例 + public static Type GetRosMessageType(string typeName) + { + return typeName switch + { + "String" => typeof(std_msgs.msg.String), + "Int32" => typeof(std_msgs.msg.Int32), + "Float32" => typeof(std_msgs.msg.Float32), + "Float64" => typeof(std_msgs.msg.Float64), + "Double" => typeof(std_msgs.msg.Float64), + "Bool" => typeof(std_msgs.msg.Bool), + "Byte" => typeof(std_msgs.msg.Byte), + "Float64MultiArray" => typeof(std_msgs.msg.Float64MultiArray), + _ => throw new ArgumentException($"Unsupported type name: {typeName}", nameof(typeName)) + }; + } + + // 根据名字返回类型,例如Fibonacci 字符串就返回四个类型 Fibonacci, Fibonacci_Goal, Fibonacci_Result, Fibonacci_Feedback + public static List GetActionMessageType(string typeName) + { + switch (typeName) + { + case "Fibonacci": + return new List { typeof(Fibonacci), typeof(Fibonacci_Goal), typeof(Fibonacci_Result), typeof(Fibonacci_Feedback) }; + case "SendCmd": + return new List { typeof(SendCmd), typeof(SendCmd_Goal), typeof(SendCmd_Result), typeof(SendCmd_Feedback) }; + case "Stir": + return new List { typeof(Stir), typeof(Stir_Goal), typeof(Stir_Result), typeof(Stir_Feedback) }; + case "HeatChill": + return new List { typeof(HeatChill), typeof(HeatChill_Goal), typeof(HeatChill_Result), typeof(HeatChill_Feedback) }; + default: + throw new ArgumentException($"Unsupported type name: {typeName}", nameof(typeName)); + } + } + + + // 新增方法: 使用 switch type 识别不同类型的数据并转换为对应的 std_msgs.msg 类型 + public static T ConvertToRosMessage(object data) where T : IRosMessage, new() + { + T rosMessage = new T(); + + switch (rosMessage) + { + case std_msgs.msg.String stringMsg when data is string strData: + stringMsg.Data = strData; + return rosMessage; + + case std_msgs.msg.Int32 int32Msg when data is int intData: + int32Msg.Data = intData; + return rosMessage; + + case std_msgs.msg.Float32 float32Msg when data is float floatData: + float32Msg.Data = floatData; + return rosMessage; + + case std_msgs.msg.Float64 float64Msg when data is double doubleData: + float64Msg.Data = doubleData; + return rosMessage; + + case std_msgs.msg.Bool boolMsg when data is bool boolData: + boolMsg.Data = boolData; + return rosMessage; + + case std_msgs.msg.Byte byteMsg when data is byte byteData: + byteMsg.Data = byteData; + return rosMessage; + + case std_msgs.msg.Char charMsg when data is char charData: + charMsg.Data = (byte)charData; + return rosMessage; + + case std_msgs.msg.ByteMultiArray byteArrayMsg when data is byte[] byteArrayData: + byteArrayMsg.Data = byteArrayData.ToList(); + byteArrayMsg.Layout = new std_msgs.msg.MultiArrayLayout(); + return rosMessage; + + case std_msgs.msg.Float64MultiArray float64ArrayMsg when data is double[] doubleArrayData: + float64ArrayMsg.Data = doubleArrayData.ToList(); + float64ArrayMsg.Layout = new std_msgs.msg.MultiArrayLayout(); + return rosMessage; + + case std_msgs.msg.Float64MultiArray float64ArrayMsg when data is List doubleListData: + float64ArrayMsg.Data = doubleListData; + float64ArrayMsg.Layout = new std_msgs.msg.MultiArrayLayout(); + return rosMessage; + + case builtin_interfaces.msg.Time timeMsg when data is DateTime dateTimeData: + timeMsg.Sec = (int)(dateTimeData.ToUniversalTime().Subtract(new DateTime(1970, 1, 1))).TotalSeconds; + timeMsg.Nanosec = (uint)(dateTimeData.ToUniversalTime().Subtract(new DateTime(1970, 1, 1))).Milliseconds * 1000000; + return rosMessage; + + case builtin_interfaces.msg.Duration durationMsg when data is TimeSpan timeSpanData: + durationMsg.Sec = (int)timeSpanData.TotalSeconds; + durationMsg.Nanosec = (uint)(timeSpanData.Milliseconds * 1000000); + return rosMessage; + + default: + throw new InvalidOperationException( + $"Cannot convert data of type '{data.GetType().Name}' to ROS message type '{typeof(T).Name}'"); + } + } + + // 将ROS消息转换回C#数据类型的方法 + public static object ConvertFromRosMessage(IRosMessage rosMessage) + { + return rosMessage switch + { + std_msgs.msg.String strMsg => strMsg.Data, + std_msgs.msg.Int32 int32Msg => int32Msg.Data, + std_msgs.msg.Float32 float32Msg => float32Msg.Data, + std_msgs.msg.Float64 float64Msg => float64Msg.Data, + std_msgs.msg.Bool boolMsg => boolMsg.Data, + std_msgs.msg.Byte byteMsg => byteMsg.Data, + std_msgs.msg.Char charMsg => charMsg.Data, + std_msgs.msg.ByteMultiArray byteArrayMsg => byteArrayMsg.Data, + std_msgs.msg.Float64MultiArray float64ArrayMsg => float64ArrayMsg.Data, + builtin_interfaces.msg.Time timeMsg => new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc) + .AddSeconds(timeMsg.Sec) + .AddMilliseconds(timeMsg.Nanosec / 1000000.0), + builtin_interfaces.msg.Duration durationMsg => TimeSpan.FromSeconds(durationMsg.Sec) + .Add(TimeSpan.FromMilliseconds(durationMsg.Nanosec / 1000000.0)), + // 可以根据需要添加更多类型的匹配 + _ => throw new ArgumentException($"Unsupported ROS message type: {rosMessage.GetType().Name}", nameof(rosMessage)) + }; + } + + // 泛型版本,当客户端代码知道期望的返回类型时使用 + public static T ConvertFromRosMessage(IRosMessage rosMessage) + { + object result = ConvertFromRosMessage(rosMessage); + if (result is T typedResult) + { + return typedResult; + } + throw new InvalidCastException($"Cannot convert from {rosMessage.GetType().Name} to {typeof(T).Name}"); + } + } +} \ No newline at end of file diff --git a/ros2_csharp/src/ros_driver/utils/Yaml.cs b/ros2_csharp/src/ros_driver/utils/Yaml.cs new file mode 100644 index 00000000..fceb8880 --- /dev/null +++ b/ros2_csharp/src/ros_driver/utils/Yaml.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; +using YamlDotNet.RepresentationModel; + +namespace RosDriver.Utils +{ + public class ActionValueMapping + { + public string Type { get; set; } + public Dictionary Goal { get; set; } + public Dictionary Feedback { get; set; } + public Dictionary Result { get; set; } + + public ActionValueMapping() + { + Goal = new Dictionary(); + Feedback = new Dictionary(); + Result = new Dictionary(); + } + } + + public class DeviceInfo + { + public Dictionary StatusTypes { get; set; } + public Dictionary ActionValueMappings { get; set; } + + public DeviceInfo() + { + StatusTypes = new Dictionary(); + ActionValueMappings = new Dictionary(); + } + } + + public class YamlConfigParser + { + + private static void ExtractMappingSection(YamlMappingNode parentNode, string sectionName, Dictionary targetDict) + { + var sectionKeyNode = new YamlScalarNode(sectionName); + if (parentNode.Children.ContainsKey(sectionKeyNode)) + { + var sectionNode = parentNode.Children[sectionKeyNode] as YamlMappingNode; + if (sectionNode != null) + { + foreach (var entry in sectionNode.Children) + { + if (entry.Key is YamlScalarNode entryKeyNode && + entry.Value is YamlScalarNode entryValueNode) + { + targetDict[entryKeyNode.Value] = entryValueNode.Value; + } + } + } + } + } + + private static Dictionary ParseDeviceInfo(string filePath) + { + var devices = new Dictionary(); + + using (var reader = new StreamReader(filePath)) + { + var yaml = new YamlStream(); + yaml.Load(reader); + + var rootNode = yaml.Documents[0].RootNode as YamlMappingNode; + if (rootNode == null) + { + throw new InvalidDataException("Root node is not a mapping node."); + } + + foreach (var deviceEntry in rootNode.Children) + { + var deviceKey = ((YamlScalarNode)deviceEntry.Key).Value; + var deviceNode = deviceEntry.Value as YamlMappingNode; + if (deviceNode == null) continue; + + var deviceInfo = new DeviceInfo(); + + // Parse status_types + YamlMappingNode classMappingNode = null; + if (deviceNode.Children.TryGetValue(new YamlScalarNode("class"), out var classNode) && + classNode is YamlMappingNode tempClassNode) + { + classMappingNode = tempClassNode; + if (classMappingNode.Children.TryGetValue(new YamlScalarNode("status_types"), out var statusTypesNode) && + statusTypesNode is YamlMappingNode statusTypesMappingNode) + { + foreach (var statusEntry in statusTypesMappingNode.Children) + { + var key = ((YamlScalarNode)statusEntry.Key).Value; + var value = ((YamlScalarNode)statusEntry.Value).Value; + deviceInfo.StatusTypes[key] = value; + } + } + } + + // Parse action_value_mappings + if (classMappingNode != null && + classMappingNode.Children.TryGetValue(new YamlScalarNode("action_value_mappings"), out var actionMappingsNode) && + actionMappingsNode is YamlMappingNode actionMappingsMappingNode) + { + foreach (var actionEntry in actionMappingsMappingNode.Children) + { + var actionKey = ((YamlScalarNode)actionEntry.Key).Value; + var actionNode = actionEntry.Value as YamlMappingNode; + if (actionNode == null) continue; + + var actionValue = new ActionValueMapping(); + + // Extract type + if (actionNode.Children.TryGetValue(new YamlScalarNode("type"), out var typeNode) && + typeNode is YamlScalarNode typeScalarNode) + { + actionValue.Type = typeScalarNode.Value; + } + + // Extract goal, feedback, result + ExtractMappingSection(actionNode, "goal", actionValue.Goal); + ExtractMappingSection(actionNode, "feedback", actionValue.Feedback); + ExtractMappingSection(actionNode, "result", actionValue.Result); + + deviceInfo.ActionValueMappings[actionKey] = actionValue; + } + } + + devices[deviceKey] = deviceInfo; + } + } + + return devices; + } + + public static Dictionary GetDeviceInfoDict(string filePath) + { + return ParseDeviceInfo(filePath); + } + } +}