当前位置: 首页 > article >正文

从零开始基于ROS-Noetic使用gazebo操控真实机械臂(附完整运行代码)

一、前言

1.1 总体概述

        本文从零开始介绍如何使用ROS来操控真实的机械臂,其中包括创建机械臂的xacro模型,为机械臂创建仿真环境以及路径规划环境,使用了包括rviz,gazebo的使用。在ROS上订阅关节数据节点来操控真实的机械臂。本文在文末有完整的开源代码,只要在~/arm_ws/src/arm-robot下,在次目录下运行./start.sh,就能直接实现所有功能。

1.2 硬件系统

        本文采用的硬件包括:arduino mega2560,pca9685多舵机控制模块,USB转TTL调试模块,测试用的舵机或直接是自己做的机械臂,杜邦线若干。

1.3 控制系统

        PC端上的ROS-Noetic,操作系统是Ubuntu20.04。调试用的有串口调试助手,在Ubuntu上安装的Arduino IDE。

二、环境的配置

2.1 使用前环境的配置

        这部分的配置可以去参考作者写的一篇详细文章,里面详细写了从Ubuntu安装到ROS-Noetic的详细过程,里面也有Arduino的安装教程,本文就不详细叙述了。

基于树莓派ubuntu20.04的ros-noetic小车

        要注意的是,作者那篇文章使用的是PC端虚拟机和树莓派的分布式通信,本文目前并未使用到树莓派,如果读者使用的是树莓派来驱动Arduino的话,也可以按照作者那篇文章的来设置。如果不是,就要在 3.3.1 修改.bashrc的那一步, 将ROS_MASTER_URI这个的IP地址改回你自己虚拟机Ubuntu上的IP地址,因为目前主机和从机都是你自己的虚拟机。

2.2 为机械臂创建一个工作空间

mkdir -p arm_ws/src

        这步会创建一个文件夹,进入到arm_ws里面后执行catkin_make,执行完之后就会多出两个文件夹,一个是build,一个是devel,src是我们的源代码目录,build是编译过程中的中间文件存放的目录,devel是开发环境目录,编译后的目标文件和开发环境变量。

2.3 创建URDF文件夹

        在src目录下创建一个arm-robot来存放该项目相关的文件夹。

mkdir -p arm-robot/

        在arm-robot下创建一个用来放URDF文件。std_msgs rospy roscpp tf urdf xacro为该ros包的依赖项。

catkin_create_pkg marm_description std_msgs rospy roscpp tf urdf xacro 

        创建存放各个功能包的文件夹。

mkdir config launch meshes scripts urdf

        config:存放配置文件,通常用于参数设置,比如机器人模型的参数、传感器配置等。文件通常为 YAML 格式。

        launch:存放启动文件(.launch),用于启动一个或多个节点及其参数。这些文件定义了如何配置和启动你的 ROS 系统。

        meshes:存放三维模型文件,通常为 STL 或 DAE 格式,用于可视化和仿真中表示机器人的物理形状。

        scripts:存放可执行的 Python 脚本或其他脚本文件。这些脚本可以是 ROS 节点,也可以是辅助工具。

        urdf:存放机器人的 URDF 文件,描述机器人的结构和关节信息。URDF 文件定义了机器人的几何形状、关节类型、连杆等。

三、可视化操作步骤

3.1 编写arm.xacro文件

        此步骤如果是会建模的读者,或是已经有建模文件的读者,就可以跳过此步骤了,由于读者对机械臂的仿真要求不是很高,所以这里就直接使用圆柱体来代替了。如果是有建模文件的同学,就直接用solidwork来导出ros的urdf文件,具体的操作步骤请读者自行去搜索。

        小鱼ros一键安装已经在安装的过程中帮我们把必要的rviz等依赖都安装好了,所以我们不需要另外再去安装什么东西了。

        在urdf文件夹下创建一个arm.xacro文件

sudo nano arm.xacro

        该文件的完整内容如下:

<?xml version="1.0"?>
<robot name="arm" xmlns:xacro="http://ros.org/wiki/xacro">

    <!-- Defining the colors used in this robot -->
    <material name="Black">
        <color rgba="0 0 0 1"/>
    </material>

    <material name="White">
        <color rgba="1 1 1 1"/>
    </material>

    <material name="Blue">
        <color rgba="0 0 1 1"/>
    </material>

    <material name="Red">
        <color rgba="1 0 0 1"/>
    </material>

    <!-- Constants -->
    <xacro:property name="M_PI" value="3.14159"/>

    <!-- link1 properties -->
    <xacro:property name="link1_width" value="0.03" />
    <xacro:property name="link1_len" value="0.10" />

    <!-- link2 properties -->
    <xacro:property name="link2_width" value="0.03" />
    <xacro:property name="link2_len" value="0.14" />

    <!-- link3 properties -->
    <xacro:property name="link3_width" value="0.03" />
    <xacro:property name="link3_len" value="0.22" />

    <!-- link4 properties -->
    <xacro:property name="link4_width" value="0.025" />
    <xacro:property name="link4_len" value="0.06" />

    <!-- link5 properties -->
    <xacro:property name="link5_width" value="0.03" />
    <xacro:property name="link5_len" value="0.06" />

    <!-- link6 properties -->
    <xacro:property name="link6_width" value="0.04" />
    <xacro:property name="link6_len" value="0.02" />

    <!-- Left gripper -->
    <xacro:property name="left_gripper_len" value="0.08" />
    <xacro:property name="left_gripper_width" value="0.01" />
    <xacro:property name="left_gripper_height" value="0.01" />

    <!-- Right gripper -->
    <xacro:property name="right_gripper_len" value="0.08" />
    <xacro:property name="right_gripper_width" value="0.01" />
    <xacro:property name="right_gripper_height" value="0.01" />

    <!-- Gripper frame -->
    <xacro:property name="grasp_frame_radius" value="0.001" />

    <!-- Inertial matrix -->
    <xacro:macro name="inertial_matrix" params="mass">
        <inertial>
            <mass value="${mass}" />
            <inertia ixx="1.0" ixy="0.0" ixz="0.0" iyy="0.5" iyz="0.0" izz="1.0" />
        </inertial>
    </xacro:macro>

    <!-- ///   bottom_joint   // -->
    <joint name="bottom_joint" type="fixed">
        <origin xyz="0 0 0" rpy="0 0 0" />
        <parent link="base_link"/>
        <child link="bottom_link"/>
    </joint>
    <link name="bottom_link">
        <visual>
              <origin xyz=" 0 0 -0.02"  rpy="0 0 0"/>
                  <geometry>
                       <box size="1 1 0.02" />
                  </geometry>
              <material name="Black" />
        </visual>
        <collision>
            <origin xyz=" 0 0 -0.02"  rpy="0 0 0"/>
            <geometry>
                <box size="1 1 0.02" />
            </geometry>
        </collision>
        <xacro:inertial_matrix mass="500"/>
    </link>

    <!-- /   BASE LINK    // -->
    <link name="base_link">
        <visual>
            <origin xyz="0 0 0" rpy="0 0 0" />
            <geometry>
                <box size="0.1 0.1 0.04" />
            </geometry>
            <material name="White" />
        </visual>
        <collision>
            <origin xyz="0 0 0" rpy="0 0 0" />
            <geometry>
                <box size="0.1 0.1 0.04" />
            </geometry>
        </collision>
        <!--<xacro:inertial_matrix mass="1"/>-->
    </link>

    <joint name="joint1" type="revolute">
        <parent link="base_link"/>
        <child link="link1"/>
        <origin xyz="0 0 0.02" rpy="0 ${M_PI/2} 0" />
        <axis xyz="-1 0 0" />
        <limit effort="300" velocity="1" lower="-2.96" upper="2.96"/>
        <dynamics damping="50" friction="1"/>
    </joint>

    <!-- /   LINK1  // -->
    <link name="link1" >
        <visual>
            <origin xyz="-${link1_len/2} 0 0" rpy="0 ${M_PI/2} 0" />
            <geometry>
                <cylinder radius="${link1_width}" length="${link1_len}"/>
            </geometry>
            <material name="Blue" />
        </visual>
        <collision>
            <origin xyz="-${link1_len/2} 0 0" rpy="0 ${M_PI/2} 0" />
            <geometry>
                <cylinder radius="${link1_width}" length="${link1_len}"/>
            </geometry>
        </collision>
        <xacro:inertial_matrix mass="1"/>
    </link>

    <joint name="joint2" type="revolute">
        <parent link="link1"/>
        <child link="link2"/>
        <origin xyz="-${link1_len} 0 0.0" rpy="-${M_PI/2} 0 ${M_PI/2}" />
        <axis xyz="1 0 0" />
        <limit effort="300" velocity="1" lower="-2.35" upper="2.35" />
        <dynamics damping="50" friction="1"/>
    </joint>

    <!-- ///   LINK2  // -->
    <link name="link2" >
        <visual>
            <origin xyz="0 0 ${link2_len/2}" rpy="0 0 0" />
            <geometry>
                <cylinder radius="${link2_width}" length="${link2_len}"/>
            </geometry>
            <material name="White" />
        </visual>

        <collision>
            <origin xyz="0 0 ${link2_len/2}" rpy="0 0 0" />
            <geometry>
                <cylinder radius="${link2_width}" length="${link2_len}"/>
            </geometry>
        </collision>
        <xacro:inertial_matrix mass="1"/>
    </link>

    <joint name="joint3" type="revolute">
        <parent link="link2"/>
        <child link="link3"/>
        <origin xyz="0 0 ${link2_len}" rpy="0 ${M_PI} 0" />
        <axis xyz="-1 0 0" />
        <limit effort="300" velocity="1" lower="-2.62" upper="2.62" />
        <dynamics damping="50" friction="1"/>
    </joint>

    <!-- /   LINK3  / -->
    <link name="link3" >
        <visual>
            <origin xyz="0 0 -${link3_len/2}" rpy="0 0 0" />
            <geometry>
                <cylinder radius="${link3_width}" length="${link3_len}"/>
            </geometry>
            <material name="Blue" />
        </visual>
        <collision>
            <origin xyz="0 0 -${link3_len/2}" rpy="0 0 0" />
            <geometry>
                <cylinder radius="${link3_width}" length="${link3_len}"/>
            </geometry>
        </collision>
        <xacro:inertial_matrix mass="1"/>
    </link>

    <joint name="joint4" type="revolute">
        <parent link="link3"/>
        <child link="link4"/>
        <origin xyz="0.0 0.0 -${link3_len}" rpy="0 ${M_PI/2} ${M_PI}" />
        <axis xyz="1 0 0" />
        <limit effort="300" velocity="1" lower="-2.62" upper="2.62" />
        <dynamics damping="50" friction="1"/>
    </joint>

    <!-- ///   LINK4   -->
    <link name="link4" >
        <visual>
            <origin xyz="${link4_len/2} 0 0" rpy="0 ${M_PI/2} 0" />
            <geometry>
                <cylinder radius="${link4_width}" length="${link4_len}"/>
            </geometry>
            <material name="Black" />
        </visual>
        <collision>
            <origin xyz="${link4_len/2} 0 0" rpy="0 ${M_PI/2} 0" />
            <geometry>
                <cylinder radius="${link4_width}" length="${link4_len}"/>
            </geometry>
        </collision>
        <xacro:inertial_matrix mass="1"/>
    </link>

    <joint name="joint5" type="revolute">
        <parent link="link4"/>
        <child link="link5"/>
        <origin xyz="${link4_len} 0.0 0.0" rpy="0 ${M_PI/2} 0" />
        <axis xyz="1 0 0" />
        <limit effort="300" velocity="1" lower="-2.62" upper="2.62" />
        <dynamics damping="50" friction="1"/>
    </joint>

    <!-- //   LINK5  / -->
    <link name="link5">
        <visual>
            <origin xyz="0 0 ${link4_len/2}" rpy="0 0 0" />
            <geometry>
                <cylinder radius="${link5_width}" length="${link5_len}"/>
            </geometry>
            <material name="White" />
        </visual>
        <collision>
            <origin xyz="0 0 ${link4_len/2} " rpy="0 0 0" />
            <geometry>
                <cylinder radius="${link5_width}" length="${link5_len}"/>
            </geometry>
        </collision>
        <xacro:inertial_matrix mass="1"/>
    </link>

    <joint name="joint6" type="revolute">
        <parent link="link5"/>
        <child link="link6"/>
        <origin xyz="0 0 ${link4_len}" rpy="${1.5*M_PI} -${M_PI/2} 0" />
        <axis xyz="1 0 0" />
        <limit effort="300" velocity="1" lower="-6.28" upper="6.28" />
        <dynamics damping="50" friction="1"/>
    </joint>

    <!--    LINK6  / -->
    <link name="link6">
        <visual>
            <origin xyz="${link6_len/2} 0 0 " rpy="0 ${M_PI/2} 0" />
            <geometry>
                <cylinder radius="${link6_width}" length="${link6_len}"/>
            </geometry>
            <material name="Blue" />
        </visual>
        <collision>
            <origin xyz="${link6_len/2} 0 0" rpy="0 ${M_PI/2} 0" />
            <geometry>
                <cylinder radius="${link6_width}" length="${link6_len}"/>
            </geometry>
        </collision>
        <xacro:inertial_matrix mass="1"/>
    </link>

    <joint name="finger_joint1" type="prismatic">
        <parent link="link6"/>
        <child link="gripper_finger_link1"/>
        <origin xyz="0.0 0 0" />
        <axis xyz="0 1 0" />
        <limit effort="100" lower="0" upper="0.06" velocity="1.0"/>
        <dynamics damping="50" friction="1"/>
    </joint>

    <!-- //   gripper   // -->
    <!-- LEFT GRIPPER AFT LINK -->
    <link name="gripper_finger_link1">
        <visual>
            <origin xyz="0.04 -0.03 0"/>
            <geometry>
                <box size="${left_gripper_len} ${left_gripper_width} ${left_gripper_height}" />
            </geometry>
            <material name="White" />
        </visual>
        <collision>
            <origin xyz="0.04 -0.03 0"/>
            <geometry>
                <box size="${left_gripper_len} ${left_gripper_width} ${left_gripper_height}" />
            </geometry>
            <material name="White" />
        </collision>
        <xacro:inertial_matrix mass="1"/>
    </link>

    <joint name="finger_joint2" type="fixed">
        <parent link="link6"/>
        <child link="gripper_finger_link2"/>
        <origin xyz="0.0 0 0" />
    </joint>

    <!-- RIGHT GRIPPER AFT LINK -->
    <link name="gripper_finger_link2">
        <visual>
            <origin xyz="0.04 0.03 0"/>
            <geometry>
                <box size="${right_gripper_len} ${right_gripper_width} ${right_gripper_height}" />
            </geometry>
            <material name="White" />
        </visual>
        <collision>
            <origin xyz="0.04 0.03 0"/>
            <geometry>
                <box size="${right_gripper_len} ${right_gripper_width} ${right_gripper_height}" />
            </geometry>
            <material name="White" />
        </collision>
        <xacro:inertial_matrix mass="1"/>
    </link>

    <!-- Grasping frame -->
    <link name="grasping_frame"/>

    <joint name="grasping_frame_joint" type="fixed">
      <parent link="link6"/>
      <child link="grasping_frame"/>
      <origin xyz="0.08 0 0" rpy="0 0 0"/>
    </joint>

    <!-- /   Gazebo   // -->
    <gazebo reference="bottom_link">
        <material>Gazebo/White</material>
    </gazebo>
    <gazebo reference="base_link">
        <material>Gazebo/White</material>
    </gazebo>
    <gazebo reference="link1">
        <material>Gazebo/Blue</material>
    </gazebo>
    <gazebo reference="link2">
        <material>Gazebo/White</material>
    </gazebo>
    <gazebo reference="link3">
        <material>Gazebo/Blue</material>
    </gazebo>
    <gazebo reference="link4">
        <material>Gazebo/Black</material>
    </gazebo>
    <gazebo reference="link5">
        <material>Gazebo/White</material>
    </gazebo>
    <gazebo reference="link6">
        <material>Gazebo/Blue</material>
    </gazebo>
    <gazebo reference="gripper_finger_link1">
        <material>Gazebo/White</material>
    </gazebo>
    <gazebo reference="gripper_finger_link2">
        <material>Gazebo/White</material>
    </gazebo>
    
    <!-- Transmissions for ROS Control -->
    <xacro:macro name="transmission_block" params="joint_name">
        <transmission name="tran1">
            <type>transmission_interface/SimpleTransmission</type>
            <joint name="${joint_name}">
                <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
            </joint>
            <actuator name="motor1">
                <hardwareInterface>hardware_interface/PositionJointInterface</hardwareInterface>
                <mechanicalReduction>1</mechanicalReduction>
            </actuator>
        </transmission>
    </xacro:macro>
    
    <xacro:transmission_block joint_name="joint1"/>
    <xacro:transmission_block joint_name="joint2"/>
    <xacro:transmission_block joint_name="joint3"/>
    <xacro:transmission_block joint_name="joint4"/>
    <xacro:transmission_block joint_name="joint5"/>
    <xacro:transmission_block joint_name="joint6"/>
    <xacro:transmission_block joint_name="finger_joint1"/>

    <!-- ros_control plugin -->
    <gazebo>
        <plugin name="gazebo_ros_control" filename="libgazebo_ros_control.so">
            <robotNamespace>/arm</robotNamespace>
        </plugin>
    </gazebo>

</robot>

        接下来解析代码。

        颜色定义,定义了不同的颜色,用于机器人部件的视觉表示。每种颜色都有一个名称和 RGBA 值。

         链接属性,定义了不同链接的宽度和长度属性,便于在后续使用。

         惯性矩阵宏,定义了一个宏,用于创建惯性矩阵,接受一个质量参数。这样可以在多个链接中复用这一部分代码。

         链接定义,定义了链接(link)的视觉和碰撞特性,并设置了质量。每个链接都有视觉(visual)和碰撞(collision)部分。

         关节定义,定义了一个可转动关节joint1,其父链接和子链接分别是base_link 和link。还定义了关节的原点、旋转轴、限制和动态特性。

        type为revolute时,表示这是一个可转动的关节,type的类型有以下这几种:

revolute:旋转关节,允许围绕一个轴旋转。
prismatic:平移关节,允许沿一个轴移动。
fixed:固定关节,不允许相对运动。
floating:浮动关节,允许在三维空间中自由移动。
planar:平面关节,允许在一个平面内移动。

        那个lower和upper代表的是弧度制的0到Π(3.1415926),转换为角度大概就是0到180°,作者使用的是180°舵机,所以就使用了这个弧度作为限制。如果读者使用的是360°舵机,那请自行修改,计算的公式如下:

        度 = 弧度 × 180/Π

        geometry的就是你定义的几何体形状,几何体的类型有:

box:立方体,定义一个长方体的几何形状。
<box>
    <size>x y z</size>
</box>

cylinder:圆柱体,定义一个圆柱的几何形状。
<cylinder>
    <radius>r</radius>
    <length>l</length>
</cylinder>

sphere:球体,定义一个球的几何形状。
<sphere>
    <radius>r</radius>
</sphere>

mesh:网格,定义一个复杂形状的几何体,通常用于导入3D模型。
<mesh>
    <uri>path/to/your/model.stl</uri>
</mesh>

         夹爪定义,定义了一个用于抓取的链接,包括视觉和碰撞特性,表明它的形状和材料。

        Gazebo 相关定义,为 Gazebo 中的链接定义了材料属性,用于在仿真中显示的外观。

         传动系统宏,定义了传动系统的宏,接受关节名称作为参数,用于简化多个关节的传动系统定义。

        ros_control 插件, 为 Gazebo 添加了 ROS 控制插件,允许 ROS 控制机器人并与 Gazebo 进行交互。

3.2  检查xacro格式

        由于作者的xarco不能直接检查,只能将其转化为urdf文件后再进行检查,所以作者直接就写了一个脚本来执行这个过程,在读者写了xacro,如果想要检查一下自己写的是否有错误,也可以进行这个操作。

        报错的内容如下

        检查格式的脚本,先创建一个check.sh文件,sudo nano check.sh,将以下内容填进去。

#!/bin/bash
xacro arm.xacro > arm.urdf
check_urdf arm.urdf

        如果出现下面的这个的话,就说明你的xacro文件没错了,下面的这些child是你的关节的父子类。

3.3 编写可视化执行启动文件

        在这步会创建一个启动文件,会发布机械臂的关节状态,并使用rviz加载出来。在marm_description/launch目录下:

sudo nano view_arm.launch

        写入节点,在这个启动文件里,启动了一个gui节点,读者可以使用此gui,使用滑块来控制机械臂,在滑动机械臂运动之后,机械臂可能会一直不断的抽搐,也就是在不断的往返往返运动,这个是正常现象,无需担心。

<launch>
    <arg name="model" />
    <!-- 加载机器人模型参数 -->
    <param name="robot_description" command="$(find xacro)/xacro --inorder $(find marm_description)/urdf/arm.xacro" />

    <!-- 设置GUI参数,显示关节控制插件 -->
    <!--<param name="use_gui" value="true"/>-->

    <!-- 运行joint_state_publisher节点,发布机器人的关节状态  -->
    <node name="joint_state_publisher" pkg="joint_state_publisher" type="joint_state_publisher" />
    
    <!-- 运行joint_state_publisher节点,发布机器人的关节状态  -->
    <node name="joint_state_publisher_gui" pkg="joint_state_publisher_gui" type="joint_state_publisher_gui" />
    
    <!-- 运行robot_state_publisher节点,发布tf  -->
    <node name="robot_state_publisher" pkg="robot_state_publisher" type="robot_state_publisher" />

    <!-- 运行rviz可视化界面 -->
    <node name="rviz" pkg="rviz" type="rviz" args="-d $(find marm_description)/urdf.rviz" required="true" />
</launch>

        随后在工作空间下执行

catkin_make
source devel/setup.bash
roslaunch marm_description view_arm.launch

 

        执行后的效果如下图: 

四、Moveit运动规划

        在弄完rviz之后,就可以开始弄moveit运动规划了。在终端输入

rosrun moveit_setup_assistant moveit_setup_assistant

        要注意:你的电脑网络最好是连你自己的热点,如果是连公网的话,你的主机请求可能会被防火墙拦截,导致启动不了moveit。

        先启动ros节点

roscore

        再开另外一个终端来运行moveit配置(打开终端的快捷方式ctrl alt t)。

rosrun moveit_setup_assistant moveit_setup_assistant

        开始界面如下,新建一个Package,导入刚刚我们写的那个urdf文件, /home/pi/arm_ws/src/arm-robot/marm_description/urdf/arm.urdf

         首先点Self-Collisions,点Generate Collision Matrix生成碰撞矩阵,用于自动生成和管理机器人各个部分之间的碰撞检测信息。使用默认的配置就行。

         Virtual Joints本文是没有用到虚拟关节的,所以就不需要管他了。

        然后是Planning Groups,规划组是将机器人的的关节或链接组合在一起,以便于进行运动规划和控制。点击Add Group,为组别起名字,机械臂运动是符合运动学规律的,所以要给机械臂添加kdl kinematics plugin/KDLKinematicsPlugin,机器人运动学计算。

         点击Add Kin.chain,为机械臂添加和配置运动学链(kinematic chain)。运动学链是指机器人的关节和链接的集合,描述了从基座到末端执行器的运动关系。通过添加运动学链,用户可以明确机器人模型的各个部分如何相互连接和运动。配置运动学链后,MoveIt可以进行逆向运动学和正向运动学的计算,帮助确定关节位置。

        再添加一个gripper组,这个组不需要添加运动学配置,这个组需要添加关节和链接。点击Add Joints,添加夹爪

        保存完之后,再次点击gripper组的Links,添加链接。

         Robot Pose是我们用来定义机械臂的姿态,用来表示当前机械臂的状态。Robot Pose用于表示机器人的当前状态,便于在运动规划和控制过程中跟踪机器人的位置和方向。这里作者就只定义一个初始化的姿态。读者可以根据自己的需求设置到想要的姿态。

         然后是填写Author Information,这个一定要填,随便填也要填一个进去,不然他是会导出失败的。

        最后是Configuration Files,我们先去arm-robot下创建一个存放Moveit配置文件的文件夹。

mkdir robot_moveit_config

        在Configuration 中选择该保存路径。

        如果生成成功应该会出现下面这些东西。

        在launch文件夹下,会有一个示例文件demo.launch,我们可以试着启动他一下。

 

        启动之后,就可以进行路径规划了。你可以通过移动那个球(Goal Pose)来将机械臂移动到指定的位置,也可以选择你之前在Moveit上设置的Goal State,弄完之后点Plan & Execute就会开始路径规划了。

        这时候你再开启另外一个终端,用来获取此时的关节数据,首先先输入 rostopic list获取此时在运行的节点,发现有一个节点名字叫做/joint_states,这个就是驱动你机械臂运动的关节数据,我们可以看一下这个关节数据的内容

rostopic echo /joint_states

         下面这些就是我们关节的数据,这个/joint_states提供的数据非常重要,在我们后续用gazebo来驱动真实的机械臂上起的至关重要的作用。

五、gazebo仿真 

        在moveit的配置文件当中,在launch中也有一个文件是叫做gazebo的文件的,我们将其运行一下就会进入到gazebo当中了。

source devel/setup.bash
roslaunch robot_moveit_config gazebo.launch

         这个就是我们的gazebo界面了,接下来我们要试着来写一个逻辑控制节点来控制此机械臂。

         在marm_description的script目录下创建一个simple_mover.py的运动控制节点,写入以下代码。

#!/usr/bin/env python

import math
import rospy
from std_msgs.msg import Float64

def mover():
    # 发布每个关节的控制指令
    pub_j1 = rospy.Publisher('/arm/joint1_position_controller/command', Float64, queue_size=10)
    pub_j2 = rospy.Publisher('/arm/joint2_position_controller/command', Float64, queue_size=10)
    pub_j3 = rospy.Publisher('/arm/joint3_position_controller/command', Float64, queue_size=10)
    pub_j4 = rospy.Publisher('/arm/joint4_position_controller/command', Float64, queue_size=10)
    pub_j5 = rospy.Publisher('/arm/joint5_position_controller/command', Float64, queue_size=10)
    pub_j6 = rospy.Publisher('/arm/joint6_position_controller/command', Float64, queue_size=10)
    
    rospy.init_node('arm_mover')
    rate = rospy.Rate(10)  # 10Hz 发布频率
    start_time = 0

    # 等待时间初始化
    while not start_time:
        start_time = rospy.Time.now().to_sec()

    # 主循环
    while not rospy.is_shutdown():
        elapsed = rospy.Time.now().to_sec() - start_time
        # 每个关节周期性移动,使用正弦函数
        position = math.sin(2 * math.pi * 0.1 * elapsed) * (math.pi / 2)
        pub_j1.publish(position)
        pub_j2.publish(position)
        pub_j3.publish(position)
        pub_j4.publish(position)
        pub_j5.publish(position)
        pub_j6.publish(position)
        
        rate.sleep()

if __name__ == '__main__':
    try:
        mover()
    except rospy.ROSInterruptException:
        pass

        此代码通过sin函数,产生一个关节控制数据,通过/arm/joint1_position_controller/command节点来控制各个关节的运动。这个路径名字是取决于你在urdf中所定义的名字。

roslaunch marm_description gazebo.launch
rosrun marm_description simple_mover.py     #打开另外一个终端

        这时候你就可以看见你的机械臂在一直不断的运动了,懂得了如何操控机械臂之后,接下来就是自己编写ros节点来控制机械臂运动了,控制机械臂运动可以用逻辑节点来控制,也就是你自己编写一个逻辑代码,使机械臂自主决策来运动,本文这里没有采用这种方式,而是采用一个gui界面来手动控制机械臂运动。

六、编写节点控制gazebo机械臂

        在上面我们使用一个gui节点来控制rviz的机械臂运动,在gazebo中我们也可以采用一个gui节点调出这个界面,从而用这个界面来控制gazebo的机械臂。

        我们首先先启动gazebo

source devel/setup.bash
roslaunch robot_moveit_config gazebo.launch

        在另外一个终端启动gui界面

source devel/setup.bash
rosrun joint_state_publisher_gui joint_state_publisher_gui

        这时候我们滑动这个滑块,是不是不能控制我们的gazebo,这是当然的,在上面作者也说了,gazebo的关节控制是通过/arm/joint1_position_controller/command节点来控制的,所以我们还需要一个转换节点来将/joint_states中的关节数据订阅,然后发布到command上。

        在marm_description的script目录中创建一个joint_states_to_command.py文件,这个节点文件将帮助我们订阅/joint_states节点然后发布给command。

#!/usr/bin/env python

import rospy
from sensor_msgs.msg import JointState
from std_msgs.msg import Float64

# 定义关节的命令发布器
joint_pubs = []

def joint_states_callback(msg):
    # 将收到的 joint_states 数据转换为各个关节的控制命令并发布
    if len(msg.position) >= 6:  # 确保有至少6个关节数据
        joint_pubs[0].publish(msg.position[0])  # joint1
        joint_pubs[1].publish(msg.position[1])  # joint2
        joint_pubs[2].publish(msg.position[2])  # joint3
        joint_pubs[3].publish(msg.position[3])  # joint4
        joint_pubs[4].publish(msg.position[4])  # joint5
        joint_pubs[5].publish(msg.position[5])  # joint6

def main():
    global joint_pubs

    rospy.init_node('joint_states_to_command', anonymous=True)

    # 创建发布器,将关节位置发布到对应的控制器话题
    joint_pubs = [
        rospy.Publisher('/arm/joint1_position_controller/command', Float64, queue_size=10),
        rospy.Publisher('/arm/joint2_position_controller/command', Float64, queue_size=10),
        rospy.Publisher('/arm/joint3_position_controller/command', Float64, queue_size=10),
        rospy.Publisher('/arm/joint4_position_controller/command', Float64, queue_size=10),
        rospy.Publisher('/arm/joint5_position_controller/command', Float64, queue_size=10),
        rospy.Publisher('/arm/joint6_position_controller/command', Float64, queue_size=10)
    ]

    # 订阅 /arm/joint_states 话题
    rospy.Subscriber("/joint_states", JointState, joint_states_callback)

    # 保持节点运行
    rospy.spin()

if __name__ == '__main__':
    try:
        main()
    except rospy.ROSInterruptException:
        pass

        再另外开一个终端,将节点激活并开始运行。

source devel/setup.bash
rosrun marm_description joint_states_to_command.py

        这时候你再滑动gui的滑块就发现能够成功控制到机械臂的运作了。

七、ros与arduino通讯

        弄了这么多软件方面的东西,终于也是回到了我们的硬件环节了,ros和arduino的通讯方式主要有两种,一种是直接用串口来通讯,但是直接用串口来通讯的话,处理的时候可能会比较麻烦,所以本文这里直接就选择了第二种方法,也就是直接使用rosserial来通讯,在使用这个之前,我们首先要在Ubuntu上安装rosserial的依赖。版本名字依据你自己的ros版本来更改。

sudo apt install ros-noetic-rosserial ros-noetic-rosserial-client ros-noetic-rosserial-arduino

        然后在arduino上,安装rosserial的库,如果你是和参考我的这篇文章安装的arduino的话,那你就完全跟着我的步骤走,但是步骤其实都差不多的。

        点击左边导航栏的第三个库管理,然后搜索rosserial,点击安装。如果没有左边的导航栏的话,就在工具——>库管理,也是可以下载这个rosserial库的。

                                        

                ​​​​​​​        ​​​​​​​        ​​​​​​​        

        安装完毕之后,接下来我们就可以测试一下这个库了。

        我们可以打开示例中的 hello world来测试我们的rosserial,如果有读者发现打不开,这是正常的,因为那个hello world的后缀名还是旧版的ide格式,我们现在的arduino已经是ino后缀了,所以我们只需要去arduino的库里面把那个示例的后缀名改一下就行了。

        在arduino内点击文件——>首选项,在那里可以看见项目的文件夹地址,点击浏览,进入arduino的库。

        选择Rosserial_Arduino_Library库,找到examples里面的hello world示例,将ide改成ino,再返回arduino,就发现这个示例能够打开了。如果有想看看其他示例的,也可以去一个个更改那个文件的格式。

        如果有懒得去修改的,这里也粘贴出完整的测试代码。

/*
 * rosserial Publisher Example
 * Prints "hello world!"
 */

#include <ros.h>
#include <std_msgs/String.h>

ros::NodeHandle  nh;

std_msgs::String str_msg;
ros::Publisher chatter("chatter", &str_msg);

char hello[13] = "hello world!";

void setup()
{
  nh.initNode();
  nh.advertise(chatter);
}

void loop()
{
  str_msg.data = hello;
  chatter.publish( &str_msg );
  nh.spinOnce();
  delay(1000);
}

        将你的arduino接到虚拟机上,把程序烧录进去,烧进去之后,按一下arduino的复位,返回Ros上,打开rosserial。USB根据你自己的实际来修改。在arduino上可以看见Arduino连接到了哪个USB上。

rosrun rosserial_python serial_node.py /dev/ttyUSB0

        如果运行之后发现没有权限操控ttyUSB0,那就要把ttyUSB0添加到dialout用户组当中。

sudo usermod -aG dialout pi  #你自己的用户

        运行完上面的命令后,需要重新登录你的用户账户,以使组权限更改生效。你可以注销并重新登录,或者重启虚拟机。

        重启后,通过以下命令来检查/dev/ttyUSB0的权限。

ls -l /dev/ttyUSB0

        输出中应该显示设备属于dialout组,并且拥有读取和写入权限(通常为crw-rw----)。

        这时再运行一次rosrun rosserial_python serial_node.py /dev/ttyUSB0应该就没有报错了。

        没报错之后我们就可以来订阅arduino发布的/chatter节点了。

rostopic echo /chatter

        这时候我们就可以看见来自arduino发布的消息了。

八、真实机械臂的驱动

        解决完ros与arduino的通讯之后,接下来的问题就是如何驱动机械臂了,这里作者采用了PCA9685的模块来驱动机械臂,PCA9685是一款可以驱动多路舵机的模块,使用的是IIC通讯,在arduino上是有pca9685的相关库的。

        在arduino上搜索Adafruit PwM Servo Driver Library,随后可以使用一个舵机来测试这个库。

        测试代码如下,是在该库的servo示例上:

/*************************************************** 
  This is an example for our Adafruit 16-channel PWM & Servo driver
  Servo test - this will drive 8 servos, one after the other on the
  first 8 pins of the PCA9685

  Pick one up today in the adafruit shop!
  ------> http://www.adafruit.com/products/815
  
  These drivers use I2C to communicate, 2 pins are required to  
  interface.

  Adafruit invests time and resources providing this open source code, 
  please support Adafruit and open-source hardware by purchasing 
  products from Adafruit!

  Written by Limor Fried/Ladyada for Adafruit Industries.  
  BSD license, all text above must be included in any redistribution
 ****************************************************/

#include <Wire.h>
#include <Adafruit_PWMServoDriver.h>

// called this way, it uses the default address 0x40
Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver();
// you can also call it with a different address you want
//Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver(0x41);
// you can also call it with a different address and I2C interface
//Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver(0x40, Wire);

// Depending on your servo make, the pulse width min and max may vary, you 
// want these to be as small/large as possible without hitting the hard stop
// for max range. You'll have to tweak them as necessary to match the servos you
// have!
#define SERVOMIN  150 // This is the 'minimum' pulse length count (out of 4096)
#define SERVOMAX  600 // This is the 'maximum' pulse length count (out of 4096)
#define USMIN  600 // This is the rounded 'minimum' microsecond length based on the minimum pulse of 150
#define USMAX  2400 // This is the rounded 'maximum' microsecond length based on the maximum pulse of 600
#define SERVO_FREQ 50 // Analog servos run at ~50 Hz updates

// our servo # counter
uint8_t servonum = 0;

void setup() {
  Serial.begin(9600);
  Serial.println("8 channel Servo test!");

  pwm.begin();
  /*
   * In theory the internal oscillator (clock) is 25MHz but it really isn't
   * that precise. You can 'calibrate' this by tweaking this number until
   * you get the PWM update frequency you're expecting!
   * The int.osc. for the PCA9685 chip is a range between about 23-27MHz and
   * is used for calculating things like writeMicroseconds()
   * Analog servos run at ~50 Hz updates, It is importaint to use an
   * oscilloscope in setting the int.osc frequency for the I2C PCA9685 chip.
   * 1) Attach the oscilloscope to one of the PWM signal pins and ground on
   *    the I2C PCA9685 chip you are setting the value for.
   * 2) Adjust setOscillatorFrequency() until the PWM update frequency is the
   *    expected value (50Hz for most ESCs)
   * Setting the value here is specific to each individual I2C PCA9685 chip and
   * affects the calculations for the PWM update frequency. 
   * Failure to correctly set the int.osc value will cause unexpected PWM results
   */
  pwm.setOscillatorFrequency(27000000);
  pwm.setPWMFreq(SERVO_FREQ);  // Analog servos run at ~50 Hz updates

  delay(10);
}

// You can use this function if you'd like to set the pulse length in seconds
// e.g. setServoPulse(0, 0.001) is a ~1 millisecond pulse width. It's not precise!
void setServoPulse(uint8_t n, double pulse) {
  double pulselength;
  
  pulselength = 1000000;   // 1,000,000 us per second
  pulselength /= SERVO_FREQ;   // Analog servos run at ~60 Hz updates
  Serial.print(pulselength); Serial.println(" us per period"); 
  pulselength /= 4096;  // 12 bits of resolution
  Serial.print(pulselength); Serial.println(" us per bit"); 
  pulse *= 1000000;  // convert input seconds to us
  pulse /= pulselength;
  Serial.println(pulse);
  pwm.setPWM(n, 0, pulse);
}

void loop() {
  // Drive each servo one at a time using setPWM()
  Serial.println(servonum);
  for (uint16_t pulselen = SERVOMIN; pulselen < SERVOMAX; pulselen++) {
    pwm.setPWM(servonum, 0, pulselen);
  }

  delay(500);
  for (uint16_t pulselen = SERVOMAX; pulselen > SERVOMIN; pulselen--) {
    pwm.setPWM(servonum, 0, pulselen);
  }

  delay(500);

  // Drive each servo one at a time using writeMicroseconds(), it's not precise due to calculation rounding!
  // The writeMicroseconds() function is used to mimic the Arduino Servo library writeMicroseconds() behavior. 
  for (uint16_t microsec = USMIN; microsec < USMAX; microsec++) {
    pwm.writeMicroseconds(servonum, microsec);
  }

  delay(500);
  for (uint16_t microsec = USMAX; microsec > USMIN; microsec--) {
    pwm.writeMicroseconds(servonum, microsec);
  }

  delay(500);

  servonum++;
  if (servonum > 7) servonum = 0; // Testing the first 8 servo channels
}

        编译后烧录到arduino上,就可以驱动舵机运动了,接下来就可以使用该库来驱动我们的机械臂了。

        在ros上,我们还需要编写一个节点,用来订阅/joint_states的消息和将该消息转换为我们舵机的PWM驱动值,作者使用的舵机是180°舵机(标准180°舵机的信号脉冲宽度范围为600微秒到2400微秒),所以限幅SERVOMIN和SERVOMAX是150到600,作者对精度的要求目前不是很高,所以就直接使用了pwm.setPWM来编写,如果读者的精度的要求较高的话,也可以使用pwm.writeMicroseconds编写。

        转换的公式为:SERVOMIN = (脉冲长度/1000000)*频率*4096

        所以SERVOMIN = (600/20000)*4096 = 150  (50hz约为20毫秒)

        在marm_description/script下创建一个states_to_positions.py节点文件,写入以下内容:

#!/usr/bin/env python

import rospy
from sensor_msgs.msg import JointState
from std_msgs.msg import Float64MultiArray,Float32MultiArray

SERVOMIN = 150  # 最小脉冲长度
SERVOMAX = 600  # 最大脉冲长度

def map_value(value, in_min, in_max, out_min, out_max):
    # 将输入值从一个范围映射到另一个范围
    return out_min + (out_max - out_min) * (value - in_min) / (in_max - in_min)

def joint_states_callback(msg):
    # 创建一个新的消息来发布关节位置
    position_msg = Float32MultiArray()
    
    # 将关节位置从弧度转换为PWM值
    position_msg.data = [
        int(map_value(round(pos, 7), 0, 3.1415926, SERVOMIN, SERVOMAX)) 
        for pos in msg.position[:7]  # 只取前7个关节的位置
    ]

    # 发布到新的话题
    joint_positions_pub.publish(position_msg)

if __name__ == '__main__':
    rospy.init_node('states_test')  # 初始化节点

    # 订阅 /joint_states 话题
    rospy.Subscriber('/joint_states', JointState, joint_states_callback)

    # 创建一个发布者,发布到 /joint_positions 话题
    joint_positions_pub = rospy.Publisher('/joint_positions', Float32MultiArray, queue_size=10)

    rospy.loginfo("states_test node is running, publishing joint positions...")
    rate = rospy.Rate(10)  # 设置频率为 10 Hz

    # 循环等待回调
    #rospy.spin()
    while not rospy.is_shutdown():
        # 循环等待回调
        rate.sleep()

        在这个节点上,也顺便把关节数据映射为角度了,因为弧度转换为角度是浮点数运算,所以为了避免arduino端算力不足,还可以加快运算的速度,就直接在ros端直接把要做的全做了。

        启动这个节点之后,我们也可以再写一个订阅节点,来查看states_to_positions节点运作是否正常。也可以直接rostopic echo /joint_positions

        创建joint_positions_listener.py文件。

#!/usr/bin/env python

import rospy
from std_msgs.msg import Float64MultiArray

def joint_positions_callback(msg):
    # 打印收到的关节位置
    rospy.loginfo("Received joint positions: %s", msg.data)

def main():
    rospy.init_node('joint_positions_listener', anonymous=True)

    # 订阅 /joint_positions 话题
    rospy.Subscriber("/joint_positions", Float64MultiArray, joint_positions_callback)

    # 保持节点运行
    rospy.spin()

if __name__ == '__main__':
    try:
        main()
    except rospy.ROSInterruptException:
        pass

        移动gui的滑块界面,如果数据的范围都是在你设定的范围内,那就是成功订阅到/joint_states并成功转换了。

        回到arduino端,现在到arduino端来订阅/joint_positios的数据了,arduino端的代码建议使用serial1串口一来作为调试串口来输出你订阅到的数据,因为你不知道会不会出现什么问题,然后又找不到问题所在,arduino端的代码如下:

#include <Arduino.h>
#include <ros.h>
#include <std_msgs/Float32MultiArray.h>
#include <Wire.h>
#include <Adafruit_PWMServoDriver.h>

Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver();
#define SERVOMIN  150 
#define SERVOMAX  600 
#define SERVO_FREQ 50 
#define SERVO_NUMS 7

ros::NodeHandle nh;

// 创建 Float32MultiArray 消息
std_msgs::Float32MultiArray joint_positions_msg;

// 回调函数,处理接收到的关节位置消息
void joint_positions_callback(const std_msgs::Float32MultiArray& msg) {
    Serial1.print("Received joint positions: ");
    for (int i = 0; i < msg.data_length; i++) {
      Serial1.print(i);
      Serial1.print(", ");
        Serial1.print(msg.data[i]);
        if (i < msg.data_length - 1) {
            Serial1.print(", "); // 添加逗号分隔
        }
    }
    Serial1.println(); // 换行
}

void servo_set(const std_msgs::Float32MultiArray& msg){
    for(int i=0;i<msg.data_length;i++){
        pwm.setPWM(i,0,msg.data[i]);
    }
}

// 创建订阅者
//ros::Subscriber<std_msgs::Float32MultiArray> sub("/joint_positions", joint_positions_callback);
ros::Subscriber<std_msgs::Float32MultiArray> sub("/joint_positions", servo_set);

void setup() {
    Serial1.begin(57600); // 初始化串口
    Serial1.println("Arduino ROS Node Started");
    pwm.begin();
    pwm.setPWMFreq(SERVO_FREQ);
    delay(10);

    nh.initNode();
    nh.subscribe(sub); // 订阅 joint_positions 话题
}

void loop() {
    nh.spinOnce(); // 处理 ROS 消息
    delay(100); // 设置一个适当的延时
}

        这里串口一是作为我的调试串口来使用的,要特别注意:arduino订阅的数据长度是有限的,例如作者之前使用的是Float64的数组,所以导致回调函数根本就没有触发,因为他数据溢出了,所以要注意你数据的类型,如果你数据太多的话,建议将数据类型缩小,不然就会导致空空如也的情况,使用Float64经过作者一个个的测试之后,发现最多只能容纳5个Float64的数据,多了都会导致回调函数不能正常被触发,所以作者这里将数据类型改成了Float32,如果读者的关节数据没有那么多的话,就可以直接不用管这个了。

        现在有了这么多的节点需要启动,一个个启动实在是太麻烦了,所以作者直接就将要启动的东西写成了一个脚本,直接运行就能实现想要运作的节点。

        选择一个你喜欢的位置来安放你的脚本,作者这里选择的是arm-robot的目录下。

sudo nano start.sh

        写入要启动的东西。

#!/bin/bash

# 确保脚本在失败时停止
set -e

# 源代码环境
source ~/catkin_test/devel/setup.bash

# 启动 Gazebo
roslaunch robot_moveit_config gazebo.launch &

# 等待 Gazebo 启动
sleep 15  # 根据需要调整等待时间

# 启动 joint_state_publisher_gui
rosrun joint_state_publisher_gui joint_state_publisher_gui &

# 启动 joint_states_to_command.py
rosrun marm_description joint_states_to_command.py &

sleep 1

rosrun marm_description states_to_positions.py &

sleep 1

rosrun rosserial_python serial_node.py /dev/ttyUSB0 

        sleep是为了等待进程启动完毕,作者的电脑是比较卡的,所以启动时间就放得长一点了,读者可根据自己的需求来更改。

        在arm-robot下直接执行./start,启动所有的东西,移动你的滑块,就能驱动你的机械臂了。

九、结语

        本文做的gazebo驱动真实机械臂还只是一个很粗略很粗略的东西,如果是想要完全在gazebo中驱动的结果和你在真实驱动的机械臂做到完全一样,那就需要读者自己的画3d模型,然后在states_to_positions.py中,发送数据的时候为每个关节都添加PID控制,这样应该就能做到大致一样了。

附录:

本文的Gitee开源如下:

arm_ws: 在Ubuntu的ros-noetic上,使用gazebo来控制真实的机械臂运作

        也可以直接在你的Ubuntu上运行,这样就能获取到完整的ros工作空间了

git clone https://gitee.com/luxi_an/arm_ws.git

http://www.kler.cn/a/369718.html

相关文章:

  • 什么是Maxscript?为什么要学习Maxscript?
  • HarmonyOS应用开发快速入门
  • 网络爬虫学习:应用selenium获取Edge浏览器版本号,自动下载对应版本msedgedriver,确保Edge浏览器顺利打开。
  • 動態住宅IP提升網站訪問成功率
  • Edge-TTS在广电系统中的语音合成技术的创新应用
  • 使用Redis生成全局唯一ID示例
  • 论文笔记:TELLER 可解释的、可概括的、可控的假新闻检测的可信框架
  • <项目代码>YOLOv8 煤矸石识别<目标检测>
  • 在 macOS 上添加 hosts 文件解析的步骤
  • docker 安装部署 nginx
  • SPA和SSR
  • 构造+置换环,CF 1983D - Swap Dilemma
  • 计算机网络:网络层 —— IP数据报的发送和转发过程
  • php伪协议和move_uploaded_file、rename、copy等文件操作
  • 【python】OpenCV—WaterShed Algorithm(1)
  • 控制卸载/安装应用
  • Chromium HTML5 新的 Input 类型date 对应c++
  • C++基于opencv的视频质量检测--画面冻结检测
  • Vue3中ref、toRef和toRefs之间有什么区别?
  • 基于SSM+微信小程序的快递的管理系统(快递1)
  • 基于脚手架创建前端工程
  • Linux 应用领域
  • 老电脑不能装纯净版windows
  • GEE APP:加载Landsat TOA数据可视化界面,实现点击提取ndvi值
  • 云原生后端开发教程
  • Python实现微博舆情分析的设计与实现