文章目录
代码引用:https://github.com/DroidAITech/ROS-Academy-for-Beginners
工程架构
创建工作空间及编译
mkdir catkin_ws
创建一个catkin编译的工作空间cd catkin_ws
进入到catkin_wsmkdir src
再创建一个src的文件夹,这个是必须的cd ../..
回到最外层的目录sudo chmod -R 777 catkin_ws
对整个工作空间及子目录进行可读可执行可操作的赋权catkin_make
在工作空间内执行catkin_make生成所需要的基本框架cd src
进入srccatkin_create_pkg test1
创建一个test1的packagecatkin_create_pkg test2 roscpp rospy std_msgs nav_msgs
创建一个带有依赖的test2的package,roscpp用来cpp,rospy用于python脚本,std_msgs消息,以及导航专门用的nav_msgsgit clone git@github.com:DroidAITech/ROS-Academy-for-Beginners.git
克隆一个别人的packagessh-keygen -t rsa -C "974782852@qq.com"
生成git的公钥,否则git没有权限cd catkin_ws
回到catkin_ws目录rosinstall --from-paths src --ignore-src --rosdistro=melodic -y
安装所有package会用到的依赖,rosdistro需要和ubuntu版本对应,所有用到的依赖可以在每个package中的package.xml的build中找到source ~/catkin_ws/devel/setup.bash
加载当前的环境echo "source ~/catkin_ws/devel/setup.bash " >> ~/.bashrc
这样就不用每次再加载一次环境,打开一个bash自动加载好rospack find test2
rospack find命令可以直接找到相应的packagerosls topic_demon
rosls 可以直接列出package下的内容
通信架构
master和node
- master, 节点管理器
- 每个节点启动时要向它注册
- 管理node之间的通信
roscore
启动master,rosout日志输出(warn,error等), parameter server参数服务器
- node, ros的进程
- 一个package可以有多个可执行文件,运行后就加载到内存中,成了一个node
- 一般来说一个功能一个node
rosrun [pkg_name] [node_name]
启动一个节点rosnode list
列出当前运行的node的信息rosnode info [node_name]
显示某个node的详细信息rosnode kill [node_name]
结束某个node- 一次启动多个node,用
roslaunch [pkg_name] [file_name.launch]
, 可以在pkg中建立一个launch文件夹,xx.launch 专门用来启动多个节点,一般命名为xx_bringup, 不用死记硬背,找别人的模板看着改
打开robot sim仿真
roslaunch robot_sim_demo robot_spawn.launch
启动sim仿真,比较卡,应该看看网上有没有什么优化的策略rosnode list
列出当前有哪些noderosnode info xxx
列出xxx节点的具体信息rosrun robot_sim_demo robot_keyboard_teleop.py
按ijkl就可以控制仿真中机器人的前后左右运动rosrun image_view image_view image:=camera/rgb/image_raw
可以看到摄像头看到的画面。
topics和msgs
- 异步通信方式,node a只管publish, 不用管有没有人接收,node b有消息来了就去处理,没有就做别的;可以有多个node向同一个topic发布,也可以有多个node订阅同一个topic
- node之间通过publish-subscribe机制通信, 接收者收到数据会回调
- node a向node b发送一个信息,发送到topic频道,然后node b从频道中取出数据
- Message是topic的数据类型,定义在package里面的msg文件夹中的*.msg文件中
- bool, int8, int16, int32, int64, float32, float64, string, time, duration, header, 可变长数组[], 固定长度数组[C]
rostopic list
列出当前所有topicrostopic info /topic_mame
显示某个topic的属性信息rostopic echo /topic_name
显示某个topic的内容rostopic pub /topic_name ...
向某个topic发布内容rosmsg list
列出系统上所有的msgrosmsg show /msg_name
显示某msg的内容
service和srv
- 同步通信方式,node a调用node b的函数, 并且会阻塞等待返回一个结果,request-reply的方式,node b作为server, node a为client;远程过程调用RPC
- 偶尔调用的功能、具体任务
- srv定义在srv文件夹中*.srv文件
- 返回的数组嵌套一个msg文件
- 修改package.xml,添加依赖
- 修改CMakeList.txt,添加相应的srv和msg
rosservice list
列出当前所有活跃的servicerosservice info service_name
显示某个service的属性信息rosservice call service_name args
调用某个service,eg:rosservice call /go/delte_light 'light_name: 'sun''
,删除gazebo中的sun灯光rossrv list
列出系统上所有的srvrossrv show srv_name
显示某个srv内容
parameter server
roscore
时候已经启动parameter server- 存储各种参数的字典,可以用命令行,launch文件(两个标签和)和node api读写
- key和value的形式
rosparam list
列出当前所有的参数rosparam get param_key
显示某个参数的值rosparam set param_key param_value
设置某个参数的值rosparam dump file_name
保存参数到文件中rosparam load file_name
从文件中读取参数,yaml格式:key:valuerosparam delete param_key
删除参数
action
- 类似service, 带有状态反馈的通信方式
- 通常用在长时间、可抢占的任务中
- action定义在action文件夹中的*.action
- 发送一个goal,实时返回feedback,最终完成返回result
常用工具
gazebo
- 和ros是用一个团队开发,可以用来仿真导航、路径规划等,有很多现成的模型可以使用。如桌子,方块,圆球等动态的障碍物,测试避障等功能。
RViz
- The Robot Visualization tool可视化工具
- 方便调试和监控
rviz
可以启动- 添加一个robotmodel可以把xbot的模型加载到界面中,如果显示的是一片白色,把左侧选项栏中的framebase换成机器人即可。
- 添加一个camera,在其属性中可以添加imagetopic,可以实时的显示摄像头看到的画面
- 添加一个laser
- 添加一个pointclould2点云,由深度摄像头得到的,一个个小方格,不仅有颜色rgb,还有x,y,z信息
- 左边的每个插件都类似一个subscriber,订阅了这些传感器的topic,用来显示
Rqt
rqt_graph
显示通信架构,当前有哪些node,哪些topic,数据流rqt_plot
绘制曲线,比如odo的速度,imu的数据等。
rosbag
- 记录和回访数据
- *.bag
rosbag record <topic_names>
记录某些topic到bag中rosbag record -a
记录所有的topic到bag中rosbag play <bag_files>
回放bag
roscpp
- Client Library提供ROS编程的库,在API上面又封装了一层,如roscpp, rospy, roslisp
void ros::init();
解析ROS参数,为本node命名ros::NodeHandle
是一个类ros::Publisher advertise(const string &topic, uint32_t queue_size);
创建话题的publisherros::Subscriber subscribe(const string &topic, uint32_t queue_size, void(*)(M));
创建话题的subscriberros::ServiceServer advertiseService(const string &service, bool(*srv_func)(Mreq &, Mres &));
创建服务的serverrosServiceClient serviceClient(const string &service_name, bool persistent=false);
创建服务的clientbool getParam(const string &key, void &val);
查询某个参数的值bool setParam(const string &key, void val);
给某个参数赋值
ros::masters
是一个namespacebool check();
检查master是否启动const string& getHost();
返回master所处的Hostnamebool getNodes(V_string &nodes);
从master返回已知的node名称列表bool getTopics(V_Topicinfo &topics);
返回所有正在被发布的topic列表bool getURl();
返回到master的URl地址,如http://host:port/
uint32_t getPort();
返回master运行在的端口
ros::this_node
是一个namespacevoid getAdvertisedTopics(V_string &topics);
返回本node发布的topicconst string &getName();
返回当前node的名称const string &getNamespace();
返回当前node的命名空间void getSubscribedTopics(V_string &topics);
返回当前node订阅的topic
ros::service
是一个namespacebool call(const string &service, Service &service);
调用一个RPC服务ServiceClient createClient(const string& service_name, bool persistent=false, const M_string &header_value=M_string());
创建一个服务的clientbool exists(const string& service_name, bool print_failure_reason);
确认服务可调用bool waitForService(const string &service_name, int32_t timeout);
等待一个服务,直到它可以调用
topic_demo
- package
catkin_create_pkg topic_demo roscpp rospy std_msgs
- msg
mkdir msg
gedit gps.msg
- talker.cpp
- listener.cpp
- CMakeList.txt&Package.xml
- 添加依赖,msg等,具体看详细
talker
//ROS头文件
#include <ros/ros.h>
//自定义msg产生的头文件
#include <topic_demo/gps.h>
int main(int argc, char **argv)
{
//用于解析ROS参数,第三个参数为本节点名
ros::init(argc, argv, "talker");
//实例化句柄,初始化node
ros::NodeHandle nh;
//自定义gps msg
topic_demo::gps msg;
msg.x = 1.0;
msg.y = 1.0;
msg.state = "working";
//创建publisher
ros::Publisher pub = nh.advertise<topic_demo::gps>("gps_info", 1);
//定义发布的频率
ros::Rate loop_rate(1.0);
//循环发布msg
while (ros::ok())
{
//以指数增长,每隔1秒更新一次
msg.x = 1.03 * msg.x ;
msg.y = 1.01 * msg.y;
ROS_INFO("Talker: GPS: x = %f, y = %f ", msg.x ,msg.y);
//以1Hz的频率发布msg
pub.publish(msg);
//根据前面定义的频率, sleep 1s
loop_rate.sleep();//根据前面的定义的loop_rate,设置1s的暂停
}
return 0;
}
listener
//ROS头文件
#include <ros/ros.h>
//包含自定义msg产生的头文件
#include <topic_demo/gps.h>
//ROS标准msg头文件
#include <std_msgs/Float32.h>
void gpsCallback(const topic_demo::gps::ConstPtr &msg)
{
//计算离原点(0,0)的距离
std_msgs::Float32 distance;
distance.data = sqrt(pow(msg->x,2)+pow(msg->y,2));
//float distance = sqrt(pow(msg->x,2)+pow(msg->y,2));
ROS_INFO("Listener: Distance to origin = %f, state: %s",distance.data,msg->state.c_str());
}
int main(int argc, char **argv)
{
ros::init(argc, argv, "listener");
ros::NodeHandle n;
ros::Subscriber sub = n.subscribe("gps_info", 1, gpsCallback);
//ros::spin()用于调用所有可触发的回调函数。将进入循环,不会返回,类似于在循环里反复调用ros::spinOnce()。
ros::spin();
return 0;
}
service_demo
- package
catkin_create_pkg service_demo roscpp rospy std_msgs
- msg
mkdir srv
gedit Greeeting.srv
- talker.cpp
- listener.cpp
- CMakeList.txt&Package.xml
- 添加依赖,msg等,具体看详细
server
// This is the C++ version server file of the service demo
//
// 加载必要文件,注意Service_demo的加载方式
# include "ros/ros.h"
# include "service_demo/Greeting.h"
# include "string"
// 定义请求处理函数
bool handle_function(service_demo::Greeting::Request &req,
service_demo::Greeting::Response &res)
{
// 此处我们对请求直接输出
ROS_INFO("Request from %s with age %d ", req.name.c_str(), req.age);
// 返回一个反馈,将response设置为"..."
res.feedback = "Hi " + req.name + ". I'm server!";
return true;
}
int main(int argc, char **argv)
{
// 初始化节点,命名为"greetings_server"
ros::init(argc, argv, "greetings_server");
// 定义service的server端,service名称为“greetings”,收到request请求之后传递给handle_function进行处理
ros::NodeHandle nh;
ros::ServiceServer service = nh.advertiseService("greetings", handle_function);
// 调用可
ros::spin();
return 0;
}
client
// This is client of the service demo
// 包含必要文件,注意Service文件的包含方式,我们定义的srv文件为Greeting.srv,在包含时需要写成Greeting.h
# include "ros/ros.h"
# include "service_demo/Greeting.h"
int main(int argc, char **argv)
{
// 初始化,节点命名为"greetings_client"
ros::init(argc, argv, "greetings_client");
// 定义service客户端,service名字为“greetings”,service类型为Service_demo
ros::NodeHandle nh;
ros::ServiceClient client = nh.serviceClient<service_demo::Greeting>("greetings");
// 实例化srv,设置其request消息的内容,这里request包含两个变量,name和age,见Greeting.srv
service_demo::Greeting srv;
srv.request.name = "HAN";
srv.request.age = 20;
if (client.call(srv))
{
// 注意我们的response部分中的内容只包含一个变量response,另,注意将其转变成字符串
ROS_INFO("Response from server: %s", srv.response.feedback.c_str());
}
else
{
ROS_ERROR("Failed to call service Service_demo");
return 1;
}
return 0;
}
param_demo
- cpp写法
#include<ros/ros.h>
int main(int argc, char **argv){
ros::init(argc, argv, "param_demo");
ros::NodeHandle nh;
int parameter1, parameter2, parameter3, parameter4, parameter5;
//Get Param的三种方法
//① ros::param::get()获取参数“param1”的value,写入到parameter1上
bool ifget1 = ros::param::get("param1", parameter1);
//② ros::NodeHandle::getParam()获取参数,与①作用相同
bool ifget2 = nh.getParam("param2",parameter2);
//③ ros::NodeHandle::param()类似于①和②
//但如果get不到指定的param,它可以给param指定一个默认值(如33333)
nh.param("param3", parameter3, 33333);
if(ifget1)
ROS_INFO("Get param1 = %d", parameter1);
else
ROS_WARN("Didn't retrieve param1");
if(ifget2)
ROS_INFO("Get param2 = %d", parameter2);
else
ROS_WARN("Didn't retrieve param2");
if(nh.hasParam("param3"))
ROS_INFO("Get param3 = %d", parameter3);
else
ROS_WARN("Didn't retrieve param3");
//Set Param的两种方法
//① ros::param::set()设置参数
parameter4 = 4;
ros::param::set("param4", parameter4);
//② ros::NodeHandle::setParam()设置参数
parameter5 = 5;
nh.setParam("param5",parameter5);
ROS_INFO("Param4 is set to be %d", parameter4);
ROS_INFO("Param5 is set to be %d", parameter5);
//Check Param的两种方法
//① ros::NodeHandle::hasParam()
bool ifparam5 = nh.hasParam("param5");
//② ros::param::has()
bool ifparam6 = ros::param::has("param6");
if(ifparam5)
ROS_INFO("Param5 exists");
else
ROS_INFO("Param5 doesn't exist");
if(ifparam6)
ROS_INFO("Param6 exists");
else
ROS_INFO("Param6 doesn't exist");
//Delete Param的两种方法
//① ros::NodeHandle::deleteParam()
bool ifdeleted5 = nh.deleteParam("param5");
//② ros::param::del()
bool ifdeleted6 = ros::param::del("param6");
if(ifdeleted5)
ROS_INFO("Param5 deleted");
else
ROS_INFO("Param5 not deleted");
if(ifdeleted6)
ROS_INFO("Param6 deleted");
else
ROS_INFO("Param6 not deleted");
ros::Rate rate(0.3);
while(ros::ok()){
int parameter = 0;
ROS_INFO("=============Loop==============");
//roscpp中尚未有ros::param::getallparams()之类的方法
if(ros::param::get("param1", parameter))
ROS_INFO("parameter param1 = %d", parameter);
if(ros::param::get("param2", parameter))
ROS_INFO("parameter param2 = %d", parameter);
if(ros::param::get("param3", parameter))
ROS_INFO("parameter param3 = %d", parameter);
if(ros::param::get("param4", parameter))
ROS_INFO("parameter param4 = %d", parameter);
if(ros::param::get("param5", parameter))
ROS_INFO("parameter param5 = %d", parameter);
if(ros::param::get("param6", parameter))
ROS_INFO("parameter param6 = %d", parameter);
rate.sleep();
}
}
- launch写法
<launch>
<!--param参数配置-->
<param name="param1" value="1" />
<param name="param2" value="2" />
<!--param name="table_description" command="$(find xacro)/xacro.py $(find gazebo_worlds)/objects/table.urdf.xacro" /-->
<!--rosparam参数配置-->
<rosparam>
param3: 3
param4: 4
param5: 5
</rosparam>
<!--以上写法将参数转成YAML文件加载,注意param前面必须为空格,不能用Tab,否则YAML解析错误-->
<!--rosparam file="$(find robot_sim_demo)/config/xbot-u_control.yaml" command="load" /-->
<node pkg="param_demo" type="param_demo" name="param_demo" output="screen" />
</launch>
rospy
- script文件夹中放py
api
http://docs.ros.org/api/rospy/html/
wait_for_message(topic, topic_type, time_out=None)
等待指定topic的一个message,一般用于只执行一次,如按下摄像头的拍照,等待一次信息即可。
topic_demo
pytalker
#!/usr/bin/env python
#coding=utf-8
import rospy
#倒入自定义的数据类型
from topic_demo.msg import gps
def talker():
#Publisher 函数第一个参数是话题名称,第二个参数 数据类型,现在就是我们定义的msg 最后一个是缓冲区的大小
#queue_size: None(不建议) #这将设置为阻塞式同步收发模式!
#queue_size: 0(不建议)#这将设置为无限缓冲区模式,很危险!
#queue_size: 10 or more #一般情况下,设为10 。queue_size太大了会导致数据延迟不同步。
pub = rospy.Publisher('gps_info', gps , queue_size=10)
rospy.init_node('pytalker', anonymous=True)
#更新频率是1hz
rate = rospy.Rate(1)
x=1.0
y=2.0
state='working'
while not rospy.is_shutdown():
#计算距离
rospy.loginfo('Talker: GPS: x=%f ,y= %f',x,y)
pub.publish(gps(state,x,y))
x=1.03*x
y=1.01*y
rate.sleep()
if __name__ == '__main__':
talker()
pylistener
#!/usr/bin/env python
#coding=utf-8
import rospy
import math
#导入mgs到pkg中
from topic_demo.msg import gps
#回调函数输入的应该是msg
def callback(gps):
distance = math.sqrt(math.pow(gps.x, 2)+math.pow(gps.y, 2))
rospy.loginfo('Listener: GPS: distance=%f, state=%s', distance, gps.state)
def listener():
rospy.init_node('pylistener', anonymous=True)
#Subscriber函数第一个参数是topic的名称,第二个参数是接受的数据类型 第三个参数是回调函数的名称
rospy.Subscriber('gps_info', gps, callback)
rospy.spin()
if __name__ == '__main__':
listener()
service_demo
server
#!/usr/bin/env python
# coding:utf-8
# 上面指定编码utf-8,使python能够识别中文
# 加载必需模块,注意service模块的加载方式,from 包名.srv import *
# 其中srv指的是在包根目录下的srv文件夹,也即srv模块
import rospy
from service_demo.srv import *
def server_srv():
# 初始化节点,命名为 "greetings_server"
rospy.init_node("greetings_server")
# 定义service的server端,service名称为"greetings", service类型为Greeting
# 收到的request请求信息将作为参数传递给handle_function进行处理
s = rospy.Service("greetings", Greeting, handle_function)
rospy.loginfo("Ready to handle the request:")
# 阻塞程序结束
rospy.spin()
# Define the handle function to handle the request inputs
def handle_function(req):
# 注意我们是如何调用request请求内容的,与前面client端相似,都是将其认为是一个对象的属性,通过对象调用属性,在我们定义
# 的Service_demo类型的service中,request部分的内容包含两个变量,一个是字符串类型的name,另外一个是整数类型的age
rospy.loginfo( 'Request from %s with age %d', req.name, req.age)
# 返回一个Service_demoResponse实例化对象,其实就是返回一个response的对象,其包含的内容为我们再Service_demo.srv中定义的
# response部分的内容,我们定义了一个string类型的变量,因此,此处实例化时传入字符串即可
return GreetingResponse("Hi %s. I' server!"%req.name)
# 如果单独运行此文件,则将上面定义的server_srv作为主函数运行
if __name__=="__main__":
server_srv()
client
#!/usr/bin/env python
# coding:utf-8
# 上面的第二句指定编码类型为utf-8,是为了使python能够识别中文
# 加载所需模块
import rospy
from service_demo.srv import *
def client_srv():
rospy.init_node('greetings_client')
# 等待有可用的服务 "greetings"
rospy.wait_for_service("greetings")
try:
# 定义service客户端,service名称为“greetings”,service类型为Greeting
greetings_client = rospy.ServiceProxy("greetings",Greeting)
# 向server端发送请求,发送的request内容为name和age,其值分别为"HAN", 20
# 注意,此处发送的request内容与service文件中定义的request部分的属性是一致的
#resp = greetings_client("HAN",20)
resp = greetings_client.call("HAN",20)
# 打印处理结果,注意调用response的方法,类似于从resp对象中调取response属性
rospy.loginfo("Message From server:%s"%resp.feedback)
except rospy.ServiceException, e:
rospy.logwarn("Service call failed: %s"%e)
# 如果单独运行此文件,则将上面函数client_srv()作为主函数运行
if __name__=="__main__":
client_srv()
TF&URDF
TF
- TransForm 坐标系转换
- 机器人的部件叫做link,对应一个frame坐标系
- TF树
- 每两个link之间的联通,broadcaster,需要有一个node来发布给broadcaster,来联通他们之间的关系
rosrun tf_view_frames
根据当前的tf树创建一个pdf图,监听/tf这个topic 5srosrun rqt_tf_tree rqt_tf_tree
查看当前的tf树rosrun tf tf_echo [reference_frame] [target_frame]
查看两个frame之间的变换关系
urdf
- Unified Robot Description Format
- 每个link之间的链接叫做joint,
- 定义一个urdf文件就是描述了整个机器人的组成关系,遵循xml格式
SLAM
- 同步定位与建图Simutaneous Localization And Mapping
- Mapping算法:
- Gmapping
- Karto
- Hector
- Cartographer
- Localization算法:
- AMCL
- Path Planning算法:
- Navigation
- Global:
- Digkstra、A*
- Locaal:
- DWA
- Global:
- Navigation
- Map:
# This represents a 2-D grid map, in which each cell represents the probability of
# occupancy.
Header header
#MetaData for the map
MapMetaData info
# The map data, in row-major order, starting with (0,0). Occupancy
# probabilities are in the range [0,100]. Unknown is -1.
int8[] data
# This hold basic information about the characterists of the OccupancyGrid
# The time at which the map was loaded
time map_load_time
# The map resolution [m/cell]
float32 resolution
# Map width [cells]
uint32 width
# Map height [cells]
uint32 height
# The origin of the map [m, m, rad]. This is the real-world pose of the
# cell (0,0) in the map.
geometry_msgs/Pose origin
- 一个topic:/map,type:nav_msgs/OccupancyGrid
- resolution单位:m/pixed 米每像素,一个珊格就是一个像素,resolution代表一个像素是多少米, 一般为0.05、0.025
- width、height单位:pixed
- data数组的长度就是width*height
slam演示
roscore
roslaunch robot_sim_demo robot_spawn.launch
roslaunch slam_sim_demo gmapping_demo.launch
roslaunch slam_sim_demo view_slam.launch
Navigation
movebase
- 以下三个插件都是继承于nav_core
- 全局规划,只考虑静态障碍物, Base Local Planner
- base_local_planner
- dwa_local_planner
- 局部规划,动态障碍物, Base Global Planner
- carrot_planner
- navfn
- global_planner
- 处理异常行为, Recovery Behavior
- clear_costmap_recovery
- rotate_recovery
- move_slow_and_clear
costmap
- 代价地图,珊格地图
- 主要给导航使用,有两张(global_costmap, local_costmap), 多层
- static_layer(map),静态不变的
- obstacle layer, 动态的,由激光扫到的地图,加到static_layer,支持3D点云的投影
- inflation layer, 膨胀, 把障碍物进行膨胀一圈,防止机器人碰撞
- 以上3层可通过YAML配置
MapServer
- 提供地图,直接发布/map这个topic
- 地图格式:*.pgm
- 表现障碍物有无:occ = (255 - color_avg) / 255.0
- 255减去某一像素上的RGB平均值/255,比如一个地方是白色的[255,255,255],那么occ为0,说明该像素点为空,没有障碍
rosrun map_server map_server my_map.yaml
发布地图rosrun map_server map_saver [-f my_map]
保存地图