ROS 学习记录(三)——ROS 通信机制实践

文章目录

话题通信

话题发布

在安装完 ROS 的测试样例中,系统提供了一个 turtlesim_node 节点,用于显示乌龟移动路径。我们要尝试的就是向这个节点发布话题,让它显示我们自定义的移动轨迹。

首先运行 ros 和 turtlesim_node 节点:

1roscore &
2rosrun turtlesim turtlesim_node

查看当前可用话题:

1rostopic list

输出:

1/rosout
2/rosout_agg
3/turtle1/cmd_vel
4/turtle1/color_sensor
5/turtle1/pose

可以看到,turtlesim_node 节点提供了三个话题,而我们要发布的轨迹信息就发布在 /turtle1/cmd_vel 话题上。

用下面的命令查看 /turtle1/cmd_vel 话题的类型:

1rostopic type /turtle1/cmd_vel

输出:

1geometry_msgs/Twist

可以看到,/turtle1/cmd_vel 话题的类型是 geometry_msgs/Twist。再查看一下这个类型的定义:

1rosmsg show geometry_msgs/Twist

输出:

1geometry_msgs/Vector3 linear
2  float64 x
3  float64 y
4  float64 z
5geometry_msgs/Vector3 angular
6  float64 x
7  float64 y
8  float64 z

可以看到,/turtle1/cmd_vel 话题的消息类型是 geometry_msgs/Twist,其中包含两个成员变量 linear 和 angular,分别表示线速度和角速度。我们希望乌龟做匀速圆周运动,因此设定其线速度为 1,角速度为 0.5,即 linear.x = 1, angular.z = 0.5 并发布到 /turtle1/cmd_vel 话题。随后开始编写代码。

C++ 实现:

 1#include "ros/ros.h"
 2#include "geometry_msgs/Twist.h"
 3
 4int main(int argc, char **argv)
 5{
 6    setlocale(LC_ALL, "");
 7    ros::init(argc, argv, "topic_publish_cpp");
 8    ros::NodeHandle n;
 9
10    ros::Publisher publisher = n.advertise<geometry_msgs::Twist>("/turtle1/cmd_vel", 1000);
11    geometry_msgs::Twist twist;
12    twist.linear.x = 1.0;
13    twist.linear.y = 0.0;
14    twist.linear.z = 0.0;
15    twist.angular.x = 0.0;
16    twist.angular.y = 0.0;
17    twist.angular.z = 0.5;
18
19    ros::Rate rate(10);
20    while (ros::ok())
21    {
22        publisher.publish(twist);
23        ros::spinOnce();
24        rate.sleep();
25    }
26    return 0;
27}

Python 实现:

 1import rospy
 2from geometry_msgs.msg import Twist
 3
 4
 5def main():
 6    rospy.init_node("topic_publish_py")
 7    publisher = rospy.Publisher("/turtle1/cmd_vel", Twist, queue_size=1000)
 8
 9    twist = Twist()
10    twist.linear.x = 1.0
11    twist.linear.y = 0.0
12    twist.linear.z = 0.0
13    twist.angular.x = 0.0
14    twist.angular.y = 0.0
15    twist.angular.z = 0.5
16
17    rate = rospy.Rate(10)
18    while not rospy.is_shutdown():
19        publisher.publish(twist)
20        rate.sleep()
21
22
23if __name__ == "__main__":
24    main()

修改 launch 文件:

1<launch>
2    <node pkg="turtle" type="topic_publish_cpp_node" name="topic_publish_cpp_node" />
3    <node pkg="turtle" type="topic_publish_py.py" name="topic_publish_py_node" />
4
5    <node pkg="turtlesim" type="turtlesim_node" name="turtlesim_node" required="true" />
6</launch>

required 属性表示这个节点必须运行,如果该节点关闭则通过当前 launch 文件启动的节点也会关闭。

运行:

1roslaunch turtle topic_publish.launch

随后就可以看见小乌龟做匀速圆周运动了。

话题订阅

刚才在执行 rostopic list 时,有一个话题为 /turtle1/pose,这个话题是 turtlesim_node 节点发布的,用于显示当前乌龟的位置信息。我们可以订阅这个话题,并在控制台打印出来。

查看 /turtle1/pose 话题的类型:

1rostopic type /turtle1/pose

输出:

1turtlesim/Pose

可以看到,/turtle1/pose 话题的类型是 turtlesim/Pose,这个类型定义在 turtlesim_node 包中。我们可以查看这个类型的定义:

1rosmsg show turtlesim/Pose

输出:

1float32 x
2float32 y
3float32 theta
4float32 linear_velocity
5float32 angular_velocity

这些参数分别代表了当前乌龟的 x 坐标、y 坐标、朝向角、线速度和角速度。我们可以编写代码订阅这个话题并将信息打印出来。

C++ 实现:

 1#include "ros/ros.h"
 2#include "turtlesim/Pose.h"
 3#include <cstdlib>
 4
 5void callback(const turtlesim::Pose::ConstPtr &msg)
 6{
 7    printf("\033[H");
 8    printf("Turtle Position Information:    \nx=%.3f    \ny=%.3f    \ntheta=%.3f    \nlinear_velocity=%.3f    \nangular_velocity=%.3f    \n",
 9           msg->x,
10           msg->y,
11           msg->theta,
12           msg->linear_velocity,
13           msg->angular_velocity);
14}
15
16int main(int argc, char **argv)
17{
18    setlocale(LC_ALL, "");
19    ros::init(argc, argv, "topic_subscribe_cpp");
20    ros::NodeHandle n;
21    std::system("clear");
22    ros::Subscriber subscriber = n.subscribe<turtlesim::Pose>("/turtle1/pose", 1000, callback);
23    ros::spin();
24    return 0;
25}

Python 实现:

 1import os
 2import rospy
 3from turtlesim.msg import Pose
 4
 5
 6def callback(msg):
 7    print("\033[H")
 8    print(
 9        "Turtle Position Information:    \nx={:.3f}    \ny={:.3f}    \ntheta={:.3f}    \nlinear_velocity={:.3f}    \nangular_velocity={:.3f}    \n".format(
10            msg.x, msg.y, msg.theta, msg.linear_velocity, msg.angular_velocity
11        )
12    )
13
14
15def main():
16    rospy.init_node("topic_subscribe_py")
17    os.system("clear")
18    subscriber = rospy.Subscriber("/turtle1/pose", Pose, callback)
19    rospy.spin()
20
21
22if __name__ == "__main__":
23    main()

代码中的 ANSI 控制字符 \033[H 用于清屏,且避免使用 clear 命令时终端频闪。

用 launch 文件启动多个节点固然方便,但是 roslaunch 命令启动的节点全部输出都在当前终端里,而我们这里需要用到多个终端来观察效果,所以需要开多个终端分别运行节点:

1roscore &
2rosrun turtlesim turtlesim_node
3rosrun turtle topic_subscribe_cpp_node
4rosrun turtlesim turtle_teleop_key

turtle_teleop_key 节点用于控制小乌龟的运动,我们可以用方向键控制小乌龟的运动方向。

或者直接用 topic_publish_py.py 节点发布轨迹信息:

1roscore &
2rosrun turtlesim turtlesim_node
3rosrun turtle topic_subscribe_py.py
4rosrun turtle topic_publish_py.py

服务通信

查看 turtlesim_node 节点提供的服务:

1rosservice list

输出:

 1/clear
 2/kill
 3/reset
 4/rosout/get_loggers
 5/rosout/set_logger_level
 6/spawn
 7/turtle1/set_pen
 8/turtle1/teleport_absolute
 9/turtle1/teleport_relative
10/turtlesim/get_loggers
11/turtlesim/set_logger_level

其中 kill 和 spawn 分别用于移除小乌龟和生成小乌龟。查看这些服务的详细信息:

 1jackgdn@ubuntu:~$ rosservice type /kill
 2turtlesim/Kill
 3jackgdn@ubuntu:~$ rossrv show turtlesim/Kill
 4string name
 5---
 6
 7jackgdn@ubuntu:~$ rosservice type /spawn
 8turtlesim/Spawn
 9jackgdn@ubuntu:~$ rossrv show turtlesim/Spawn
10float32 x
11float32 y
12float32 theta
13string name
14---
15string name

根据这些信息编写脚本,实现自定义位置生成乌龟,以及移除乌龟:

C++ 实现:

 1#include "ros/ros.h"
 2#include "turtlesim/Spawn.h"
 3#include "turtlesim/Kill.h"
 4#include <iostream>
 5#include <vector>
 6
 7int main(int argc, char **argv)
 8{
 9    setlocale(LC_ALL, "");
10    ros::init(argc, argv, "service_cpp");
11    ros::NodeHandle n;
12    ros::ServiceClient client_spawn = n.serviceClient<turtlesim::Spawn>("/spawn");
13    ros::ServiceClient client_kill = n.serviceClient<turtlesim::Kill>("/kill");
14    client_spawn.waitForExistence();
15    client_kill.waitForExistence();
16
17    std::vector<std::string> turtle_names;
18    float x, y, theta;
19    std::string name;
20    while (std::cin >> x >> y >> theta >> name) {
21        turtlesim::Spawn spawn;
22        spawn.request.x = x;
23        spawn.request.y = y;
24        spawn.request.theta = theta;
25        spawn.request.name = name;
26
27        if (client_spawn.call(spawn)) {
28            ROS_INFO("Turtle Spawned");
29            turtle_names.push_back(name);
30        }
31        else
32        {
33            ROS_ERROR("Failed to Spawn Turtle");
34        }
35    }
36
37    for (auto name : turtle_names) {
38        turtlesim::Kill kill;
39        kill.request.name = name;
40        if (client_kill.call(kill)) {
41            ROS_INFO("Turtle Removed!");
42        }
43        else
44        {
45            ROS_ERROR("Failed to Remove Turtle");
46        }
47    }
48
49    return 0;
50}

Python 实现:

 1#!/usr/bin/python3
 2
 3import rospy
 4from turtlesim.srv import Spawn, SpawnRequest, SpawnResponse, Kill, KillRequest , KillResponse
 5
 6
 7def main():
 8    rospy.init_node("service_py")
 9    client_spawn = rospy.ServiceProxy("/spawn", Spawn)
10    client_kill = rospy.ServiceProxy("/kill", Kill)
11    client_spawn.wait_for_service()
12    client_kill.wait_for_service()
13
14    turtle_name = list()
15    while True:
16        try:
17            try:
18                x, y, theta, name = input().strip().split()
19                spawn_request = SpawnRequest()
20                spawn_request.x = float(x)
21                spawn_request.y = float(y)
22                spawn_request.theta = float(theta)
23                spawn_request.name = name
24            except ValueError:
25                rospy.logerr("Invalid Input")
26                continue
27
28            try:
29                client_spawn.call(spawn_request)
30                rospy.loginfo("Turtle Spawned")
31                turtle_name.append(name)
32            except rospy.ServiceException:
33                rospy.logerr("Failed to Spawn Turtle")
34
35        except EOFError:
36            break
37
38    for name in turtle_name:
39        kill_request = KillRequest()
40        kill_request.name = name
41        try:
42            client_kill.call(kill_request)
43            rospy.loginfo("Turtle Removed")
44        except rospy.ServiceException:
45            rospy.logerr("Failed to Remove Turtle")
46
47
48if __name__ == "__main__":
49    main()

配置文件的编写就不再赘述。

参数服务器通信

首先查看 turtlesim_node 节点提供的参数:

1rosparam list

输出:

1/rosdistro
2/roslaunch/uris/host_ubuntu__46433
3/rosversion
4/run_id
5/turtlesim/background_b
6/turtlesim/background_g
7/turtlesim/background_r

可以看到,/turtlesim/background_b, /turtlesim/background_g, /turtlesim/background_r 三个参数用于控制背景颜色,通过修改这些参数可以改变背景颜色。/turtlesim 表示这些参数的命名空间。

查看这些参数的类型:

1rosparam get /turtlesim

输出:

1background_b: 255
2background_g: 86
3background_r: 69

可以看到,这些参数的类型都是 int。编写脚本修改参数。

C++ 实现:

 1#include "ros/ros.h"
 2#include <iostream>
 3
 4int main(int argc, char **argv)
 5{
 6    setlocale(LC_ALL, "");
 7    ros::init(argc, argv, "param_cpp");
 8
 9    ros::NodeHandle n("turtlesim_node");  // 指定命名空间
10    n.setParam("background_r", 113);
11    n.setParam("background_g", 113);
12    n.setParam("background_b", 113);
13
14    return 0;
15}

Python 实现:

 1import rospy
 2
 3
 4def main():
 5    rospy.init_node("param_py")
 6    rospy.set_param("background_r", 113)
 7    rospy.set_param("background_g", 113)
 8    rospy.set_param("background_b", 113)
 9
10
11if __name__ == "__main__":
12    main()

用于 launch 文件无法决定节点的加载顺序,因此我们需要在 turtlesim_node 节点启动前修改参数。

1roscore &
2rosrun turtle param_cpp_node
3rosrun turtlesim turtlesim_node

随后可以看到背景颜色已经变成灰色。

由于 Python 脚本中无法指定命名空间,因此需要在 rospy.set_param 函数中指定参数的完整路径(我显然没有这样做),或者在启动节点时传递 __ns: 参数。

1roscore &
2rosrun turtle param_py.py __ns:="turtlesim"
3rosrun turtlesim turtlesim_node

这样可以起到同样的效果。

系列文章