機器人編程與ROS
機器人是一個帶有傳感器,作動器(電動機)和一個計算單元的機器。它可以更具用戶的控制或者基於傳感器的輸入做出決定。
- 計算單元:其中計算單元可以是一塊單片機,也可以是PC,其中的程序用以控制機器人的決策和行動。機器人內部的編程的主要部分是PC和微控制器/PLC,其中PLC主要用於工業機器人。
- 作動器:作動器為移動機器人提供直線運動或者旋轉運動。常用的作動器如伺服電機,步進電機,和減速齒輪等。
- 傳感器:反饋機器人的狀態和進行環境感知。如輪速編碼器,超聲波傳感器和攝像機等。
機器人編程的一些特性
- 線程化:機器人有多個傳感器和作動器,需要兼容多線程的編程語言來實現在不同的線程中使用不同的傳感器和作動器,也即多任務。而各個線程之間需要相互通信,以便交換數據。
- 面向對象編程:面向對象編程語言可以使程序更加的模塊化,代碼也可以很容易的被重復使用。
- 底層設備控制:高層編程語言可以訪問底層設備,如GPIO(通用輸入輸出)引腳,串口,並口,USB,SPI和I2C。C/C++和python這樣的編程語言可以使用底層設備,所以這些語言在樹莓派等嵌入式計算機中也很受歡迎。
- 編程實現的簡便性:python是快速實現機器人算法的一個很好的選擇。
- 進程間通信:機器人的多個傳感器和作動器之間為了實現相互通信,可以使用多線程架構模型(如ROS)。
- .....
ROS框架的功能
-
進程間的消息傳遞接口(進程間的通信):ROS提供的消息傳遞接口可以用於兩個程序或進程之間的通信。如相機通過圖像處理檢測到人臉,並將坐標發送到跟蹤器進程,而跟蹤器進程就可以使用電動機來跟蹤人臉。
-
與操作系統類似的特性:ROS不是真正的操作系統,但是可以提供一些操作系統的功能。如多線程,底層設備控制,包管理和硬件抽象。硬件抽象可以使程序員在不知道設備的完整細節的情況下編寫程序,以適應來自不同供應商的產品。
-
支持高級編程語言和工具鏈:支持C++,Python,Lisp等語言,此外還可以對Java,C#等語言有實驗性的支持。ROS為這些語言提供客戶端庫,所以可以通過這些語言使用ROS中的功能函數。
-
第三方庫的可用性:如OpenCV,PCL等被集成在ROS的框架內
-
通用算法:ROS已經實現了一些通用的機器人算法,如PID,SLAM(即使定位與地圖構建),路徑規划算法,自適應蒙特卡洛定位算法等(AMCL)。
-
編程實現的簡便性:除了通用算法之外,ROS包可以很容易的被其他機器人重用。
-
.....
ROS編程開發
- ROS開發的第一步就是創建工作空間,即ROS程序包存儲的地方;
- 先用mkdir創建工作的文件夾;
- 然后在此文件夾下創建【src】文件夾,並通過初始化命令【catkin_init_workspace】將該文件初始化為一個ROS的工作空間,否則是不能正確的創建、編譯和鏈接程序包的;
- 編譯工作空間:經過上述步驟就創建了工作空間了,雖然此時還沒有程序包,但仍然可以編譯該工作空間。
- 編譯工作空間要在工作空間的根目錄下進行,通過【catkin_make】命令進行編譯;
- 經過編譯之后再工作空間中就會產生build和devel兩個文件夾
- 設置環境變量:創建工作空間之后,還要設置系統路徑,這樣工作空間中的程序包才可以被訪問。
- 【source~/catkin_ws/devel/setup.bash】
ROS工作空間
由上述的命令常見了catkin工作空間,該控件是存放工程相關的文件的文件夾,並且可以主要分為下面四個文件夾
-
src:(代碼空間,source space) 用來存放所有的功能包,所有功能包的代碼,配置文件等都是放在這個里面;ROS的程序包只有在src文件夾下是才可以被編譯執行。當執行編譯命令【catkin_make】時,該命令就會檢查src文件夾中的每個程序包並且一一編譯它們。
-
bulid:(編譯空間,build space) 存放編譯過程中產生的文件;編譯過程中會在該文件夾中創建一些中間編譯文件和中間緩存文件,這些中間緩存文件可以避免在執行catkin_make命令時重新編譯所有的文件。所以如果刪了這個文件夾,那么所有的程序包都會被重新編譯。比如已經編譯了5個程序包了,現在又往src文件夾中添加了一個程序包,那么在執行下一次的catkin_make命令的時候就只有該新的程序包會被編譯。
-
devel:(開發空間,development space) 存放編譯生成的可執行文件,還有一些腳本文件,這些腳本文件可以將當前的工作空間添加到ROS的系統工作空間目錄中。在.bashrc文件中添加以下命令,就可以在所有的終端會話總訪問工作空間的程序包了。
source home/lim/<workspace_name>/devel/setup.bash
-
install:(安裝空間,install space) ,該文件夾可以通過以下命令創建
catkin_make install
在上述步驟中生成了可執行文件之后,該文件就是存放用install命令安裝的結果文件,當運行程序時,install文件夾中的相應的可執行文件就會被執行。
ROS程序包創建
通過以上有了工作空間之后,就要創建程序包了。程序包通過以下命令實現:
catkin_create_pkg ros_package_name package_dependencies
上述命令中的【catkin_create_pkg】是創建程序包的命令,【ros_package_name】是要創建的程序包的名字,【package_dependencies】是程序包的依賴包。
在每個程序包下會看到如圖4個文件:

-
CMakeLists.txt:包含了要編譯程序包中的ROS源碼和要創建可執行文件的所有的命令。
-
package.xml:該XML文件記錄了程序包的依賴包和一些與自身相關信息等。
-
src文件夾:ROS程序包的源碼保存在該文件夾下,通常,C++ 文件保存在該文件夾中,python腳本另創建文件夾”scripts"保存。
-
include:包含了源程序的頭文件,該文件夾可以自動生成, 第三方的函數庫文件也可以放在該文件夾中;
ROS客戶端庫
原則上可以用任意的編程語言編寫ROS節點,但是ROS提供了幾種編程語言的庫,所以此時用這些庫就會簡單許多。若沒有相應的庫,那么就需要自己實現ROS庫了。主要的客戶端庫有:
- roscpp:C++的ROS客戶端庫。具有高性能,廣泛使用;
- rospy:python的ROS客戶端庫。性能相較於roscpp較差,但優勢在於節省開發時間,如常用的大多都ROS命令行,roslaunch,roscore等均是用rospy客戶端編寫的;
- roslisp:Lisp語言的客戶端庫。主要用於ROS中的運動規划庫。
此外還有一些實驗性的客戶端庫如rosjava,roslua等。
但在上面中常用的還是roscpp和rospy
roscpp和rospy
-
頭文件和模塊
編寫程序時,在C++中第一部分要包含頭文件,在python中要導入python模塊
- C++中:創建C++節點時必須要包含頭文件【#include “ros/ros.h"】ros.h中包含了實現ROS功能的所必須的【所有】頭文件,不包含這個頭文件就不能創建節點。此外,C++中還有第二種頭文件,就是ROS消息頭文件,如果要在ROS中使用某種特定的消息類型,則需要包含這種【消息頭文件】。如ROS中有一些內置的消息類型,內置消息程序包【std_msgs】內就包含有整型,浮點數,字符串等標准數據類型的消息定義,所以如果要使用字符串消息,則要包含頭文件【#include "std_msgs/String.h"】,此外還有如【#include "std_msgs/Int32.h"】【#include "std_msgs/Int64.h"】等消息類型,其中前一部分是程序包名,后部分是消息類型名。
- Python中:Python中要導入模塊來創建節點,對應的這里是【import rospy】.同樣,要是使用消息類型,也需要導入【from std_msgs.msg import String】
-
初始化ROS節點
【在開始編寫任何節點之前,都要初始化ROS節點】。
-
C++中:用以下幾行代碼初始化節點
int main(int argc,char *argv) { ros::init(argc,argv,"name_of_node") ... }
在main函數中必須要包含ros::init()函數,該函數用來初始化節點,另外還可以通過(argc,argv等)向初始化函數傳遞命令行參數和節點名稱。
-
Python中:第一個參數是節點名,第二個參數=True意味着可以在系統中同時運行該節點的多個實例。
rospy.init_node('name_of_node',anonymous=True);
-
-
在ROS節點中打印日志信息
ROS提供了記錄日志信息的應用程序接口,這些信息是可讀字符串,【表示節點的運行狀態】
-
C++中:ROS_INFO(string_msg,args):記錄節點輸出的基本信息。
ROS_WARN(string_msg,args):記錄節點輸出的警告信息。
ROS_DEBUG(string_msg,args):記錄節點輸出的調試信息。等等
-
Python中:rospy.logingo(msg,*args)
rospy.logwarn(msg,*args)
rospy.logdebug(msg,*args)
-
-
創建節點句柄
初始化節點之后需要創建一個節點句柄實例,用於啟動ROS節點或發布,訂閱等操作。
-
C++中:如下,節點中的其他操作可以通過使用nh實例來訪問。
ros::NOdeHandle nh;
-
python中:不需要,rospy模塊內部自動解決這個問題
-
-
創建ROS消息定義
發布話題之前,要先創建一個ROS消息定義
-
C++中:創建std_msgs/String實例方式如下
std_msgs::String msg; //創建之后添加數據 msg.data="Hello"
-
python中:添加相同的字符串數據如下
msg=String() msg.data="Hello"
-
-
在ROS節點中發布話題
-
C++:發布話題語法如下,chatter_pub是發布者實例,chatter_pub會發布消息類型為std_msgs/String的話題,話題的名稱為chatter,隊列大小為1000
ros::Publisher publisher_object=node_handle.advertise<ROS message type>("topic_name",1000) //創建了待發布的話題之后,通過publish發出 publisher_object.publish(message) 例如 ros::Publisher chatter_pub=nh.advertise<std_msgs::String>("Chatter",1000) chatter_pub.publish(msg)
-
python:語法如下
publisher_instance=rospy.Publisher('topic_name',message_type,queue_size) 例如: pub=rospy.Publisher('chatter',String,queue_size=1000) pub.publish(hello_str)
-
-
在ROS節點中訂閱話題
-
C++:訂閱話題時不需要話題的類型,但是要給定【話題名和對應的回調函數】。回調函數是用戶自定義的函數,一旦從話題中接收到ROS消息,那么回調函數就會被執行。在回調函數中,我們可以自定義要操作的內容,如可以打印消息或者根據消息做出決定。回調函數的編寫看下一小節。
ros::Subscriber subscriber_obj=nodehandle.subscribe("topic_name",1000,callback function)
-
Python:在python中要指定消息的類型,同樣也可以設置回調函數
-
rospy.Subscriber("topic_name",message_type,callback funtion name)
-
-
在ROS節點中編寫回調函數
當訂閱的話題有消息到達時,回調函數就會被觸發。
-
C++:例子中是在回調函數中處理ROS字符串消息並顯示數據。
語法: void callback_name(const ros_message_const_pointer&pointer) { pointer->data//獲取數據 } 實例: void chatterCallback(const std_msgs::String::ConstPtr&msg) { ROS_INFO("I heard:[%s]",msg->data.c_str()); }
-
python:
def callback(data): rospy.loginfo(rospy.get_caller_id()+"I heard %s",data.data)
-
-
ROS節點中的ROS spin()函數
在訂閱或者發布函數之后,還需要調用一個函數來處理來自其他節點的訂閱請求或發布請求。
- C++:若只訂閱話題,那么就調用ros::spinOnce()函數。若既要訂閱又要發布,那么就調用ros::spin()函數
- python:python中沒有spin()函數。可以使用rospy.sleep()
-
ROS節點中的循環設置和ROS sleep()函數
為節點的循環設置設定一個恆定的速率,那么就可以用ros::Rate創建實例,並調用sleep()函數使之生效。
-
C++:
ros::Rate r(10); //10Hz的速率 r.sleep();
-
python:
rate=rospy.Rate(10) rate.sleep()
-
-
設置和獲取ROS參數
-
C++:獲取參數如下。之前先要聲明一個變量,然后用節點句柄node_handle中的【getParam()函數】獲取所需要的參數
std::string global_name; if(nh.getParam("/global_name",global_name)) { .... }
設置參數如下:
nh.setParam("/global_param",5)
-
python:
global_name=rospy.get_param("/global_name")rospy.set_param('~private_int','2')
-
ROS編程程序包實例
在該實例中創建名為hello_world的程序包,和一個發送節點和一個接受節點,用於傳遞字符串“Hello World"消息。
1.創建hello_world程序包
要在工作空間的src文件夾中創建程序包,創建之后產生的文件如下圖所示。
catkin_create_pkg hello_world roscpp rospy std_msgs

其中的package.xml文件中包含了程序包及其依賴包的相關信息。
CMakeLists.txt是cmake文件,工程的名字在該文件的頂部,其內find_package命令用於尋找這個程序包所必須的系統依賴包。其內catkin_package命令是catkin提供的一個CMake宏,用於向編譯系統指定工程所需要依賴的其他ROS程序包。
2.創建ROS C++ 節點
創建完程序包之后就可以創建節點,C++代碼放在src文件夾中。如下的talker.cpp中,
talker.cpp
#include "ros/ros.h"
#include "std_msgs/String.h"
#include <stream>
int main()
{
ros::init(argc,argv,"talker");
ros::NodeHandle n;
msgs::String>("chatter",1000);
ros::Rate loop_rate(10);
int count=0;
while(ros::ok())
{
std_msgs::String msg;
std::stringstream ss;
ss<<"hello world"<< count;
msg.data=ss.str();
ROS_INFO("%s",msg.data.c_str());
chatter_pub.publish(msg);
ros::spinOnce();
loop_rate.sleep();
++count;
}
return 0;
}
listener.cpp
#include "ros/ros.h"
#include "std_msgs/String.h"
void chatterCallback(const std_msgs::String::ConstPtr&msg)
{
ROS_INFO("I heard:[%s]",msg->data.c_str());
}
int main(int argc,char **argc)
{
ros::init(argc,argv,"listener");
ros::NodeHandle n;
ros::Subscriber sub=n.subscribe("chatter",1000,chatterCallback);
ros::spin();
return 0;
}
3.編輯CMakeLists.txt文件
創建節點之后,需要編輯CMakeLists.txt文件,然后通過編譯節點來創建可執行文件。如下所示,其中的add_executable表示從源代碼中創建可執行文件,然后該文件通過target_link_libraries命令和指定的庫函數相鏈接。
add_executable(talker src/talker.cpp)
add_executable(listener src/listener.cpp)
target_link_libraries(talker ${catkin_LIBRARIES})
target_link_libraries(listener ${catkin_LIBRARIES})
4.編譯C++節點
編輯完CMakeLists文件后就可以編譯了,切換到工作空間的文件夾下,然后用編譯命令catkin_make。
5.執行C++節點
編譯完成之后的可執行文件存放在catkin_ws/devel/lib/{程序包名}/文件夾下,則執行該節點可通過如下的命令:
roscore
rosrun 程序包名 節點名
在執行rosrun命令之前先要設置一下環境變量 ,要在工作空間下進行,即source devel/setup.bash
或者在.bashrc中加入如下的命令
source /home/用戶名/工作空間名/devel/setup.bash
6.創建ROS python節點
首先在程序包中創建名為【scripts】文件夾,然后python的腳本文件都保存在這個文件夾中。
talker.py
#!/usr/bin/env python
# license removed for brevity
import rospy
from std_msgs.msg import String
def talker():
pub = rospy.Publisher('chatter', String, queue_size=10)
rospy.init_node('talker', anonymous=True)
rate = rospy.Rate(10) # 10hz
while not rospy.is_shutdown():
hello_str = "hello world %s" % rospy.get_time()
rospy.loginfo(hello_str)
pub.publish(hello_str)
rate.sleep()
if __name__ == '__main__':
try:
talker()
except rospy.ROSInterruptException:
pass
listener.py
#!/usr/bin/env python
import rospy
from std_msgs.msg import String
def callback(data):
rospy.loginfo(rospy.get_caller_id() + "I heard %s", data.data)
def listener():
# In ROS, nodes are uniquely named. If two nodes with the same
# name are launched, the previous one is kicked off. The
# anonymous=True flag means that rospy will choose a unique
# name for our 'listener' node so that multiple listeners can
# run simultaneously.
rospy.init_node('listener', anonymous=True)
rospy.Subscriber("chatter", String, callback)
# spin() simply keeps python from exiting until this node is stopped
rospy.spin()
if __name__ == '__main__':
listener()
7.對python腳本編輯CMakeLists.txt文件
創建好腳本文件后,還要在CMakeLists.txt文件中編輯。
catkin_install_python(PROGRAMS scripts/talker.py
DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)
catkin_install_python(PROGRAMS scripts/talker.py scripts/listener.py
DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)
8.弄完之后最好也編譯以下
用catkin_make命令在工作空間中編譯一下。
9.運行python節點
rosrun 程序包名 腳本名
10.創建launch啟動文件
ROS啟動文件可以在一條指令中運行任意數量的節點。
-
C++: 該啟動文件可以以此運行talker和listener兩個節點,程序包在"pkg="域中,可執行文件在"type="域中,【其中”name=''為節點的名字,可以任意命名】,此時若執行rosnode list命令,則節點顯示的名字就是這里面的名字。注意,這里面的pkg,name,type 的順序可以任意,另外, 組后的output是將結果顯示在屏幕上,若沒有這個,則節點執行了,一些結果就不顯示了。將這個文件保存為【xxxx.launch】,則運行時就要執行【roslaunch 程序包名 xxxx.launch】
<launch> <node pkg="hello_world" name="listener_node" type="listener" output="screen"/> <node pkg="hello_world" name="talker_node" type="talker" output="screen"/> </launch>
-
python:python中除了將type中的可執行文件的名字改一改,其他不變,運行時也同上。
<launch> <node pkg="hello_world" name="listener_node" type="listener.py" output="screen"/> <node pkg="hello_world" name="talker_node" type="talker.py" output="screen"/> </launch>
ROS其他相關
ROS話題的類型
如小烏龜移動的節點,要向turtle1/cmd_vel話題發送指令,則要查看該話題的發送的數據類型:
rostopic type /turtle1/cmd_vel
geometry_msgs/Twist
則得到話題的消息類型是geometry_msgs/Twist,則發送命令時要按照該類型發送,現在查看該類型的定義:
rosmsg show geometry_msgs/Twist
geometry_msgs/Vector3 linear
float64 x
float64 y
float64 z
geometry_msgs/Vector3 angular
float64 x
float64 y
float64 z
結果如上所示,該消息的定義有如上兩部分。
ROS服務和參數
ROS中的服務是另外的一種通訊形式,采用一問一答的方式進行節點之間的通信。
rosservice list
調用:rosservice call 參數
查看服務參數:rosservice type /服務名
接上一行,具體參數:rossrv show

ROS參數用法:
rosparam set set parameter
rosparam get get parameter
rosparam load load parameters from file
rosparam dump dump parameters to file
rosparam delete delete parameter
rosparam list list parameter names