1. 程式人生 > >我的ROS入門(五):總算搞通ROS的服務節點訂閱釋出訊息話題了

我的ROS入門(五):總算搞通ROS的服務節點訂閱釋出訊息話題了

總算搞通ROS的服務節點訂閱釋出訊息主題了。可以實現那幾個東西。記錄一下吧。

首先要一個工作空間。

在當前系統使用者的home目錄下的.bashrc檔案中新增source /opt/ros/jade/setup.bash,才能執行ros相關的命令。

開始建立一個catkin工作空間

$ mkdir -p ~/catkin_lljws/src
$ cd ~/catkin_lljws/src
p引數可以建立多級父級目錄,這裡同時建立了catkin_ws資料夾和src資料夾。裡面什麼都沒有,但是也可以build它
$ cd ~/catkin_ws/
$ catkin_make
這個時候記得要source一下devel中的setup.bash,否則不會將當前工作空間設定在ROS工作環境的最頂層。
$ source devel/setup.bash
或者直接將這個放到.bashrc檔案中去。

下一步建立一個軟體包

首先切換到catkin工作空間中的src目錄下:

$ cd ~/catkin_lljws/src

使用catkin_create_pkg命令,這個程式包依賴於std_msgs、roscpp和rospy:

$ catkin_create_pkg llj_package std_msgs rospy roscpp
這些依賴包隨後儲存在package.xml檔案中。自定義 package.xml:

描述標籤<description>,維護者標籤maintainer,許可標籤license,依賴項標籤depend,在本例中,因為在編譯和執行時我們需要用到所有指定的依賴包,因此還需要將每一個依賴包分別新增到run_depend
標籤中。

編譯軟體包,直接和之前一樣build即可。 node節點,一個節點即為一個可執行檔案,它可以通過ROS與其它節點進行通訊。
message訊息,訊息是一種ROS資料型別,用於訂閱或釋出到一個話題。
topic話題,節點可以釋出訊息到話題,也可以訂閱話題以接收訊息。
master節點管理器,ROS名稱服務 (比如幫助節點找到彼此)。

rosout節點用於收集和記錄節點除錯輸出資訊,所以它總是在執行的。

節點與節點之間通過話題相互通訊。

話題之間的通訊是通過在節點之間傳送ROS訊息實現的。

釋出器和訂閱器之間必須傳送和接收相同型別的訊息。

話題的型別是由釋出在它上面的訊息型別決定的。

服務(services)是節點之間通訊的另一種方式。服務允許節點發送請求(request)

 並獲得一個響應(response)

rosparam使得我們能夠儲存並操作ROS引數伺服器搞(Parameter Server )上的資料。引數伺服器能夠儲存整型、浮點、布林、字串、字典和列表等資料型別。

rqt_console屬於ROS日誌框架(logging framework)的一部分,用來顯示節點的輸出資訊。

roslaunch可以用來啟動定義在launch檔案中的多個節點。

訊息和服務

msg檔案就是一個描述ROS中所使用訊息型別的簡單文字。它們會被用來生成不同語言的原始碼。
注意,在構建的時候,我們只需要"message_generation"。然而,在執行的時候,我們只需要"message_runtime"。

檢視package.xml, 確保它包含以下兩條語句: <build_depend>message_generation</build_depend> <run_depend>message_runtime</run_depend>

在 CMakeLists.txt檔案中,利用find_packag函式,增加對message_generation的依賴,這樣就可以生成訊息了。

同樣,你需要確保你設定了執行依賴:catkin_package( ... CATKIN_DEPENDS message_runtime ... ...)

add_message_files( FILES Num.msg)

手動新增.msg檔案後,我們要確保CMake知道在什麼時候重新配置我們的project。 確保添加了如下程式碼:generate_messages()

以上就是建立訊息的所有步驟。

一個srv檔案描述一項服務。它包含兩個部分:請求和響應。

編寫簡單的訊息釋出器和訂閱器 (C++)

在package裡建立src/talker.cpp檔案,建立src/listener.cpp檔案,修改CMakeLists.txt檔案,build即可。
#include "ros/ros.h"//ros/ros.h是一個實用的標頭檔案,它引用了ROS系統中大部分常用的標頭檔案,使用它會使得程式設計很簡便。
#include "std_msgs/String.h"//這引用了std_msgs/String 訊息, 它存放在std_msgs package裡,是由String.msg檔案自動生成的標頭檔案。
#include <sstream>
/*This tutorial demonstrates simple sending of messages over the ROS system.*/
int main(int argc, char **argv)
{
  /* The ros::init() function needs to see argc and argv so that it can perform any ROS arguments and name remapping that were provided at the command line. For programmatic remappings you can use a different version of init() which takes remappings directly, but for most command-line programs, passing argc and argv is the easiest way to do it.  The third argument to init() is the name of the node.You must call one of the versions of ros::init() before using any other part of the ROS system. */
  ros::init(argc, argv, "talker");//初始化ROS。它允許ROS通過命令列進行名稱重對映——目前,這不是重點。同樣,我們也在這裡指定我們節點的名稱——必須唯一。這裡的名稱必須是一個base name,不能包含/。
  /*NodeHandle is the main access point to communications with the ROS system.The first NodeHandle constructed will fully initialize this node, and the last NodeHandle destructed will close down the node.*/
  ros::NodeHandle n;//為這個程序的節點建立一個控制代碼。第一個建立的NodeHandle會為節點進行初始化,最後一個銷燬的NodeHandle會清理節點使用的所有資源。
  /* The advertise() function is how you tell ROS that you want to publish on a given topic name. This invokes a call to the ROS master node, which keeps a registry of who is publishing and who is subscribing. After this advertise() call is made, the master node will notify anyone who is trying to subscribe to this topic name, and they will in turn negotiate a peer-to-peer connection with this node.  advertise() returns a Publisher object which allows you to publish messages on that topic through a call to publish().  Once all copies of the returned Publisher object are destroyed, the topic will be automatically unadvertised.The second parameter to advertise() is the size of the message queue used for publishing messages.  If messages are published more quickly than we can send them, the number here specifies how many messages to buffer up before throwing some away.*/
  ros::Publisher chatter_pub = n.advertise<std_msgs::String>("chatter", 1000);//告訴節點管理器master我們將要在chatter topic上釋出一個std_msgs/String的訊息。這樣master就會告訴所有訂閱了chatter topic的節點,將要有資料釋出。第二個引數是釋出序列的大小。在這樣的情況下,如果我們釋出的訊息太快,緩衝區中的訊息在大於1000個的時候就會開始丟棄先前釋出的訊息。NodeHandle::advertise() 返回一個 ros::Publisher物件,它有兩個作用: 1) 它有一個publish()成員函式可以讓你在topic上釋出訊息; 2) 如果訊息型別不對,它會拒絕釋出。
  ros::Rate loop_rate(10);//ros::Rate物件可以允許你指定自迴圈的頻率。它會追蹤記錄自上一次呼叫Rate::sleep()後時間的流逝,並休眠直到一個頻率週期的時間。在這個例子中,我們讓它以10hz的頻率執行。
  /*A count of how many messages we have sent. This is used to create a unique string for each message.*/
  int count = 0;
  while (ros::ok())
  {//roscpp會預設安裝一個SIGINT控制代碼,它負責處理Ctrl-C鍵盤操作——使得ros::ok()返回FALSE。ros::ok()返回false,如果下列條件之一發生:SIGINT接收到(Ctrl-C);被另一同名節點踢出ROS網路;ros::shutdown()被程式的另一部分呼叫;所有的ros::NodeHandles都已經被銷燬.一旦ros::ok()返回false, 所有的ROS呼叫都會失效。
    /*This is a message object. You stuff it with data, and then publish it.*/
    std_msgs::String msg;
    std::stringstream ss;
    ss << "hello world " << count;
    msg.data = ss.str();
//我們使用一個由msg file檔案產生的‘訊息自適應’類在ROS網路中廣播訊息。現在我們使用標準的String訊息,它只有一個數據成員"data"。當然你也可以釋出更復雜的訊息型別。
    ROS_INFO("%s", msg.data.c_str());//ROS_INFO和類似的函式用來替代printf/cout. 
    /*The publish() function is how you send messages. The parameter
is the message object. The type of this object must agree with the type
given as a template parameter to the advertise<>() call, as was done
in the constructor above.*/
    chatter_pub.publish(msg);//現在我們已經向所有連線到chatter topic的節點發送了訊息。
    ros::spinOnce();//在這個例子中並不是一定要呼叫ros::spinOnce(),因為我們不接受回撥。然而,如果你想拓展這個程式,卻又沒有在這呼叫ros::spinOnce(),你的回撥函式就永遠也不會被呼叫。所以,在這裡最好還是加上這一語句。
    loop_rate.sleep();//這條語句是呼叫ros::Rate物件來休眠一段時間以使得釋出頻率為10hz。
    ++count;
  }
  return 0;
}
#include "ros/ros.h"
#include "std_msgs/String.h"
/*This tutorial demonstrates simple receipt of messages over the ROS system.*/
void chatterCallback(const std_msgs::String::ConstPtr& msg)
{
  ROS_INFO("I heard: [%s]", msg->data.c_str());
}//這是一個回撥函式,當訊息到達chatter topic的時候就會被呼叫。訊息是以 boost shared_ptr指標的形式傳輸,這就意味著你可以儲存它而又不需要複製資料
int main(int argc, char **argv)
{
  ros::init(argc, argv, "listener");
  ros::NodeHandle n;
  /*The subscribe() call is how you tell ROS that you want to receive messages on a given topic.  This invokes a call to the ROS master node, which keeps a registry of who is publishing and who is subscribing.  Messages are passed to a callback function, here called chatterCallback.  subscribe() returns a Subscriber object that you must hold on to until you want to unsubscribe.  When all copies of the Subscriber object go out of scope, this callback will automatically be unsubscribed from this topic.The second parameter to the subscribe() function is the size of the message queue.  If messages are arriving faster than they are being processed, this is the number of messages that will be buffered up before beginning to throw away the oldest ones.*/
  ros::Subscriber sub = n.subscribe("chatter", 1000, chatterCallback);
//告訴master我們要訂閱chatter topic上的訊息。當有訊息到達topic時,ROS就會呼叫chatterCallback()函式。第二個引數是佇列大小,以防我們處理訊息的速度不夠快,在快取了1000個訊息後,再有新的訊息到來就將開始丟棄先前接收的訊息。NodeHandle::subscribe()返回ros::Subscriber物件,你必須讓它處於活動狀態直到你不再想訂閱該訊息。當這個物件銷燬時,它將自動退訂上的訊息。有各種不同的NodeHandle::subscribe()函式,允許你指定類的成員函式,甚至是Boost.Function物件可以呼叫的任何資料型別。
  /*ros::spin() will enter a loop, pumping callbacks.  With this version, all callbacks will be called from within this thread (the main one).  ros::spin() will exit when Ctrl-C is pressed, or the node is shutdown by the master.*/
  ros::spin();//ros::spin()進入自迴圈,可以儘可能快的呼叫訊息回撥函式。如果沒有訊息到達,它不會佔用很多CPU,所以不用擔心。一旦ros::ok()返回FALSE,ros::spin()就會立刻跳出自迴圈。這有可能是ros::shutdown()被呼叫,或者是使用者按下了Ctrl-C,使得master告訴節點要shutdown。也有可能是節點被人為的關閉。
  return 0;
}

編寫簡單的Service和Client (C++)

#include "ros/ros.h"
#include "beginner_tutorials/AddTwoInts.h"//beginner_tutorials/AddTwoInts.h是由編譯系統自動根據我們先前建立的srv檔案生成的對應該srv檔案的標頭檔案。
//這個add函式提供兩個int值求和的服務,int值從請求request裡面獲取,而返回資料裝入響應response內,這些資料型別都定義在srv檔案內部,函式返回一個boolean值。
bool add(beginner_tutorials::AddTwoInts::Request  &req,
         beginner_tutorials::AddTwoInts::Response &res)
{
  res.sum = req.a + req.b;
  ROS_INFO("request: x=%ld, y=%ld", (long int)req.a, (long int)req.b);
  ROS_INFO("sending back response: [%ld]", (long int)res.sum);
  return true;
}//現在,兩個int值已經相加,並存入了response。然後一些關於request和response的資訊被記錄下來。最後,service完成計算後返回true值。

int main(int argc, char **argv)
{
  ros::init(argc, argv, "add_two_ints_server");
  ros::NodeHandle n;

  ros::ServiceServer service = n.advertiseService("add_two_ints", add);
//這裡,service已經建立起來,並在ROS內釋出出來。
  ROS_INFO("Ready to add two ints.");
  ros::spin();

  return 0;
}

#include "ros/ros.h"
#include "beginner_tutorials/AddTwoInts.h"
#include <cstdlib>

int main(int argc, char **argv)
{
  ros::init(argc, argv, "add_two_ints_client");
  if (argc != 3)
  {
    ROS_INFO("usage: add_two_ints_client X Y");
    return 1;
  }

  ros::NodeHandle n;
  ros::ServiceClient client = n.serviceClient<beginner_tutorials::AddTwoInts>("add_two_ints");
//這段程式碼為add_two_ints service建立一個client。ros::ServiceClient 物件待會用來呼叫service。
  beginner_tutorials::AddTwoInts srv;
  srv.request.a = atoll(argv[1]);
  srv.request.b = atoll(argv[2]);
//這裡,我們例項化一個由ROS編譯系統自動生成的service類,並給其request成員賦值。一個service類包含兩個成員request和response。同時也包括兩個類定義Request和Response。
  if (client.call(srv))//這段程式碼是在呼叫service。由於service的呼叫是模態過程(呼叫的時候佔用程序阻止其他程式碼的執行),一旦呼叫完成,將返回呼叫結果。如果service呼叫成功,call()函式將返回true,srv.response裡面的值將是合法的值。如果呼叫失敗,call()函式將返回false,srv.response裡面的值將是非法的。
  {
    ROS_INFO("Sum: %ld", (long int)srv.response.sum);
  }
  else
  {
    ROS_ERROR("Failed to call service add_two_ints");
    return 1;
  }

  return 0;
}