1. 程式人生 > >ros 工作空間的覆蓋、c++程式設計介面實現通訊方式、自定義訊息格式、分散式多機通訊

ros 工作空間的覆蓋、c++程式設計介面實現通訊方式、自定義訊息格式、分散式多機通訊

工作空間的覆蓋

同一個工作空間下,不允許出現同名的功能包,否則無法編譯。不同工作空間下,允許出現同名的功能包,但會出現工作空間覆蓋的現象。

ros工作空間的路徑記錄在ROS_PACKAGE_PATH環境變數中,可以通過env命令檢視計算機中的環境變數,再利用管道過濾出有關ros的環境變數。

env | grep ros

新新增的工作空間路徑會自動放置在ROS_PACKAGE_PATH環境變數最前端。ros執行時,會按照順序從前往後查詢工作空間中是否存在指定的功能包。一旦找到指定的功能包就不再查詢後面的工作空間。因此,如果我們的系統當中存在名稱相同的功能包,就會出現工作空間向後覆蓋的問題。當我們使用rospack find命令查詢功能包的時候,只能查詢到最新路徑下的同名功能包,而一旦有第三方功能包依賴於原來的同名功能包,就有可能會出現問題。

通訊方式的c++實現

話題

釋出者實現步驟大致如下:

1、初始化ROS節點

2、向ROS Master註冊節點資訊,包括髮布的話題名,訊息型別

3、設定一定的頻率迴圈釋出訊息

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

int main(int argc,char **argv)
{
    ros::init(argc,argv,"talker"); //ROS節點的初始化,指定節點名稱為talker
    ros::NodeHandle n; //建立節點控制代碼
    ros::Publisher pub=n.advertise<std_msgs::String>("greet",1000); 
    //建立一個釋出者pub,指定訊息型別為std_msgs::String,話題名稱為greet,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());
        pub.publish(msg); //釋出訊息
        ros::spinOnce(); //等待訊息呼叫一次回撥函式
        loop_rate.sleep(); //按照迴圈頻率休眠
        ++count;
    }

    return 0;
}

訂閱者實現步驟大致如下:

1、初始化ROS節點

2、向ROS Master註冊節點資訊,包括訂閱的話題名,回撥函式

3、迴圈等待話題訊息,接收到訊息後進入回撥函式

4、在回撥函式中處理訊息

#include "ros/ros.h"
#include "std_msgs/String.h"

void greetCallback(const std_msgs::String::ConstPtr& msg)
// 接收到訂閱的訊息後,會進入訊息回撥函式
{
  ROS_INFO("I heard: [%s]", msg->data.c_str());// 將接收到的訊息打印出來
}

int main(int argc, char **argv)
{
  ros::init(argc, argv, "listener");// 初始化ROS節點
  ros::NodeHandle n;// 建立節點控制代碼
  ros::Subscriber sub = n.subscribe("greet", 1000, greetCallback);
  // 建立一個Subscriber,訂閱名為greet的topic,註冊回撥函式greetCallback
  ros::spin();// 迴圈等待訊息呼叫回撥函式
  return 0;
}

修改CMakeLists檔案

1)、設定要編譯的檔案

 add_executable(${PROJECT_NAME}_node src/a_node.cpp)

其中,${PROJECT_NAME}_node 代表節點名稱,a_node.cpp 表示對應的c++檔案,修改這兩個引數即可。當然有時候一個節點可能對應著多個c++檔案,這種情況需要把所有需要的檔名都列出來。

add_executable(talker src/talker.cpp)
add_executable(listener src/talker.cpp)

2)、設定連結庫

 target_link_libraries(${PROJECT_NAME}_node    ${catkin_LIBRARIES}  )

其中,${PROJECT_NAME}_node 表示節點名稱,這裡只需要修改這一個引數即可,因為沒有使用第三方的連結庫。${catkin_LIBRARIES} 代表的是預設的ros catkin連結庫,如果我們用到了第三方庫的時候可以將第三方庫的名稱放到預設庫後面即可。

target_link_libraries(talker    ${catkin_LIBRARIES}  )
target_link_libraries(listener    ${catkin_LIBRARIES}  )

3)、設定依賴

add_dependencies(${PROJECT_NAME}_node ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS} )

其中,${PROJECT_NAME}_node  表示節點名稱,${${PROJECT_NAME}_EXPORTED_TARGETS} 表示在本package中依賴的訊息,${catkin_EXPORTED_TARGETS} 表示對其他package訊息的依賴。

當使用自定義的message、service、action時注意新增。這裡的例子當中並沒有使用自定義的訊息,所以不用新增依賴。

編譯功能包

回到工作空間,執行catkin_make對功能包進行編譯。編譯完成後即可生成talker和listener兩個可執行檔案,設定好工作空間的環境變數之後就可以通過rosrun來運行了,效果如下:

[email protected]:~$ rosrun a talker
[ INFO] [1537440776.481837500]: hello world0
[ INFO] [1537440776.582547600]: hello world1
[ INFO] [1537440776.682226900]: hello world2
[ INFO] [1537440776.781844500]: hello world3
[ INFO] [1537440776.882372400]: hello world4
[ INFO] [1537440776.982076300]: hello world5
[ INFO] [1537440777.082023100]: hello world6
[ INFO] [1537440777.181946400]: hello world7
[ INFO] [1537440777.283714400]: hello world8
[email protected]:~$ rosrun a listener
[ INFO] [1537440803.599349400]: hello world0
[ INFO] [1537440803.700052700]: hello world1
[ INFO] [1537440803.799371600]: hello world2
[ INFO] [1537440803.899708500]: hello world3
[ INFO] [1537440803.999728600]: hello world4
[ INFO] [1537440804.099783500]: hello world5
[ INFO] [1537440804.199640000]: hello world6
[ INFO] [1537440804.299546000]: hello world7
[ INFO] [1537440804.399672300]: hello world8

自定義話題訊息

1)、定義 .msg 檔案

話題訊息的定義格式:

定義一個變數列表即可,類似於c語言結構體。定義變數:資料型別+變數名 eg:我們自定義一個學生基本資訊的訊息stu.msg

如上圖,自定義訊息格式中還可以定義符號常量,類似於c語言中的define。即male代表1,female代表2,可以直接將這兩個符號常量賦給sex。

2)、修改package.xml檔案,新增功能包依賴

<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>

indigo之前的ros版本需要將exec_depend改成run_depend。

3)、修改CMakeLists.txt檔案,新增編譯選項

find_package(...... message_generation) #生成自定義的訊息需要使用message_generation功能包

catkin_package(CATKIN_DEPENDS geometry_msgs roscpp rospy std_msgs message_runtime) #新增編譯依賴message_runtime

add_message_files(FILES Person.msg) #新增訊息檔案
generate_messages(DEPENDENCIES  std_msgs) #新增生成依賴

經過以上三個步驟,我們就可以使用catkin_make對自定義訊息進行編譯了。編譯完成之後,會在工作空間的 devel/include/【功能包名】路徑下生成自定義訊息的標頭檔案 stu.h 。編寫節點時只要包含了這個標頭檔案( #include<【功能包名】/ stu.h> )就可以使用這個訊息進行通訊了。使用該訊息型別建立訊息物件的方式為: 【功能包名】:: stu  物件名; 

服務

自定義服務訊息

1)、定義 .srv 檔案

服務型別的定義格式:

變數的定義方式與話題訊息一致,即:資料型別+變數名,但與訊息不同的是,需要用三個短橫線(---)把請求資料和應答資料分隔開。 eg:我們自定義一個加法的服務addition.srv

2)、修改package.xml檔案,新增功能包依賴(同msg操作)

<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>

3)、修改CMakeLists.txt檔案,新增編譯選項(除新增的檔案不同,其他與msg操作相同)

find_package(...... message_generation) #生成自定義的訊息需要使用message_generation功能包

catkin_package(CATKIN_DEPENDS geometry_msgs roscpp rospy std_msgs message_runtime) #新增編譯依賴message_runtime

​add_service_files(FILES addition.srv) #新增服務型別檔案addition.srv
generate_messages(DEPENDENCIES  std_msgs) #新增生成依賴

經過以上三個步驟,我們就可以使用catkin_make對自定義服務進行編譯了。編譯完成之後,會在工作空間的 devel/include/【功能包名】路徑下生成自定義服務的標頭檔案 addition.h、additionRequest.h、additionResponse.h 。編寫節點時只要包含第一個標頭檔案( #include<【功能包名】/ addition.h> )就可以使用這個服務進行通訊了。使用該服務型別建立服務物件的方式為: 【功能包名】:: addition  物件名; 

伺服器實現步驟大致如下:

1、初始化ROS節點

2、建立server例項

3、迴圈等待服務請求,接收到請求後進入回撥函式

4、在回撥函式中處理服務請求,並反饋應答資料

#include "ros/ros.h"
#include "a/addition.h"
// service回撥函式,輸入引數req,輸出引數res
bool add(a::addition::Request  &req, a::addition::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 main(int argc, char **argv)
{
  ros::init(argc, argv, "add_two_ints_server"); // ROS節點初始化
  ros::NodeHandle n;// 建立節點控制代碼
  // 建立一個名為add_two_ints的server,註冊回撥函式add()
  ros::ServiceServer service = n.advertiseService("add_two_ints", add);
  ROS_INFO("Ready to add two ints.");
  ros::spin();// 迴圈等待回撥函式
  return 0;
}

客戶端實現步驟大致如下:

1、初始化ROS節點

2、建立client例項

3、釋出服務請求

4、等待server處理之後的應答結果

#include <cstdlib>
#include "ros/ros.h"
#include "a/addition.h"
int main(int argc, char **argv)
{
  ros::init(argc, argv, "add_two_ints_client");// ROS節點初始化
  // 從終端命令列獲取兩個數
  if (argc != 3)
  {
    ROS_INFO("usage: add_two_ints_client X Y");
    return 1;
  }
  ros::NodeHandle n;// 建立節點控制代碼
  // 建立一個client,請求add_two_int service,service訊息型別是a::addition
  ros::ServiceClient client = n.serviceClient<a::addition>("add_two_ints");
  a::addition srv;// 建立a::addition型別的service訊息
  srv.request.a = atoll(argv[1]);
  srv.request.b = atoll(argv[2]);
  // 釋出service請求,等待加法運算的應答結果
  if (client.call(srv))
  {
    ROS_INFO("Sum: %ld", (long int)srv.response.sum);
  }
  else
  {
    ROS_ERROR("Failed to call service add_two_ints");
    return 1;
  }
  return 0;
}

修改CMakeLists檔案

1)、設定要編譯的檔案(同話題)

add_executable(server src/server.cpp)
add_executable(client src/client.cpp)

2)、設定連結庫(同話題)

target_link_libraries(server    ${catkin_LIBRARIES}  )
target_link_libraries(client    ${catkin_LIBRARIES}  )

3)、設定依賴(同話題)

add_dependencies(server ${PROJECT_NAME}_gencpp)
add_dependencies(client ${PROJECT_NAME}_gencpp)

編譯功能包

回到工作空間,執行catkin_make對功能包進行編譯。編譯完成後即可生成server和client兩個可執行檔案,然後就可以通過rosrun來運行了,效果如下:

[email protected]:~$ rosrun a client 1 2
[ INFO] [1537523575.202664700]: Sum: 3
[email protected]:~$ rosrun a server
[ INFO] [1537523561.898041700]: Ready to add two ints.
[ INFO] [1537523575.202092800]: request: x=1, y=2
[ INFO] [1537523575.202301100]: sending back response: [3]

動作

自定義動作訊息

1)、定義 .action檔案

服務型別的定義格式:

變數的定義方式與話題以及服務訊息一致,即:資料型別+變數名,但與訊息和服務不同的是,需要動作訊息需要用兩條由三個短橫線(---)構成的虛線把目標資訊、結果資訊和反饋資訊分隔開。 eg:我們自定義一個洗碗機的服務DoDishes.srv

第一部分定義的是目標資料,使用哪一臺洗碗機;第二部分定義的是結果資料,任務成功完成與否;第三部分定義的是週期反饋的狀態資料,任務完成的進度。

2)、修改package.xml檔案,新增功能包依賴

<build_depend>actionlib</build_depend> 
<build_depend>actionlib_msgs</build_depend> 
<exec_depend>actionlib</exec_depend> 
<exec_depend>actionlib_msgs</exec_depend>

3)、修改CMakeLists.txt檔案,新增編譯選項(除新增的檔案不同,其他與msg操作相同)

find_package(......  actionlib_msgs actionlib) #生成自定義的訊息需要使用 actionlib_msgs、actionlib功能包

​add_action_files(FILES DoDishes.action) #新增動作訊息檔案DoDishes.action
generate_messages(DEPENDENCIES  actionlib_msgs) #新增生成依賴

經過以上三個步驟,我們就可以使用catkin_make對自定義動作進行編譯了。編譯完成之後,會在工作空間的 devel/include/【功能包名】路徑下生成自定義動作的標頭檔案 DoDishesAction.h、DoDishesActionFeedback.h、DoDishesActionResult.h 、DoDishesResult.h、DoDishesActionGoal.h、DoDishesFeedback.h、DoDishesGoal.h。編寫節點時只要包含第一個標頭檔案( #include<【功能包名】/ DoDishesAction.h> )就可以使用這個動作進行通訊了。

伺服器實現步驟大致如下:

1、初始化ROS節點

2、建立action server例項

3、啟動伺服器,等待動作請求

4、接收到請求後進入回撥函式處理動作請求,並反饋進度資訊

5、動作完成,傳送結束資訊

#include <ros/ros.h>
#include <actionlib/server/simple_action_server.h>
#include "a/DoDishesAction.h"
typedef actionlib::SimpleActionServer<a::DoDishesAction> Server;

// 收到action的goal後呼叫該回調函式
void execute(const a::DoDishesGoalConstPtr& goal, Server* as)
{
    ros::Rate r(1);
    a::DoDishesFeedback feedback;
    ROS_INFO("Dishwasher %d is working.", goal->dishwasher_id);
    // 假設洗盤子的進度平均為10%,並且按照1hz的頻率釋出進度feedback
    for(int i=1; i<=10; i++)
    {
        feedback.percent_complete = i * 10;
        as->publishFeedback(feedback);
        r.sleep();
    }
    // 當action完成後,向客戶端返回結果
    ROS_INFO("Dishwasher %d finish working.", goal->dishwasher_id);
    as->setSucceeded();
}

int main(int argc, char** argv)
{
    ros::init(argc, argv, "do_dishes_server");
    ros::NodeHandle n;
    // 定義一個伺服器
    Server server(n, "do_dishes", boost::bind(&execute, _1, &server), false);
    server.start();// 伺服器開始執行
    ros::spin();
    return 0;
}

客戶端實現步驟大致如下:

1、初始化ROS節點

2、建立action client例項

3、連線動作伺服器

4、傳送動作目標

5、根據伺服器的不同反饋資訊,處理回撥函式

#include <actionlib/client/simple_action_client.h>
#include "a/DoDishesAction.h"
typedef actionlib::SimpleActionClient<a::DoDishesAction> Client;

// 當action完成後會呼叫該回調函式一次
void doneCb(const actionlib::SimpleClientGoalState& state,
        const a::DoDishesResultConstPtr& result)
{
    ROS_INFO("Yay! The dishes are now clean");
    ros::shutdown();
}

// 當action啟用後會呼叫該回調函式一次
void activeCb()
{
    ROS_INFO("Goal just went active");
}

// 收到feedback後呼叫該回調函式
void feedbackCb(const a::DoDishesFeedbackConstPtr& feedback)
{
    ROS_INFO(" percent_complete : %f ", feedback->percent_complete);
}

int main(int argc, char** argv)
{
    ros::init(argc, argv, "do_dishes_client");
    Client client("do_dishes", true); // 定義一個客戶端
    ROS_INFO("Waiting for action server to start.");
    client.waitForServer(); // 等待伺服器端
    ROS_INFO("Action server started, sending goal.");
    a::DoDishesGoal goal;  // 建立一個action的goal
    goal.dishwasher_id = 1;
    // 傳送action的goal給伺服器端,並且設定回撥函式
    client.sendGoal(goal,  &doneCb, &activeCb, &feedbackCb);
    ros::spin();
    return 0;
}

修改CMakeLists檔案

1)、設定要編譯的檔案(同話題)

add_executable(DoDishes_client src/DoDishes_client.cpp) 
add_executable(DoDishes_server src/DoDishes_server.cpp)

2)、設定連結庫(同話題)

target_link_libraries(DoDishes_client    ${catkin_LIBRARIES}  )
target_link_libraries(DoDishes_server    ${catkin_LIBRARIES}  )

3)、設定依賴(同話題)

add_dependencies(DoDishes_client ${${PROJECT_NAME}_EXPORTED_TARGETS})
add_dependencies(DoDishes_server ${${PROJECT_NAME}_EXPORTED_TARGETS})

編譯功能包

回到工作空間,執行catkin_make對功能包進行編譯。編譯完成後即可生成DoDishes_client和DoDishes_server兩個可執行檔案,然後就可以通過rosrun來運行了,效果如下:

[email protected]:~$ rosrun b DoDishes_client
[ INFO] [1537541980.774761400]: Waiting for action server to start.
[ INFO] [1537541981.204468700]: Action server started, sending goal.
[ INFO] [1537541981.205188000]: Goal just went active
[ INFO] [1537541981.206087300]:  percent_complete : 10.000000
[ INFO] [1537541982.205776000]:  percent_complete : 20.000000
[ INFO] [1537541983.205963000]:  percent_complete : 30.000000
[ INFO] [1537541984.205360000]:  percent_complete : 40.000000
[ INFO] [1537541985.205488100]:  percent_complete : 50.000000
[ INFO] [1537541986.205560800]:  percent_complete : 60.000000
[ INFO] [1537541987.205739900]:  percent_complete : 70.000000
[ INFO] [1537541988.205424100]:  percent_complete : 80.000000
[ INFO] [1537541989.205597000]:  percent_complete : 90.000000
[ INFO] [1537541990.205898500]:  percent_complete : 100.000000
[ INFO] [1537541991.208537400]: Yay! The dishes are now clean
[email protected]:~$ rosrun b DoDishes_server
[ INFO] [1537541981.205257600]: Dishwasher 1 is working.
[ INFO] [1537541991.207887100]: Dishwasher 1 finish working.

分散式通訊

前面我們介紹ros的時候提到,它是一種分散式的軟體框架,節點之間通過鬆耦合的方式進行組合。

多機通訊

設定IP地址,確認網路連通性

首先需要將 對方主機的ip地址和主機名 新增到 本機的 /etc/hosts 檔案 中:

sudo vi /etc/hosts

然後在終端中使用 ping 命令測試網路連通性。

[email protected]:~$ ping ldz-dell
PING ldz-dell (192.168.43.144) 56(84) bytes of data.
64 bytes from ldz-dell (192.168.43.144): icmp_seq=1 ttl=64 time=1415 ms
64 bytes from ldz-dell (192.168.43.144): icmp_seq=2 ttl=64 time=1587 ms
64 bytes from ldz-dell (192.168.43.144): icmp_seq=3 ttl=64 time=1450 ms
[email protected]:~$ ping MSI
PING MSI (192.168.43.64) 56(84) bytes of data.
64 bytes from MSI (192.168.43.64): icmp_seq=1 ttl=64 time=1351 ms
64 bytes from MSI (192.168.43.64): icmp_seq=2 ttl=64 time=1597 ms
64 bytes from MSI (192.168.43.64): icmp_seq=3 ttl=64 time=1692 ms

在從機中設定節點管理器所在的地址(ROS_MASTER_URI)

在 .bashrc檔案中追加一句,設定節點管理器的地址在主控機的11311埠,11311為節點管理器的預設埠,這裡的MSI是我的主控機主機名。

echo "export ROS_MASTER_URI=http://MSI:11311" >> ~/.bashrc
source ~/.bashrc

OK,經過以上配置,我們的多機通訊環境就搭建好了。只要我們就在主控機上執行節點管理器,所有從機上就都可以執行節點了,而不必在每個機器上都執行節點管理器。