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

目录

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)。文件具体内容如下,直接拿去用的小伙伴要检查一下文件名字、文件路径、控制器名字跟自己设置的是否一致哦!!!

import os
from launch import LaunchDescription
from launch.actions import ExecuteProcess, RegisterEventHandler
from launch_ros.actions import Node
from launch_ros.substitutions import FindPackageShare
from launch.substitutions import PathJoinSubstitution
from launch.event_handlers import OnProcessExit

import xacro
import re

def remove_comments(text):
    pattern = r'<!--(.*?)-->'
    return re.sub(pattern, '', text, flags=re.DOTALL)

def generate_launch_description():
    robot_name_in_model = 'dummy-ros2'
    package_name = 'dummy-ros2_description'
    urdf_name = "dummy-ros2.xacro"

    pkg_share = FindPackageShare(package=package_name).find(package_name) 
    urdf_model_path = os.path.join(pkg_share, f'urdf/{urdf_name}')
    dummy_moveit_config = FindPackageShare(package='dummy_moveit_config').find('dummy_moveit_config')

    controller_config = PathJoinSubstitution(
        [dummy_moveit_config, 'config', 'ros2_controllers.yaml']
    )

    
    # Start Gazebo server
    start_gazebo_cmd =  ExecuteProcess(
        cmd=['gazebo', '--verbose','-s', 'libgazebo_ros_init.so', '-s', 'libgazebo_ros_factory.so'],
        output='screen')


    # 因为 urdf文件中有一句 $(find dummy_moveit_config) 需要用xacro进行编译一下才行
    xacro_file = urdf_model_path
    doc = xacro.parse(open(xacro_file))
    xacro.process_doc(doc)
    # params = {'robot_description': doc.toxml()}
    params = {'robot_description': remove_comments(doc.toxml())}

    # 启动了robot_state_publisher节点后,该节点会发布 robot_description 话题,话题内容是模型文件urdf的内容
    # 并且会订阅 /joint_states 话题,获取关节的数据,然后发布tf和tf_static话题.
    robot_state_publisher = Node(
        package='robot_state_publisher',
        executable='robot_state_publisher',
        parameters=[{'use_sim_time': True}, params, {"publish_frequency":15.0}],
        output='screen'
    )

    # 非必要,只是之前在修改的过程中验证的时候老是出现找不到控制器配置文件ros2_controller.yaml就这这里再加了一点,但根本原因不在于没加这一段但是懒得删了
    controller_manager = Node(
        package='controller_manager',
        executable='ros2_control_node',
        parameters=[controller_config],
        output='screen'
    )  

    # Launch the robot, 通过robot_description话题进行模型内容获取从而在gazebo中生成模型
    spawn_entity_cmd = Node(
        package='gazebo_ros', 
        executable='spawn_entity.py',
        arguments=['-entity', robot_name_in_model,  '-topic', 'robot_description'], output='screen')

    # gazebo在加载urdf时,根据urdf的设定,会启动一个joint_states节点
    # 关节状态发布器
    load_joint_state_controller = ExecuteProcess(
        cmd=['ros2', 'control', 'load_controller', '--set-state', 'active', 'joint_state_broadcaster'],
        output='screen'
    )

    # 路径执行控制器,这个控制器名字不一样的要注意进行更换哦,控制器名字可以在类似ros2_controllers.yaml的文件里找到
    load_joint_trajectory_controller = ExecuteProcess(
        cmd=['ros2', 'control', 'load_controller', '--set-state', 'active', 'dummy_arm_controller'],
        output='screen'
    )

    # 控制好各个节点的启动顺序
    close_evt1 =  RegisterEventHandler( 
            event_handler=OnProcessExit(
                target_action=spawn_entity_cmd,
                on_exit=[load_joint_state_controller],
            )
    )
    # 监听 load_joint_state_controller
    close_evt2 = RegisterEventHandler(
            event_handler=OnProcessExit(
                target_action=load_joint_state_controller,
                on_exit=[load_joint_trajectory_controller],
            )
    )
    
    ld = LaunchDescription()

    ld.add_action(close_evt1)
    ld.add_action(close_evt2)

    ld.add_action(start_gazebo_cmd)
    ld.add_action(robot_state_publisher)
    ld.add_action(controller_manager)
    ld.add_action(spawn_entity_cmd)
    
    
    return ld

2.1.2 my_moveit_rviz.launch.py

         是加载Moveit配置和启动rviz的文件,该文件的主要内容有:1.moveit配置;2.启动moveit中move_group节点处理运动规划与执行;3.启动rviz并加载moveit配置。文件具体内容如下,这里同样也需要检查并更改对应文件的名字。

from moveit_configs_utils import MoveItConfigsBuilder
from moveit_configs_utils.launches import generate_moveit_rviz_launch
from launch import LaunchDescription
from launch.actions import DeclareLaunchArgument, IncludeLaunchDescription
from moveit_configs_utils.launch_utils import add_debuggable_node, DeclareBooleanLaunchArg
from launch.substitutions import LaunchConfiguration
from launch_ros.actions import Node
from launch_ros.parameter_descriptions import ParameterValue
from ament_index_python.packages import get_package_share_directory

def generate_launch_description():
  
    # MoveIt 配置
    moveit_config = MoveItConfigsBuilder("dummy-ros2", package_name="dummy_moveit_config").to_moveit_configs()
    ld = LaunchDescription()
    # 启动 MoveIt 中的 move_group 节点
    my_generate_move_group_launch(ld, moveit_config)

    # 启动 RViz 并加载 MoveIt 配置
    my_generate_moveit_rviz_launch(ld, moveit_config)

    return ld

def my_generate_move_group_launch(ld, moveit_config):
    ld.add_action(DeclareBooleanLaunchArg("debug", default_value=False))
    ld.add_action(
        DeclareBooleanLaunchArg("allow_trajectory_execution", default_value=True)
    )
    ld.add_action(
        DeclareBooleanLaunchArg("publish_monitored_planning_scene", default_value=True)
    )
    # load non-default MoveGroup capabilities (space separated)
    ld.add_action(DeclareLaunchArgument("capabilities", default_value=""))
    # inhibit these default MoveGroup capabilities (space separated)
    ld.add_action(DeclareLaunchArgument("disable_capabilities", default_value=""))

    # do not copy dynamics information from /joint_states to internal robot monitoring
    # default to false, because almost nothing in move_group relies on this information
    ld.add_action(DeclareBooleanLaunchArg("monitor_dynamics", default_value=False))
    
    should_publish = LaunchConfiguration("publish_monitored_planning_scene")

    move_group_configuration = {
        "robot_description": moveit_config.robot_description,
        "robot_description_semantic": moveit_config.robot_description_semantic,
        "publish_robot_description_semantic": True,
        "allow_trajectory_execution": LaunchConfiguration("allow_trajectory_execution"),
        "capabilities": ParameterValue(LaunchConfiguration("capabilities"), value_type=str),  # 使用 capabilities 参数
        "disable_capabilities": ParameterValue(LaunchConfiguration("disable_capabilities"), value_type=str),
  
        # Publish the planning scene of the physical robot so that rviz plugin can know actual robot
        "publish_planning_scene": should_publish,
        "publish_geometry_updates": should_publish,
        "publish_state_updates": should_publish,
        "publish_transforms_updates": should_publish,
        "monitor_dynamics": False,
  }
    move_group_params = [moveit_config.to_dict(), move_group_configuration]
    move_group_params.append({"use_sim_time": True})

    add_debuggable_node(
        ld,
        package="moveit_ros_move_group",
        executable="move_group",
        parameters=move_group_params,
        output="screen",
        extra_debug_args=["--debug"] if LaunchConfiguration("debug") == "true" else []
    )

def my_generate_moveit_rviz_launch(ld, moveit_config):
   
    ld.add_action(DeclareBooleanLaunchArg("debug", default_value=False))
    ld.add_action(
        DeclareLaunchArgument(
            "rviz_config",
            default_value=str(moveit_config.package_path / "config/moveit.rviz"),
        )
    )
   
    rviz_parameters = [
        moveit_config.planning_pipelines,
        moveit_config.robot_description_kinematics,
        {"use_sim_time": True}
    ]
    
    add_debuggable_node(
        ld,
        package="rviz2",
        executable="rviz2",
        arguments=['-d', LaunchConfiguration("rviz_config")],
        parameters=rviz_parameters,
        output="log"
    )

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文件的添加和修改有多处需要对应!!!)

controller_manager:
  ros__parameters:
    update_rate: 100
    
    dummy_arm_controller:
      type: joint_trajectory_controller/JointTrajectoryController

    joint_state_broadcaster:
      type: joint_state_broadcaster/JointStateBroadcaster



dummy_arm_controller:
  ros__parameters:
    joints:
      - Joint1
      - Joint2
      - Joint3
      - Joint4
      - Joint5
      - Joint6
    command_interfaces:
      - position  
    state_interfaces:
      - position
      - 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应该就是从这里写进去的,刚开始没写的时候老跟说找不着这个文件,要添加的代码内容如下所示:

<gazebo>
      <plugin filename="libgazebo_ros2_control.so" name="gazebo_ros2_control">
            <parameters>$(find dummy_moveit_config)/config/ros2_controllers.yaml</parameters>
            <robot_param_node>robot_state_publisher</robot_param_node>
            <robotParam>robot_description</robotParam>
      </plugin>
</gazebo>

2.3.2 dummy-ros2.trans

       这个文件是对传动的设置,要跟控制器配置文件中command_interfaces下的参数对应起来,因为里面是对六个关节做相同的配置,所以这里就放一个关节的代码作为例子,其他关节把名字改一改就行了,要进行修改的地方是<hardwareInterface>标签里的内容,最初文件里这个标签的内容写的是EffortJointInterface即力矩控制,而command_interfaces里写的参数是position,所以将内容都修改成PositionJointInterface即可:

<transmission name="transmission_joint1">
    <type>transmission_interface/SimpleTransmission</type>
    <joint name="Joint1">
      <hardwareInterface>PositionJointInterface</hardwareInterface>
    </joint>
    <actuator name="actuator_joint1">
      <hardwareInterface>PositionJointInterface</hardwareInterface>
    </actuator>
  </transmission>

2.3.3 dummy-ros2.xacro

       这个文件也需要添加部分有关ros2_control标签内容,与控制器配置文件中设置的硬件接口对应起来,即对每一个关节都写入command_interface和state_interface的内容,同时这里还规定了机械臂的初始位置即每个关节均为0.0,这个添加的部分跟dummy_moveit_config文件夹下config文件夹里dummy-ros2_control.xacro文件的内容高度重合,需要添加的具体内容如下所示:

<ros2_control name="GazeboSystem" type="system">
        <hardware>
            <plugin>gazebo_ros2_control/GazeboSystem</plugin>
        </hardware>
        <joint name="Joint1">
            <command_interface name="position"/>
            <state_interface name="position">
                <param name="initial_value">0.0</param>
            </state_interface>
            <state_interface name="velocity"/>
        </joint>
        <joint name="Joint2">
            <command_interface name="position"/>
            <state_interface name="position">
                <param name="initial_value">0.0</param>
            </state_interface>
            <state_interface name="velocity"/>
        </joint>
        <joint name="Joint3">
            <command_interface name="position"/>
            <state_interface name="position">
                <param name="initial_value">0.0</param>
            </state_interface>
            <state_interface name="velocity"/>
        </joint>
        <joint name="Joint4">
            <command_interface name="position"/>
            <state_interface name="position">
                <param name="initial_value">0.0</param>
            </state_interface>
            <state_interface name="velocity"/>
        </joint>
        <joint name="Joint5">
            <command_interface name="position"/>
            <state_interface name="position">
                <param name="initial_value">0.0</param>
            </state_interface>
            <state_interface name="velocity"/>
        </joint>
        <joint name="Joint6">
            <command_interface name="position"/>
            <state_interface name="position">
                <param name="initial_value">0.0</param>
            </state_interface>
            <state_interface name="velocity"/>
        </joint>
    </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.查看各节点之间的关系

ros2 run rqt_graph rqt_graph

2.查看各硬件接口启动情况

ros2 control list_hardware_interfaces

       写完啦!写完啦!撒花!操作过程中可能会碰到你有我没有的乱七八糟的问题所以这篇博客的内容仅供参考,有描述不准确或者出错的地方欢迎大家指出来,让我长长脑子,参考的博客也放在下面了,写的尊都很棒帮助很大呢!那么就祝大家操作顺利哦~~我爱学习!学习爱我!

在ROS2中,通过MoveIt2控制Gazebo中的自定义机械手-CSDN博客

从零开始的机械臂yolov5抓取gazebo仿真(导航贴)_gazebo 视觉抓取-CSDN博客

在虚拟机上安装Ubuntu 22.04ROS2有以下步骤: 1. 首先,你需要安装虚拟机软件,并创建一个新的虚拟机实例。你可以使用虚拟机软件如VMware或VirtualBox。 2. 下载Ubuntu 22.04的镜像文件,并将其加载到虚拟机。启动虚拟机并按照安装向导的提示进行安装。在安装过程,选择你的首选语言和其他设置。 3. 安装完成后,你需要更新系统并安装open-vm-tools。打开终端并执行以下命令: ``` sudo apt update sudo apt install open-vm-tools sudo apt install open-vm-tools-desktop ``` 4. 接下来,你需要准备ROS2的安装环境。首先,设置语言环境,打开终端并执行以下命令: ``` sudo locale-gen en_US en_US.UTF-8 sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 ``` 5. 安装Ubuntu universe存储库,执行以下命令: ``` sudo add-apt-repository universe sudo apt update ``` 6. 现在,你可以安装ROS2了。首先,添加ROS2的软件源,执行以下命令: ``` sudo apt update && sudo apt install curl gnupg2 lsb-release curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | sudo apt-key add - sudo sh -c 'echo "deb [arch=amd64,arm64 http://packages.ros.org/ros2/ubuntu $(lsb_release -cs) main" > /etc/apt/sources.list.d/ros2-latest.list' ``` 7. 安装ROS2,执行以下命令: ``` sudo apt update sudo apt install ros-foxy-desktop ``` 8. 配置ROS2的环境变量,执行以下命令: ``` source /opt/ros/foxy/setup.bash echo 'source /opt/ros/foxy/setup.bash' >> ~/.bashrc source ~/.bashrc ``` 以上是在虚拟机上安装Ubuntu 22.04ROS2的步骤。你可以参考官方文档了解更多细节。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [【ROS2实践】Vmware17下安装ubuntu22.04ros2-humble](https://blog.csdn.net/gongdiwudu/article/details/129095321)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* [Ubuntu22.04安装ROS2【图文讲解】](https://blog.csdn.net/TianHW103/article/details/127105413)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值