Ubuntu22.04+ROS2中实现Moveit2控制gazebo中机械臂,rviz和gazebo联动 ...

打印 上一主题 下一主题

主题 806|帖子 806|积分 2418

目录
1 准备工作
1.1 安装工具创建工作空间
1.2 有一份描述机械臂的urdf文件(xacro文件也可)
1.3 在Moveit中举行一系列设置
2 文件创建及修改
 2.1 Moveit设置文件下的launch文件夹
2.1.1 gazebo.launch.py
2.1.2 my_moveit_rviz.launch.py
2.2 Moveit设置文件下的config文件夹 
2.3 机器人描述文件中urdf文件夹
2.3.1 dummy-ros2.gazebo
2.3.2 dummy-ros2.trans
2.3.3 dummy-ros2.xacro
3 运行文件验证效果
       本人零基础开始的ROS学习,写这篇日记主要防我这个头脑干完就忘下次再弄又是啥也不会,本人前期用过的环境有Ubuntu18.04+ROS1版本的虚拟机,现在使用的是ubuntu22.04+ROS2 humble版本,想研究和学习机械臂相关的内容,手头上购入了木子大佬复刻的稚晖君开源的dummy机械臂第一版(6轴机械臂),现在短期目标是想实现在仿真环境中在机械臂上搭载相机,通过目标检测算法(初步考虑YOLO系列)实现对目标的辨认及规划机械臂运动实现抓取。

       考虑到要使用显卡而虚拟机只能完成一些简单的机械臂设置工作(而且在虚拟机上举行联动仿真时不知道是什么原因导致rviz和Gazebo非常卡顿,网上有相应的一些办理办法,试过之后也不绝没有办理),所以虚拟机只能是前期学习时用一用,对此纠结过两个方案:
1.之前考虑过装双系统,如许的好处就是东西都在电脑里不用外接啥东西,不用考虑数据传输的速率啥的(暂时只知道这些个好处吧,知道一点但不多),但电脑现在的内存仅允许分出不到300g内存用于Linux系统安装(固然给你的工具扩容也是个不戳的办法,但是电脑买来不久还在保修期本身加装要拆机搞坏了不保修我就要哭了,对电子装备一点不敏感的妹纸一个),加之本身对Linux打仗过但不熟悉,是一个常常不考虑版本兼容问题安装各种包(linux版本兼容问题真的很逆天呜呜呜),而且乱用不熟悉的下令在在linux系统“搞破坏”的菜菜,所以拥有备份对我来说很重要,相当于打游戏时给本身设置一些节点存档,省的被玩坏从头开始进度清零(别问!问就是干过不少);
2.然后就是我现在采用的plan B,用移动固态硬盘装系统(用的是1T的,大点好谁知道后面要干什么占位置的活呢),网上也有相应的很多教程,本人用了鱼香ROS老师的一键安装ubuntu22.04(太友爱了,fishstall一下还能装好些其他东西呢,里面的一键换源简直我心头爱),这种方法的好处就是一块移动固态硬盘可以实现随插随用,可以实现轻松备份,但是要给它一根好点的线喏,不然可能随机出现幸运用户在读取数据时可能由于数据线松动造成扇区损坏硬盘直接酿成废品(我我我!就是谁人幸运用户......)
1 准备工作

1.1 安装工具创建工作空间

       前面碎碎念了很久,现在委曲进入学前班阶段,要实现Moveit控制gazebo中的机械臂,rviz和gazebo联动的第一步就是拥有这些工具,鱼香ros老师的一键安装ros好像就把这些东西全都安装上了,省时省力省头脑,安装好了之后创建本身的ROS工作空间。
1.2 有一份描述机械臂的urdf文件(xacro文件也可)

1.3 在Moveit中举行一系列设置

       之前使用ubuntu18.04+ROS的时间,在solidworks中对机械臂举行装配后可以直接导出这个urdf文件,然后在moveit中举行一系列设置就能天生机器人描述文件和Moveit设置文件,但现在换到了ubuntu22.04+ROS2,听网友说solidwoks里导出来的文件仅实用于ROS1,假如要在ROS2中使用的话要举行一些修改(好像确实是用不起来,后来找到了木子大佬开源的新文件就没再去研究,),你说巧不巧就在一个多月从前木子大佬在他的堆栈里开源了dummy-ros2_description(据了解这个机器人描述文件是Fusion 360导出来的)和dummy_moveit_config文件,这不仅不必要本身去装配机器人获取描述文件了,连Moveit设置文件也一并搞定了(简直等等党的春天,大佬多累一点我就能少干一点呜呜呜),然后把这两个文件放入1.1创建的工作空间中。
2 文件创建及修改

       拿到机器人描述文件(用的木子写好的文件名为dummy-ros2_description)和Moveit设置文件(文件名为dummy_moveit_config)后举行一些文件的创建及修改,固然这个地方拿到本身对应的其他机器人描述文件,再举行Moveit设置天生文件之后举行创建修改也可以做为参考,注意一下每个文档里的文件名字和路径对应一致就好啦,要改的部门主要在三个文件夹里,我把颠末创建和修改文件后我的这三个文件夹里的文件构成贴图在下面啦~

 机器人描述文件中urdf文件夹


Moveit设置文件下的config文件夹 


Moveit设置文件下的launch文件夹

 2.1 Moveit设置文件下的launch文件夹

        还未创建和修改前该文件夹下的文件是不包含gazebo.launch.py文件(gazebo启动文件)和my_moveit_rviz.launch.py文件(rviz和moveit启动文件)的,所以现在要做的就是创建这两个文本文件,这一步是实现Moveit控制gazebo中机器人且实现rviz和gazebo联动的关键也是很容易出错误的一个地方,文件写好后都可以分别ros2 launch启动看看有没有报错大概警告,以便实时发现问题:

2.1.1 gazebo.launch.py

        是gazebo的启动文件,里面包含的主要内容有:1.启动gazebo服务; 2.启动robot_state_publisher节点发布robot_description话题,订阅joint_state;3.启动controller_manager加载控制器,加载控制管理各个控制器;4.启动spawn_entity_cmd订阅robot_description在gazabo中天生模型;5.启动joint_state_controller节点(关节状态发布器),启动joint_state;6.启动joint_trajectory_controller节点(路径执行控制器命名为dummy_arm_controller)。文件具体内容如下,直接拿去用的小伙伴要查抄一下文件名字、文件路径、控制器名字跟本身设置的是否一致哦!!!
  1. import os
  2. from launch import LaunchDescription
  3. from launch.actions import ExecuteProcess, RegisterEventHandler
  4. from launch_ros.actions import Node
  5. from launch_ros.substitutions import FindPackageShare
  6. from launch.substitutions import PathJoinSubstitution
  7. from launch.event_handlers import OnProcessExit
  8. import xacro
  9. import re
  10. def remove_comments(text):
  11.     pattern = r'<!--(.*?)-->'
  12.     return re.sub(pattern, '', text, flags=re.DOTALL)
  13. def generate_launch_description():
  14.     robot_name_in_model = 'dummy-ros2'
  15.     package_name = 'dummy-ros2_description'
  16.     urdf_name = "dummy-ros2.xacro"
  17.     pkg_share = FindPackageShare(package=package_name).find(package_name)
  18.     urdf_model_path = os.path.join(pkg_share, f'urdf/{urdf_name}')
  19.     dummy_moveit_config = FindPackageShare(package='dummy_moveit_config').find('dummy_moveit_config')
  20.     controller_config = PathJoinSubstitution(
  21.         [dummy_moveit_config, 'config', 'ros2_controllers.yaml']
  22.     )
  23.    
  24.     # Start Gazebo server
  25.     start_gazebo_cmd =  ExecuteProcess(
  26.         cmd=['gazebo', '--verbose','-s', 'libgazebo_ros_init.so', '-s', 'libgazebo_ros_factory.so'],
  27.         output='screen')
  28.     # 因为 urdf文件中有一句 $(find dummy_moveit_config) 需要用xacro进行编译一下才行
  29.     xacro_file = urdf_model_path
  30.     doc = xacro.parse(open(xacro_file))
  31.     xacro.process_doc(doc)
  32.     # params = {'robot_description': doc.toxml()}
  33.     params = {'robot_description': remove_comments(doc.toxml())}
  34.     # 启动了robot_state_publisher节点后,该节点会发布 robot_description 话题,话题内容是模型文件urdf的内容
  35.     # 并且会订阅 /joint_states 话题,获取关节的数据,然后发布tf和tf_static话题.
  36.     robot_state_publisher = Node(
  37.         package='robot_state_publisher',
  38.         executable='robot_state_publisher',
  39.         parameters=[{'use_sim_time': True}, params, {"publish_frequency":15.0}],
  40.         output='screen'
  41.     )
  42.     # 非必要,只是之前在修改的过程中验证的时候老是出现找不到控制器配置文件ros2_controller.yaml就这这里再加了一点,但根本原因不在于没加这一段但是懒得删了
  43.     controller_manager = Node(
  44.         package='controller_manager',
  45.         executable='ros2_control_node',
  46.         parameters=[controller_config],
  47.         output='screen'
  48.     )  
  49.     # Launch the robot, 通过robot_description话题进行模型内容获取从而在gazebo中生成模型
  50.     spawn_entity_cmd = Node(
  51.         package='gazebo_ros',
  52.         executable='spawn_entity.py',
  53.         arguments=['-entity', robot_name_in_model,  '-topic', 'robot_description'], output='screen')
  54.     # gazebo在加载urdf时,根据urdf的设定,会启动一个joint_states节点
  55.     # 关节状态发布器
  56.     load_joint_state_controller = ExecuteProcess(
  57.         cmd=['ros2', 'control', 'load_controller', '--set-state', 'active', 'joint_state_broadcaster'],
  58.         output='screen'
  59.     )
  60.     # 路径执行控制器,这个控制器名字不一样的要注意进行更换哦,控制器名字可以在类似ros2_controllers.yaml的文件里找到
  61.     load_joint_trajectory_controller = ExecuteProcess(
  62.         cmd=['ros2', 'control', 'load_controller', '--set-state', 'active', 'dummy_arm_controller'],
  63.         output='screen'
  64.     )
  65.     # 控制好各个节点的启动顺序
  66.     close_evt1 =  RegisterEventHandler(
  67.             event_handler=OnProcessExit(
  68.                 target_action=spawn_entity_cmd,
  69.                 on_exit=[load_joint_state_controller],
  70.             )
  71.     )
  72.     # 监听 load_joint_state_controller
  73.     close_evt2 = RegisterEventHandler(
  74.             event_handler=OnProcessExit(
  75.                 target_action=load_joint_state_controller,
  76.                 on_exit=[load_joint_trajectory_controller],
  77.             )
  78.     )
  79.    
  80.     ld = LaunchDescription()
  81.     ld.add_action(close_evt1)
  82.     ld.add_action(close_evt2)
  83.     ld.add_action(start_gazebo_cmd)
  84.     ld.add_action(robot_state_publisher)
  85.     ld.add_action(controller_manager)
  86.     ld.add_action(spawn_entity_cmd)
  87.    
  88.    
  89.     return ld
复制代码
2.1.2 my_moveit_rviz.launch.py

         是加载Moveit设置和启动rviz的文件,该文件的主要内容有:1.moveit设置;2.启动moveit中move_group节点处理运动规划与执行;3.启动rviz并加载moveit设置。文件具体内容如下,这里同样也必要查抄并更改对应文件的名字。
  1. from moveit_configs_utils import MoveItConfigsBuilder
  2. from moveit_configs_utils.launches import generate_moveit_rviz_launch
  3. from launch import LaunchDescription
  4. from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
  5. from moveit_configs_utils.launch_utils import add_debuggable_node, DeclareBooleanLaunchArg
  6. from launch.substitutions import LaunchConfiguration
  7. from launch_ros.actions import Node
  8. from launch_ros.parameter_descriptions import ParameterValue
  9. from ament_index_python.packages import get_package_share_directory
  10. def generate_launch_description():
  11.   
  12.     # MoveIt 配置
  13.     moveit_config = MoveItConfigsBuilder("dummy-ros2", package_name="dummy_moveit_config").to_moveit_configs()
  14.     ld = LaunchDescription()
  15.     # 启动 MoveIt 中的 move_group 节点
  16.     my_generate_move_group_launch(ld, moveit_config)
  17.     # 启动 RViz 并加载 MoveIt 配置
  18.     my_generate_moveit_rviz_launch(ld, moveit_config)
  19.     return ld
  20. def my_generate_move_group_launch(ld, moveit_config):
  21.     ld.add_action(DeclareBooleanLaunchArg("debug", default_value=False))
  22.     ld.add_action(
  23.         DeclareBooleanLaunchArg("allow_trajectory_execution", default_value=True)
  24.     )
  25.     ld.add_action(
  26.         DeclareBooleanLaunchArg("publish_monitored_planning_scene", default_value=True)
  27.     )
  28.     # load non-default MoveGroup capabilities (space separated)
  29.     ld.add_action(DeclareLaunchArgument("capabilities", default_value=""))
  30.     # inhibit these default MoveGroup capabilities (space separated)
  31.     ld.add_action(DeclareLaunchArgument("disable_capabilities", default_value=""))
  32.     # do not copy dynamics information from /joint_states to internal robot monitoring
  33.     # default to false, because almost nothing in move_group relies on this information
  34.     ld.add_action(DeclareBooleanLaunchArg("monitor_dynamics", default_value=False))
  35.    
  36.     should_publish = LaunchConfiguration("publish_monitored_planning_scene")
  37.     move_group_configuration = {
  38.         "robot_description": moveit_config.robot_description,
  39.         "robot_description_semantic": moveit_config.robot_description_semantic,
  40.         "publish_robot_description_semantic": True,
  41.         "allow_trajectory_execution": LaunchConfiguration("allow_trajectory_execution"),
  42.         "capabilities": ParameterValue(LaunchConfiguration("capabilities"), value_type=str),  # 使用 capabilities 参数
  43.         "disable_capabilities": ParameterValue(LaunchConfiguration("disable_capabilities"), value_type=str),
  44.   
  45.         # Publish the planning scene of the physical robot so that rviz plugin can know actual robot
  46.         "publish_planning_scene": should_publish,
  47.         "publish_geometry_updates": should_publish,
  48.         "publish_state_updates": should_publish,
  49.         "publish_transforms_updates": should_publish,
  50.         "monitor_dynamics": False,
  51.   }
  52.     move_group_params = [moveit_config.to_dict(), move_group_configuration]
  53.     move_group_params.append({"use_sim_time": True})
  54.     add_debuggable_node(
  55.         ld,
  56.         package="moveit_ros_move_group",
  57.         executable="move_group",
  58.         parameters=move_group_params,
  59.         output="screen",
  60.         extra_debug_args=["--debug"] if LaunchConfiguration("debug") == "true" else []
  61.     )
  62. def my_generate_moveit_rviz_launch(ld, moveit_config):
  63.    
  64.     ld.add_action(DeclareBooleanLaunchArg("debug", default_value=False))
  65.     ld.add_action(
  66.         DeclareLaunchArgument(
  67.             "rviz_config",
  68.             default_value=str(moveit_config.package_path / "config/moveit.rviz"),
  69.         )
  70.     )
  71.    
  72.     rviz_parameters = [
  73.         moveit_config.planning_pipelines,
  74.         moveit_config.robot_description_kinematics,
  75.         {"use_sim_time": True}
  76.     ]
  77.    
  78.     add_debuggable_node(
  79.         ld,
  80.         package="rviz2",
  81.         executable="rviz2",
  82.         arguments=['-d', LaunchConfiguration("rviz_config")],
  83.         parameters=rviz_parameters,
  84.         output="log"
  85.     )
复制代码
2.2 Moveit设置文件下的config文件夹 

       这个文件夹里的内容只必要举行查抄(主要是前三个),假如发现不对应的情况才必要举行修改,首先让我们来看一看前面提到过的控制器设置文件(ros2_controllers.yaml)

       下面是颠末Moveit设置后天生的该文件的具体内容,还没弄清楚他的编写规则之前不要容易改动里面的内容和格式!!!它的书写规则是参数嵌套书写(谁能想到由于它的格式缩进问题卡了我三天,在其他文件里各种查抄修改给我花式报错和警告就是没注意到它),简单解释一下这个文件里的内容吧,controller_manager是全部控制器的老大,用于加载、设置和管理各个控制器,它下面一级的参数就包含dummy_arm_controller和joint_state_broadcaster两个控制器(名字不一样不要紧,只要能跟其他文件对应上就没事),而dummy_arm_controller下又有joints、command_interfaces、state_interfaces三个参数(硬件接口),这三个参数下又对应嵌套着一些参数,这个机械臂设置是对位置控制的所以在command_interfaces中仅写了position一种,固然还有力矩控制(effort)等其他参数范例可以往里加,joint_state_broadcaster也是一样的原理,这里只对位置和速率做状态信息输出。(注意:看清楚里面的硬件接口是什么内容哦,后面临xacro文件的添加和修改有多处必要对应!!!)
  1. controller_manager:
  2.   ros__parameters:
  3.     update_rate: 100
  4.    
  5.     dummy_arm_controller:
  6.       type: joint_trajectory_controller/JointTrajectoryController
  7.     joint_state_broadcaster:
  8.       type: joint_state_broadcaster/JointStateBroadcaster
  9. dummy_arm_controller:
  10.   ros__parameters:
  11.     joints:
  12.       - Joint1
  13.       - Joint2
  14.       - Joint3
  15.       - Joint4
  16.       - Joint5
  17.       - Joint6
  18.     command_interfaces:
  19.       - position  
  20.     state_interfaces:
  21.       - position
  22.       - velocity
复制代码
避雷题外话:这个时间好奇宝宝可能会问dummy_arm_controller下的参数能不能直接挨着它的type下面和type同级举行嵌套书写如许更简便(我的gpt好朋侪就是如许告诉我的),敲黑板!!!不可以!!!犟种在启动gazebo.launch.py文件时只会收到下面的报错和一个逐渐瘫软的机械臂(由于只启动了状态发布控制节点但没启动控制节点):

2.3 机器人描述文件中urdf文件夹

       这个文件夹下都是跟机器人描述相关的文件啦,跟之前用过的描述文件对比起来给我的感觉就是这里把文件拆开分部门放置更方便找到差别部门的代码,固然里面跟ROS1对机器人描述的文件写法差别的地方有必要可以本身去对比研究一下,主要修改的地方是下面红框框里的三个文件,第一个是针对gazebo仿真部门的代码,第二个是有关传动设置的代码,第三个是对机器人关节连杆关系的代码,dummy-ros2.xacro文件里包含了其他三个文件。

2.3.1 dummy-ros2.gazebo

       在这个文件里要添加针对gazebo仿真的ros2控制的插件内容,文件名和路径差别的要举行相应的修改,libgazebo_ros2_control.so要记得是ros2不是ros,这个文件开始开始打开的时间好像写的是ros记得不太清楚了,多检检察一下总没错,这个部门很重要,控制器设置文件ros2_controllers.yaml应该就是从这里写进去的,刚开始没写的时间老跟说找不着这个文件,要添加的代码内容如下所示:
  1. <gazebo>
  2.       <plugin filename="libgazebo_ros2_control.so" name="gazebo_ros2_control">
  3.             <parameters>$(find dummy_moveit_config)/config/ros2_controllers.yaml</parameters>
  4.             <robot_param_node>robot_state_publisher</robot_param_node>
  5.             <robotParam>robot_description</robotParam>
  6.       </plugin>
  7. </gazebo>
复制代码
2.3.2 dummy-ros2.trans

       这个文件是对传动的设置,要跟控制器设置文件中command_interfaces下的参数对应起来,由于里面是对六个关节做相同的设置,所以这里就放一个关节的代码作为例子,其他关节把名字改一改就行了,要举行修改的地方是<hardwareInterface>标签里的内容,最初文件里这个标签的内容写的是EffortJointInterface即力矩控制,而command_interfaces里写的参数是position,所以将内容都修改成PositionJointInterface即可:
  1. <transmission name="transmission_joint1">
  2.     <type>transmission_interface/SimpleTransmission</type>
  3.     <joint name="Joint1">
  4.       <hardwareInterface>PositionJointInterface</hardwareInterface>
  5.     </joint>
  6.     <actuator name="actuator_joint1">
  7.       <hardwareInterface>PositionJointInterface</hardwareInterface>
  8.     </actuator>
  9.   </transmission>
复制代码
2.3.3 dummy-ros2.xacro

       这个文件也必要添加部门有关ros2_control标签内容,与控制器设置文件中设置的硬件接口对应起来,即对每一个关节都写入command_interface和state_interface的内容,同时这里还规定了机械臂的初始位置即每个关节均为0.0,这个添加的部门跟dummy_moveit_config文件夹下config文件夹里dummy-ros2_control.xacro文件的内容高度重合,必要添加的具体内容如下所示:
  1. <ros2_control name="GazeboSystem" type="system">
  2.         <hardware>
  3.             <plugin>gazebo_ros2_control/GazeboSystem</plugin>
  4.         </hardware>
  5.         <joint name="Joint1">
  6.             <command_interface name="position"/>
  7.             <state_interface name="position">
  8.                 <param name="initial_value">0.0</param>
  9.             </state_interface>
  10.             <state_interface name="velocity"/>
  11.         </joint>
  12.         <joint name="Joint2">
  13.             <command_interface name="position"/>
  14.             <state_interface name="position">
  15.                 <param name="initial_value">0.0</param>
  16.             </state_interface>
  17.             <state_interface name="velocity"/>
  18.         </joint>
  19.         <joint name="Joint3">
  20.             <command_interface name="position"/>
  21.             <state_interface name="position">
  22.                 <param name="initial_value">0.0</param>
  23.             </state_interface>
  24.             <state_interface name="velocity"/>
  25.         </joint>
  26.         <joint name="Joint4">
  27.             <command_interface name="position"/>
  28.             <state_interface name="position">
  29.                 <param name="initial_value">0.0</param>
  30.             </state_interface>
  31.             <state_interface name="velocity"/>
  32.         </joint>
  33.         <joint name="Joint5">
  34.             <command_interface name="position"/>
  35.             <state_interface name="position">
  36.                 <param name="initial_value">0.0</param>
  37.             </state_interface>
  38.             <state_interface name="velocity"/>
  39.         </joint>
  40.         <joint name="Joint6">
  41.             <command_interface name="position"/>
  42.             <state_interface name="position">
  43.                 <param name="initial_value">0.0</param>
  44.             </state_interface>
  45.             <state_interface name="velocity"/>
  46.         </joint>
  47.     </ros2_control>
复制代码
       到这里就完成了全部文件的修改、添加和创建,完成后别忘了colcon build一下,过程中出现问题和警告报错可以问问Gpt好伙伴(别放弃!参考的资料确实比ros1要少很多,就这么点活我磨磨蹭蹭干了一两个星期呢),下面就是运行创建的gazebo.launch.py和my_moveit_rviz.launch.py两个文件举行验证。
3 运行文件验证效果

       两个文件的启动顺序是先启动gazebo.launch.py,再启动my_moveit_rviz.launch.py,顺利的话你就能在rviz界面拖动运动规划的小球设定机械臂目标位置,然后点击规划并执行,之后就能看到Gazebo里的机械臂和rviz里的机械臂一起动起来了,而且非常顺滑没有卡顿和抖动(假如有那七七八八还是有点问题),效果视频联接丢下面,手机拍的渣渣画质将就看一看,还有简单的发布话题指令控制机械臂运动的视频:
【ros2中rviz和Gazebo联动仿真机械臂】 https://www.bilibili.com/video/BV1AUxCenEgi/?share_source=copy_web&vd_source=f99d962c3738b7b927f8a872bf2a3bb9
【发布话题下令控制机械臂运动】 https://www.bilibili.com/video/BV1rUxCeJEdq/?share_source=copy_web&vd_source=f99d962c3738b7b927f8a872bf2a3bb9

还可以启动两个文件后再打开一个终端输入下面这些下令看看是否跟下每个下令下的图一致:
1.检察各节点之间的关系
  1. ros2 run rqt_graph rqt_graph
复制代码

2.检察各硬件接口启动情况
  1. ros2 control list_hardware_interfaces
复制代码

       写完啦!写完啦!撒花!操纵过程中可能会遇到你有我没有的乱七八糟的问题所以这篇博客的内容仅供参考,有描述不准确大概出错的地方欢迎各人指出来,让我长长头脑,参考的博客也放在下面了,写的尊都很棒帮助很大呢!那么就祝各人操纵顺利哦~~我爱学习!学习爱我!
在ROS2中,通过MoveIt2控制Gazebo中的自定义机械手-CSDN博客
从零开始的机械臂yolov5抓取gazebo仿真(导航贴)_gazebo 视觉抓取-CSDN博客


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

用户国营

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表