ROS 入门教程(含开发调试配置)
本文最后更新于 41 天前,内容如有失效请评论区留言。

由于安装 ROS 确实很 easy,官方文档已经是保姆级的教程了,这里不在赘述。本文的实验环境是 Ubuntu 20.04 + ROS Noetic Ninjemys

运行第一个 ROS 包

sudo apt install ros-noetic-rqt-robot-steering
sudo apt install ros-noetic-turtlesim
roscore
rosrun rqt_robot_steering rqt_robot_steering
rosrun turtlesim turtlesim_node

image-20240111215342376

image-20240111215609321

从 Github 下载软件包编译

mkdir catkin_ws
cd catkin_ws
mkdir src
cd src
git clone https://github.com/6-robot/wpr_simulation.git
cd wpr_simulation/scripts
./install_for_noetic.sh

然后返回 ~/catkin_ws 目录下,

catkin_make
cd devel
source ~/catkin_ws/devel/setup.zsh 
roslaunch wpr_simulation wpb_simple.launch
rosrun rqt_robot_steering rqt_robot_steering

image-20240111232331871

source ~/catkin_ws/devel/setup.zsh 添加到 ~/.zshrc 中。

image-20240112001510160

VSCode 配置开发环境

在 VSCode 中打开 ~/catkin_ws/src 文件夹,安装 ROS 和 CMake Tools 插件

image-20240111234738068

按住 Ctrl + Shift + B,选择 catkin_make: build 选项,就可以编译代码了。

此外,点击 catkin_make: build 旁边的设置齿轮图标,进入 task.json,修改 group 字段后内容如下:

{
    "version": "2.0.0",
    "tasks": [
        {
            "type": "catkin_make",
            "args": [
                "--directory",
                "/home/lrl52/catkin_ws",
                "-DCMAKE_BUILD_TYPE=RelWithDebInfo"
            ],
            "problemMatcher": [
                "$catkin-gcc"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "label": "catkin_make: build"
        }
    ]
}

此后直接按住 Ctrl + Shift + B 快捷键就可以编译了。

关闭 CMake 插件的自动配置,

image-20240111235649335

创建第一个 Node 结点

catkin_create_pkg ssr_pkg rospy roscpp std_msgs

image-20240111235923631

创建后得到如下文件夹和文件,

image-20240112000126195

image-20240112000402123

image-20240112000213100

创建 src/chao_node.cpp,编写如下代码:

#include <ros/ros.h>

int main(int argc, char *argv[]) {
    printf("hello, ros world!\n");
    return 0;
}

在 CMakeLists.txt 里添加 add_executable(chao_node src/chao_node.cpp)

快捷键编译,然后

roscore
rosrun ssr_pkg chao_node

可以看到终端输出了 hello, ros world!

完善上述代码,

#include <ros/ros.h>

int main(int argc, char *argv[]) {

    ros::init(argc, argv, "chao_node");

    printf("Hello, ros world!\n");
    while (ros::ok()) {
        printf("ROS is running\n");
    }    
    return 0;
}

在 CMakeLists.txt 里添加 target_link_libraries(chao_node ${catkin_LIBRARIES})。完整 CMakeLists.txt 如下:

cmake_minimum_required(VERSION 3.0.2)
project(ssr_pkg)

find_package(catkin REQUIRED COMPONENTS
  roscpp
  rospy
  std_msgs
)

catkin_package(
#  INCLUDE_DIRS include
#  LIBRARIES ssr_pkg
#  CATKIN_DEPENDS roscpp rospy std_msgs
#  DEPENDS system_lib
)

include_directories(
# include
  ${catkin_INCLUDE_DIRS}
)

add_executable(chao_node src/chao_node.cpp)

target_link_libraries(chao_node
  ${catkin_LIBRARIES}
)

再次编译运行,得到如下结果

image-20240112002509957

Topic 与 Message

std_msgs – ROS Wiki

Publisher 实现

chao_node.cpp 中编写如下代码,

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

int main(int argc, char *argv[]) {

    ros::init(argc, argv, "chao_node");

    ros::NodeHandle nh;
    ros::Publisher pub = nh.advertise<std_msgs::String>("topic_name", 25); // ("话题名称", 缓冲区长度)

    ros::Rate loop_rate(25); // 每秒 25 次

    printf("Hello, ros world!\n");
    while (ros::ok()) {
        std_msgs::String msg;
        msg.data = "message from chao_node";
        pub.publish(msg); // 发布话题
        // printf("ROS is running\n");
        loop_rate.sleep();
    }    
    return 0;
}

编译,然后

rosrun
rosrun ssr_pkg chao_node

使用 rostopic list 可以查看当前 ROS 有哪些已经发布的 topic,

image-20240112092750253

使用 rostopic echo /topic_name 查看对于 topic 里的 message。rostopic hz /topic_name 可以查看 topic 的发送频率。

image-20240112092918154

Subscriber 实现

catkin_create_pkg atr_pkg rospy roscpp std_msgs 创建一个新的 package。编写如下代码:

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

void chao_callback(const std_msgs::String &msg) {
    // std::cout << msg.data << "\n";
    ROS_INFO(msg.data.c_str());
    ROS_WARN(msg.data.c_str());
}

int main(int argc, char *argv[]) {

    ros::init(argc, argv, "ma_node");

    ros::NodeHandle nh;
    ros::Subscriber sub = nh.subscribe("topic_name", 10, chao_callback);

    while (ros::ok()) {
        ros::spinOnce();
    }

    return 0;
}

补全 CMakelists.txt 的内容(同上),接着

roscore
src rosrun ssr_pkg chao_node
src rosrun atr_pkg ma_node

可以看到效果如下图

image-20240112100640249

时间戳是从 1970 年 1 月 1 日开始的 UNIX 时间戳,即当前距离 1970 年 1 月 1 日的秒数。

使用 rqt_graph 可以图形化显示 topic 通讯关系,

image-20240112101008321

另外注意,话题并不属于发布者或者订阅者,而是由 ROS 系统创建管理的,只要向 NodeHandle 提出了话题订阅需求或者话题发布需求,话题就会自动创建。

添加一个新的 Publisher 节点

ssr_pkg/src 下新建 yao_node.cpp,编写如下代码:

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

int main(int argc, char *argv[]) {

    ros::init(argc, argv, "yao_node");

    ros::NodeHandle nh;
    ros::Publisher pub = nh.advertise<std_msgs::String>("yao_topic", 25); // ("话题名称", 缓冲区长度)

    ros::Rate loop_rate(25); // 每秒 25 次

    printf("Hello, ros world!\n");
    while (ros::ok()) {
        std_msgs::String msg;
        msg.data = "message from yao_node";
        pub.publish(msg); // 发布话题
        // printf("ROS is running\n");
        loop_rate.sleep();
    }    
    return 0;
}

更改 atr_pkg/src/ma_node.cpp 中的代码为如下:

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

void chao_callback(const std_msgs::String &msg) {
    ROS_INFO(msg.data.c_str());
}

void yao_callback(const std_msgs::String &msg) {
    ROS_WARN(msg.data.c_str());
}

int main(int argc, char *argv[]) {

    ros::init(argc, argv, "ma_node");

    ros::NodeHandle nh;
    ros::Subscriber sub1 = nh.subscribe("chao_topic", 10, chao_callback);
    ros::Subscriber sub2 = nh.subscribe("yao_topic", 10, yao_callback);

    while (ros::ok()) {
        ros::spinOnce();
    }

    return 0;
}

同时在 ssr_pkg/CMakeLists.txt 中添加

add_executable(yao_node src/yao_node.cpp)
target_link_libraries(yao_node
  ${catkin_LIBRARIES}
)

运行效果如下:

image-20240112170458344

image-20240112170518314

Python 实现 Publisher 和 Subscriber

chao_node 为例,

catkin_create_pkg ssr_pypkg rospy roscpp std_msgs
cd ..
catkin_make # py 本身不需要编译,这里只是为了让 ROS 能够找到这个包

然后新建 scripts 文件夹,添加 py 文件,

image-20240112181243820

编写如下 chao_node.py 代码:

#!/usr/bin/env python3
#coding=utf-8

import rospy
from std_msgs.msg import String

if __name__ == "__main__":
    rospy.init_node("chao_node")
    rospy.logwarn("Hello, ros world from python!")

    pub = rospy.Publisher("chao_room", String, queue_size=10)
    rate = rospy.Rate(15)

    while not rospy.is_shutdown():
        rospy.loginfo("hahaha")
        msg = String()
        msg.data = "message from chao_node"
        pub.publish(msg)
        rate.sleep()

yao_node.py 也是同理,这里略去。再创建 atr_pypkg 包,

catkin_create_pkg atr_pypkg rospy roscpp std_msgs
cd ..
catkin_make

编写如下 ma_node.py 代码:

#!/usr/bin/env python3
#coding=utf-8

import rospy
from std_msgs.msg import String

def chao_callback(msg : String) -> None:
    rospy.loginfo(msg.data)

def yao_callback(msg : String) -> None:
    rospy.logwarn(msg.data)

if __name__ == "__main__":
    rospy.init_node("ma_node")

    sub1 = rospy.Subscriber("chao_room", String, chao_callback, queue_size=10)
    sub2 = rospy.Subscriber("yao_room", String, yao_callback, queue_size=10)

    rospy.spin()

使用 launch 文件启动多个 ROS 节点

递归创建 atr_pkg/launch/start.launch,编写如下启动 XML 文件,

<launch>
    <node pkg="ssr_pkg" type="yao_node" name="yao_node"/>
    <node pkg="ssr_pkg" type="chao_node" name="chao_node" launch-prefix="gnome-terminal -e"/>
    <node pkg="atr_pkg" type="ma_node" name="ma_node" output="screen"/>
</launch>

相关解释如下:

image-20240112164300674

效果如下图

image-20240112172000567

机器人运动控制

ROS Index – geometry_msgs

运动控制使用的是 geometry_msgs/Twist Message 速度消息包,可发送六轴数据

image-20240114164701083

运动控制的话题名称一般是 /cmd_vel

image-20240114164908485

实现思路如下:

image-20240114170144027

catkin_create_pkg vel_pkg rospy roscpp geometry_msgs
cd ..
catkin_make

C++ 版本

vel_pkg/src 中创建 vel_node.cpp,编写如下代码:

#include <ros/ros.h>
#include <geometry_msgs/Twist.h>

int main(int argc, char *argv[]) {
    ros::init(argc, argv, "vel_node");

    ros::NodeHandle n;
    ros::Publisher vel_pub = n.advertise<geometry_msgs::Twist>("/cmd_vel", 10);

    geometry_msgs::Twist vel_msg;
    vel_msg.linear.x = 0.1;
    vel_msg.linear.y = 0;
    vel_msg.linear.z = 0;
    vel_msg.angular.x = 0;
    vel_msg.angular.y = 0;
    vel_msg.angular.z = 0;

    ros::Rate rate(50);
    while (ros::ok()) {
        vel_pub.publish(vel_msg);
        rate.sleep();
    }

    return 0;
}

在 CMakeLists.txt 添加如下规则:

add_executable(vel_node src/vel_node.cpp)
target_link_libraries(vel_node
  ${catkin_LIBRARIES}
)

然后

roscore
roslaunch wpr_simulation wpb_simple.launch
rosrun vel_pkg vel_node

image-20240114172849627

Python 版本

vel_pkg/scripts 中创建 vel_node.py,编写如下代码:

#!/usr/bin/env python3
# coding=utf-8

import rospy
from geometry_msgs.msg import Twist

if __name__ == "__main__":
    rospy.init_node("vel_node")

    vel_pub = rospy.Publisher("/cmd_vel", Twist, queue_size=10)

    vel_msg = Twist()
    vel_msg.linear.x = -0.1

    rate = rospy.Rate(50)
    while not rospy.is_shutdown():
        vel_pub.publish(vel_msg)
        rate.sleep()

运行方式同上,效果同上。

有趣的是,我发现这个 topic 名称填写 /cmd_velcmd_vel 都可以。

image-20240114180657780

激光雷达与使用 RViz 观测传感器数据

  • Gazebo 是模拟真是机器人发出传感器数据的工具。

  • RViz 是接收传感器数据并负责进行显示的工具。

image-20240115162527924

简单操作流程图

image-20240115164805701

image-20240115164921214

然后同样的方式添加 LazerScan,把左侧的 LazerScan 的 Topic 设置为 /scan,同时可以调整 Size,

image-20240115165344858

image-20240115165854803

保存配置文件,

image-20240115170143154

之后再重新打开即可。

获取激光雷达数据

ROS Index – sensor_msgs

[sensor_msgs/LaserScan Message](sensor_msgs/LaserScan Message)

image-20240115171620412

image-20240115172245756

C++ 版本

image-20240115173222523

image-20240115173508009

查看 LazerScan 消息包:

rostopic echo /scan --noarr

image-20240115175133352

catkin_create_pkg lidar_pkg roscpp rospy sensor_msgs

src/lidar_node.cpp 中编写如下代码:

#include <ros/ros.h>
#include <sensor_msgs/LaserScan.h>

void LidarCallback(const sensor_msgs::LaserScan msg) {
    auto dis = msg.ranges[179];
    ROS_INFO("前方测距 range[179] = %f 米", dis);
}   

int main(int argc, char *argv[]) {
    setlocale(LC_ALL, "");
    ros::init(argc, argv, "lidar_node");

    ros::NodeHandle nh;
    ros::Subscriber sub = nh.subscribe<sensor_msgs::LaserScan>("/scan", 10, LidarCallback);

    ros::spin();

    return 0;
}

修改 CMakeLists.txt 文件,添加如下规则。(从这之后不再赘述了)

add_executable(lidar_node src/lidar_node.cpp)
target_link_libraries(lidar_node
  ${catkin_LIBRARIES}
)

效果如下:

image-20240115180805893

Python 版本

#!/usr/bin/env python3
#coding=utf-8

import rospy
from sensor_msgs.msg import LaserScan

def LidarCallback(msg : LaserScan):
    dis = msg.ranges[180]
    rospy.loginfo("前方测距 ranges[180] = {} 米".format(dis))

if __name__ == "__main__":
    rospy.init_node("lidar_node")

    sub = rospy.Subscriber("/scan", LaserScan, LidarCallback, queue_size=10)
    rospy.spin()

激光雷达避障

image-20240116140243034

image-20240116140343030

C++ 版本

lidar_node.cpp 中完善代码:

#include <ros/ros.h>
#include <sensor_msgs/LaserScan.h>
#include <geometry_msgs/Twist.h>

ros::Publisher vel_pub;
static int nCount = 0;

void LidarCallback(const sensor_msgs::LaserScan msg) {
    auto dis = msg.ranges[179];
    ROS_INFO("前方测距 range[179] = %f 米", dis);

    if (nCount > 0) {
        nCount--;
        return;
    }

    geometry_msgs::Twist vel_msg;
    if (dis < 1.5f) {
        vel_msg.angular.z = 0.5;
        nCount = 50;
    } else {
        vel_msg.linear.x = 0.05;
    }
    vel_pub.publish(vel_msg);
}   

int main(int argc, char *argv[]) {
    setlocale(LC_ALL, "");
    ros::init(argc, argv, "lidar_node");

    ros::NodeHandle nh;
    ros::Subscriber sub = nh.subscribe<sensor_msgs::LaserScan>("/scan", 10, LidarCallback);
    vel_pub = nh.advertise<geometry_msgs::Twist>("/cmd_vel", 10);

    ros::spin();

    return 0;
}

但我有两个问题还不太能理解:

  1. 为什么 LaserScan 消息包的 scan_timetime_increment 字段是 0
  2. 在创建包的时候并没有引入 geometry_msgs 依赖,似乎没有什么影响

UP 主的补充回答:

  1. 仿真环境是这样的,只有真机跑起来才有时间

  2. 创建包的时候填写的依赖项,会被写在 CMakeLists.txt 和 package.xml 里。这个依赖项有点意思,我发现只要有 roscpp 就可以使用 ROS 内置的消息类型。猜想可能是 roscppConfig.cmake 的设置有关

image-20240116144056866

image-20240116143731598

Python 版本

lidar_node.py 中完善代码:

#!/usr/bin/env python3
#coding=utf-8

import rospy
from sensor_msgs.msg import LaserScan
from geometry_msgs.msg import Twist

nCount = 0

def LidarCallback(msg : LaserScan):
    global nCount, vel_pub
    dis = msg.ranges[180]
    rospy.loginfo("前方测距 ranges[180] = {:.4f} 米".format(dis))

    vel_msg = Twist()
    if nCount > 0:
        nCount -= 1
        rospy.logwarn("持续转向 nCount = {}".format(nCount))
        return
    if dis < 1.5:
        vel_msg.angular.z = 0.3
        nCount = 50
    else:
        vel_msg.linear.x = 0.05
    vel_pub.publish(vel_msg)

if __name__ == "__main__":
    rospy.init_node("lidar_node")

    sub = rospy.Subscriber("/scan", LaserScan, LidarCallback, queue_size=10)
    vel_pub = rospy.Publisher("/cmd_vel", Twist, queue_size=10)
    rospy.spin()

获取 IMU 数据

ROS Index – sensor_msgs

[sensor_msgs/Imu Message](sensor_msgs/Imu Message)

image-20240116160031817

image-20240116160308002

image-20240116160407573

catkin_create_pkg imu_pkg roscpp rospy sensor_msgs

C++ 版本

imu_pkg/src 中编写 imu_node.cpp

#include <ros/ros.h>
#include <sensor_msgs/Imu.h>
#include <tf/tf.h>

void IMUCallback(const sensor_msgs::Imu msg) {
    if (msg.orientation_covariance[0] < 0.0) return;

    tf::Quaternion q(
        msg.orientation.x,
        msg.orientation.y,
        msg.orientation.z,
        msg.orientation.w
    );
    double roll, pitch, yaw;
    tf::Matrix3x3(q).getRPY(roll, pitch, yaw);

    ROS_INFO("roll = %f, pitch = %f, yaw = %f", roll * 180 / M_PI, pitch * 180 / M_PI, yaw * 180 / M_PI);
}

int main(int argc, char *argv[]) {
    setlocale(LC_ALL, "");
    ros::init(argc, argv, "imu_data");

    ros::NodeHandle nh;
    ros::Subscriber sub = nh.subscribe("imu/data", 100, IMUCallback);

    ros::spin();

    return 0;
}

效果如下:

image-20240116165246920

Python 版本

#!/usr/bin/env python3
#coding=utf-8

import rospy
from sensor_msgs.msg import Imu
from tf.transformations import euler_from_quaternion
import math

def IMUCallback(msg : Imu):
    if msg.orientation_covariance[0] < 0:
        return
    q = [msg.orientation.x, msg.orientation.y,
         msg.orientation.z, msg.orientation.w]
    roll, pitch, yaw = euler_from_quaternion(q)
    rospy.loginfo("roll = {:.2f}, pitch = {:.2f}, yaw = {:.2f}".format(
        roll * 180 / math.pi,
        pitch * 180 / math.pi,
        yaw * 180 / math.pi
    ))

if __name__ == "__main__":
    rospy.init_node("imu_node")
    imu_sub = rospy.Subscriber("/imu/data", Imu, IMUCallback, queue_size=10)
    rospy.spin()

IMU 实现航向锁定

C++ 版本

继续在 imu_pkg/src 中完善 imu_node.cpp

#include <ros/ros.h>
#include <sensor_msgs/Imu.h>
#include <tf/tf.h>
#include <geometry_msgs/Twist.h>

ros::Publisher vel_pub;

void IMUCallback(const sensor_msgs::Imu msg) {
    if (msg.orientation_covariance[0] < 0.0) return;

    tf::Quaternion q(
        msg.orientation.x,
        msg.orientation.y,
        msg.orientation.z,
        msg.orientation.w
    );
    double roll, pitch, yaw;
    tf::Matrix3x3(q).getRPY(roll, pitch, yaw);

    ROS_INFO("roll = %f, pitch = %f, yaw = %f", roll * 180 / M_PI, pitch * 180 / M_PI, yaw * 180 / M_PI);

    geometry_msgs::Twist vel_cmd;
    double target_yaw = 90;
    double diff_yaw = target_yaw - yaw * 180 / M_PI;
    vel_cmd.angular.z = diff_yaw * 0.01;
    vel_cmd.linear.x = 0.1;
    vel_pub.publish(vel_cmd);
}

int main(int argc, char *argv[]) {
    setlocale(LC_ALL, "");
    ros::init(argc, argv, "imu_data");

    ros::NodeHandle nh;
    ros::Subscriber sub = nh.subscribe("imu/data", 100, IMUCallback);
    vel_pub = nh.advertise<geometry_msgs::Twist>("/cmd_vel", 10);

    ros::spin();

    return 0;
}

Python 版本

#!/usr/bin/env python3
# coding=utf-8

import rospy
from sensor_msgs.msg import Imu
from tf.transformations import euler_from_quaternion
import math
from geometry_msgs.msg import Twist

# IMU 回调函数
def imu_callback(msg):
    if msg.orientation_covariance[0] < 0:
        return
    # 四元数转成欧拉角
    quaternion = [
        msg.orientation.x,
        msg.orientation.y,
        msg.orientation.z,
        msg.orientation.w
    ]
    (roll,pitch,yaw) = euler_from_quaternion(quaternion)
    # 弧度换算成角度
    roll = roll*180/math.pi
    pitch = pitch*180/math.pi
    yaw = yaw*180/math.pi
    rospy.loginfo("滚转= %.0f 俯仰= %.0f 朝向= %.0f", roll, pitch, yaw)
    # 速度消息包
    vel_cmd = Twist()
    # 目标朝向角
    target_yaw = 90
    # 计算速度
    diff_angle = target_yaw - yaw
    vel_cmd.angular.z = diff_angle * 0.01
    vel_cmd.linear.x = 0.1
    global vel_pub
    vel_pub.publish(vel_cmd)

# 主函数
if __name__ == "__main__":
    rospy.init_node("imu_behavior")
    # 订阅 IMU 的数据话题
    imu_sub = rospy.Subscriber("/imu/data",Imu,imu_callback,queue_size=10)
    # 发布机器人运动控制话题
    vel_pub = rospy.Publisher("cmd_vel",Twist,queue_size=10)
    rospy.spin()

ROS 中的消息包

标准消息包

image-20240117164554515

常用消息包

image-20240117165259977

几何消息包:可以看到里面一些消息包带了 Stamped 后缀关键词,这些消息包都是多了一个 Header,多了一个时间和空间坐标系 ID,将空间量和时间量进行了绑定,在一些预测和滤波算法里会用得着。

image-20240117171457233

传感器消息包:

image-20240117171717682

自定义消息包

创建自定义消息包

image-20240117173827148

生成自定义消息包的步骤:

image-20240117174141770

catkin_create_pkg qq_msgs roscpp rospy std_msgs message_generation message_runtime

创建 msg 文件夹和 Carry.msg

string grade
int64 star
string data

然后修改 CMakeLists.txt,重新编译。终端中输入:

rosmsg show qq_msgs/Carry

image-20240117180125496
可以看到新的自定义消息包诞生了。

C++ 版本

image-20240117180703284

修改 chao_node.cppma_node.cpp 以及对应的 CMakeLists.txt 和 package.xml 文件,最后运行效果如下:

image-20240117182758609

Python 版本

image-20240118101656160

Python 不需要在编译规则中添加 add_dependencies()

image-20240118101452469

💥未完待更~

ROS 中的栅格地图

ROS – Index:map_server

ROS – Wiki:map_server

image-20240118102748650

image-20240118102911361

image-20240118103902094

image-20240118104119540

下面使用在 ROS 中发布自定义地图,

C++ 版本

image-20240118142737538

catkin_create_pkg map_pkg rospy roscpp nav_msgs

map_pkg/src/map_pub_node.cpp 编写如下代码:

#include <ros/ros.h>
#include <nav_msgs/OccupancyGrid.h>

int main(int argc, char *argv[]) {
    ros::init(argc, argv, "map_pub_node");

    ros::NodeHandle nh;
    ros::Publisher pub = nh.advertise<nav_msgs::OccupancyGrid>("/map", 10);

    ros::Rate rate(10);

    while (ros::ok()) {
        nav_msgs::OccupancyGrid msg;

        msg.header.frame_id = "map";
        msg.header.stamp = ros::Time::now();

        msg.info.origin.position.x = 2;
        msg.info.origin.position.y = 3;
        msg.info.resolution = 1.0;
        msg.info.width = 4;
        msg.info.height = 2;

        msg.data.resize(msg.info.width * msg.info.height);
        msg.data[0] = 100;
        msg.data[1] = 100;
        msg.data[2] = 0;
        msg.data[3] = -1;

        pub.publish(msg);
        rate.sleep();
    }
}

然后修改 CMakeLists.txt 添加编译规则,运行

rosrun map_pkg map_pub_node
rviz

在 RViz 中点击左下角的 Add,添加 Axes 和 Map,

image-20240118144918925

Map 话题名称设置为 /map

image-20240118144756787

Python 版本

#!/usr/bin/env python3
#coding=utf-8

import rospy
from nav_msgs.msg import OccupancyGrid

if __name__ == "__main__":
    rospy.init_node("map_pub_node")
    pub = rospy.Publisher("/map", OccupancyGrid, queue_size=10)

    rate = rospy.Rate(10)
    while not rospy.is_shutdown():
        msg = OccupancyGrid()

        msg.header.frame_id = "map"
        msg.header.stamp = rospy.Time.now()

        msg.info.origin.position.x = 0
        msg.info.origin.position.y = 0
        msg.info.resolution = 1.0
        msg.info.width = 4
        msg.info.height = 2

        msg.data = [0] * msg.info.width * msg.info.height
        msg.data[0] = 100
        msg.data[1] = 100
        msg.data[2] = 0
        msg.data[3] = -1

        pub.publish(msg)
        rate.sleep()

Hector_Mapping 初体验

ROS – Index:hector_mapping

image-20240118164311767

roslaunch wpr_simulation wpb_stage_slam.launch
rosrun hector_mapping hector_mapping
rosrun rviz rviz
rosrun rqt_robot_steering rqt_robot_steering

image-20240118171111567

image-20240118171335066

通过 lanuch 文件启动

<launch>

  <!-- 载入 机器人 和 SLAM 的仿真场景 -->
  <include file="$(find wpr_simulation)/launch/wpb_stage_slam.launch"/>

  <!-- Hector SLAM -->
  <node pkg="hector_mapping" type="hector_mapping" name="hector_mapping"></node>

  <!-- Rviz 显示 -->
  <node pkg="rviz" type="rviz" name="rviz" args="-d $(find slam_pkg)/rviz/slam.rviz"/>

  <!-- 运动控制 -->
  <node pkg="rqt_robot_steering" type="rqt_robot_steering" name="rqt_robot_steering"/>

</launch>

可以给 hector_mapping 添加参数,增快建图速度。具体见 wpr_simulation/launch/wpb_hector_comparison.launch 文件。

roslaunch wpr_simulation wpb_hector_comparison.launch

image-20240118175500954

初识 ROS 中的 TF 系统

image-20240118175910463

image-20240118175949866

image-20240118180120896

tf2_msgs

roslaunch slam_pkg hector.launch
rostopic list
rostopic type /tf
rostopic echo /tf
rosrun rqt_tf_tree rqt_tf_tree

tf 的消息类型为 tf2_msgs/TFMessage,可以看到 tf 的消息如下:

---
transforms:
  -
    header:
      seq: 0
      stamp:
        secs: 772
        nsecs: 262000000
      frame_id: "odom"
    child_frame_id: "base_footprint"
    transform:
      translation:
        x: 2.5370301864651945
        y: 1.5186889059358941
        z: 0.0
      rotation:
        x: 0.0
        y: 0.0
        z: 0.95114390602314
        w: 0.30874790693224397
---

rqt_tf_tree 可以显示 TF 树,可以看到空间坐标系的父子关系。

初识里程计

image-20240119155654640

image-20240119160001097

这一节内容比较复杂,具体见 B站-什么是里程计。有讲 hector 建图和 gmapping 建图算法的区别。

使用 Gmapping 进行 SLAM 建图

roslaunch wpr_simulation wpb_stage_robocup.launch
rostopic echo /scan --noarr
rosrun gmapping slam_gmapping
rosrun rviz rviz
rosrun wpr_simulation keyboard_vel_ctrl

image-20240119164451924

由于撞到了东西,使参照物发生了移动,可以看到建图与原地图有初入。

image-20240119165327295

然呃因为 gazebo 在强制关闭后再启动报错,我尝试寻找解决办法,

killall gzserver
killall gzclient

这下启动依旧报错,甚至重启 ubuntu 都没法解决问题了😰。暂时没有找到原因和解决办法。

gmapping lanch 文件如下(还没测试过):

<launch>
    <include file="$(find wpr_simulation)/launch/wpb_stage_robocup.launch"/>
    <node pkg="gmapping" type="slam_gmapping" name="slam_gmapping"/>
    <node pkg="rviz" type="rviz" name="rviz" args="-d $(find slam_pkg)/rviz/gmapping.rviz"/>
    <node pkg="wpr_simulation" type="keyboard_ver_ctrl" name="keyboard_ver_ctrl"/>
</launch>

Gmapping 建图的参数设置

image-20240120194438132

image-20240120194824038

地图保存与加载

ROS – Index:map_server

地图的保存:

rosrun map_server map_saver -f mymap

保存后会得到 mymap.pgmmymap.yaml 两个文件。其中 mymap.pgm 是图像文件,可以直接打开看,后者 mymap.yaml 是地图配置文件,内容如下:

image: mymap.pgm
resolution: 0.025000 #地图分辨率
origin: [-12.812499, -12.812499, 0.000000] #地图左下角的坐标和地图偏转角度
negate: 0
occupied_thresh: 0.65
free_thresh: 0.196

image-20240120201623872

地图还原:

rosrun map_server map_server mymap.yaml

VSCode 配置 ROS 调试环境

配置 X11-Forwarding

编辑 /etc/ssh/sshd_config,确认 X11Forwarding 已经打开。

#AllowAgentForwarding yes
#AllowTcpForwarding yes
#GatewayPorts no
X11Forwarding yes
#X11DisplayOffset 10
#X11UseLocalhost yes
#PermitTTY yes

安装 VScode 关于 X11 的拓展,

image-20240122095326185

在 Remote X11 的拓展中,选择设置,删除以下环境变量(笔者在这里被这个扩展卡了一会):

LIBGL_ALWAYS_INDIRECT=1

然后关闭 VScode,重新打开一个新窗口,输入 SSH 连接命令,-X 选项表示启用 X11 转发。

ssh -X lrl52@192.168.217.130

保存配置,打开 ssh config,可以看到 ForwardX11 被设置为了 yes

image-20240122100400317

此时在 VSCode 的终端输入 echo $DISPLAY,可以发现 DISPLAY 环境变量已经被设置,

> echo $DISPLAY
localhost:10.0

回到虚拟机,输入同样的命令,

> echo $DISPLAY
:0

我希望在 VSCode 运行 GUI 程序时,GUI 程序能够在虚拟机正常运行显示,这样便于在 VScode 中启动节点进行调试。因此需要把 VSCode 终端中的 DISPLAY 的值修改为 :0

export DISPLAY=:0

然后在 VScode 终端中输入 rviz 测试,可以发现启动成功。

image-20240122101225080

为了避免每次打开都手动修改环境变量,编辑 ~/.zshrc 添加如下脚本:

# Check if VSCODE_INJECTION exists and equals to 1
if [[ $VSCODE_INJECTION = 1 ]]; then
    export DISPLAY=:0
fi

这样在 VScode 中打开的终端就会自动设置该环境变量啦,其它的终端则不受影响。

配置调试

创建 launch.json 文件,添加 ROS: LaunchROS: Attach 模板配置。修改 launch 部分的启动路径:

{
    // 使用 IntelliSense 了解相关属性。 
    // 悬停以查看现有属性的描述。
    // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "ROS: Attach",
            "type": "ros",
            "request": "attach"
        },
        {
            "name": "ROS: Launch",
            "type": "ros",
            "request": "launch",
            "target": "/home/lrl52/shenlan_ws/src/grid_path_searcher/launch/demo.launch"
        }
    ]
}

调试之前,删除之前的 build 和 devel 文件夹,重新构建项目,生成调试 symbol。执行:

catkin_make -DCMAKE_BUILD_TYPE=Debug -j

可以在 VSCode 中后台启动 ROS。确保调试时 ROS master 进行在运行中。

image-20240122102305705

尝试 ROS: Launch 调试,单步调试完美启动:

image-20240122102521722

但目前笔者尚不清楚 RViz 为什么不会在后台启动运行,即使去掉所有断点也不行。

这时候还可以尝试另一种要求更低,更通用的调试办法,那就是 GDB Attach

选择 ROS: Attach,选择 demo_node 进程:

image-20240122103008243

此时可能会提示

image-20240122103054217

笔者发现不管你选择 y or N 都没法进行调试,在这里又被卡了一会,原因是 Linux 默认系统内核参数 kernel.yama.ptrace 设置为1(True),此时普通用户进程是不能对其他进程进行 attach 操作的。更改办法见非root权限进行gdb attach调试。执行命令:

sudo sysctl kernel/yama/ptrace_scope=0

此时再次尝试 Attach 就可以成功调试了。Oh wowowowhahaha!🎉

(另外如果调试的时候,调试当前行上下乱跳,记着检查一下各个包的 CMakeLists.txt ,一定要把所有 O2, O3 优化都关掉)

image-20240122103447750

References

GitHub: vscode-ros

GitHub: vscode-ros-debug-support

What are CMAKE_BUILD_TYPE: Debug, Release, RelWithDebInfo and MinSizeRel?

使用 ssh 客户端的 X11-forwarding 在远程 linux 主机上运行 GUI 程序

非 root 权限进行 gdb attach 调试

B 站 – ROS 快速入门教程

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇