1. 程式人生 > >rospy-編寫簡單的服務端和客戶端

rospy-編寫簡單的服務端和客戶端

最近一直在實習公司做課程,記錄一些筆記和自己寫的程式碼。基礎知識和程式碼解釋參考wiki、創客製造和其他部落格,侵刪。

背景知識

服務service

ROS中的通訊方式有四種,主題topic、服務service、引數伺服器param、動作庫action。當我們需要直接與節點通訊並以RPC(遠端過程呼叫RemoteProcedureCall)方式獲得應答時,將無法通過主題實現,而需要使用服務。Service通訊是雙向的,它不僅可以傳送訊息,同時還會有反饋。所以service包括兩部分,一部分是請求方(Clinet),另一部分是應答方/服務提供方(Server)。這時請求方(Client)就會發送一個request,要等待

server處理,反饋回一個reply,這樣通過類似“請求-應答”的機制完成整個服務通訊。


種通訊方式的示意圖如下
:

NodeBserver(應答方),提供了一個服務的介面,叫做/Service,我們一般都會用string型別來指定service的名稱,類似於topicNode ANodeB發起了請求,經過處理後得到了反饋。服務需由使用者開發,節點並不提供標準服務。包含訊息原始碼的檔案儲存在srv檔案中。像主題一樣,服務關聯一個以包中.srv檔名稱來命名的服務型別。與其他基於ROS檔案系統的型別一樣,服務型別是包名和.srv檔名的組合。

時間

ROS具有內建的時間和持續的原始型別,在rospy

rospy.Timerospy.Duration實現。

  • 獲取當前時間rospy.Time.now(),rospy.get_rostime()兩個是相同的。

  1. now = rospy.get_rostime( )  

  2. rospy.loginfo("Current time %i %i", now.secs, now.nsecs)

獲取當前時間:rospy.get_time(),獲取浮點值的秒數

  1. seconds= rospy.get_time( )  

使用模擬時鐘的時間,直到在/clock上收到第一條訊息,否則get_rostime() 會得到0值。0值意味客戶端還不知道時間,需要區別對待,迴圈獲取get_rostime()直到非

0值。

  • Sleepingand Rates(睡眠和速率)

rospy.sleep(duration)

duration可以是rospy.Duration或秒。會睡眠指定的時間。

  1. # sleep for 10 seconds

  2. rospy.sleep(10.)  

  3. # sleep for duration

  4. d = rospy.Duration(10, 0)  

  5. rospy.sleep(d)  

rospy.sleep()如果出現錯誤,會丟擲rospy.ROSInterruptException

rospy.Rate(hz),可以保持一定的速率來進行迴圈。

  1. r = rospy.Rate(10) # 10hz

  2. whilenot rospy.is_shutdown(): 

  3. #rospy.is_shutdown()用於檢測程式是否退出,是否按Ctrl-C或其他

  4.     pub.publish("hello")  

  5.     r.sleep()  

Rate.sleep()出現錯誤,丟擲rospy.ROSInterruptException

這兩個函式經常用來實現機器人移動過程中的停頓等功能。

異常

異常型別

ROSExceptionROS客戶端基本異常類

ROSSerializationException,資訊序列化的錯誤異常

ROSInitException,初始化ROS狀態的錯誤異常

ROSInterruptException,操作中斷的錯誤異常,經常在rospy.sleep()and rospy.Rate 中用到

ROSInternalExceptionrospy內部錯誤的異常(i.e.bugs).

ServiceExceptionROS服務通訊相關錯誤的異常

更詳細資訊可以參考http://docs.ros.org/api/rospy/html/rospy.exceptions-module.html

我們只需要知道它的用法即可。

按照學習的知識,編了一個顧客點餐的KFC_demo.

任務描述

寫一個簡單的服務端(server)和客戶端(client),模擬點餐功能,加深對服務通訊方式的理解。服務請求包括菜品,單價和數量,服務回覆需支付的總價。

  • 子任務1建立service_rospy_demo功能包

首先切換到之前通過建立catkin工作空間教程建立的catkin工作空間中的src目錄下:

  1. cd ~/catkin_ws/src  

現在使用catkin_create_pkg命令來建立一個名為service_rospy_demo的功能包,這個程式包依賴於std_msgsroscpprospy

  1. catkin_create_pkg service_rospy_demo std_msgs rospy roscpp

這將會建立一個名為service_rospy_demo的資料夾,這個資料夾裡面包含一個package.xml檔案和一個CMakeLists.txt檔案,這兩個檔案都已經自動包含了部分你在執行catkin_create_pkg命令時提供的資訊。

回到catkin工作空間下,編譯:

  1. cd ..  

  2. catkin_make  

得到如下圖所示的結果則編譯通過。

配置環境變數:

  1. sourcedevel/setup.bash //設定環境變數

  2. echo$ROS_PACKAGE_PATH //檢查環境變數

我們習慣把source~/catkin_ws/devel/setup.bash 命令追加到~/.bashrc檔案中,這樣每次開啟終端,系統就會重新整理工作空間環境,不需要每次都手動配置環境變數。可以通過

echo"source ~/catkin_ws/devel/setup.bash" >> ~/.bashrc

命令來追加。

  • 子任務2定義srv服務

    • 建立新的服務

service_rospy_demo包下新建服務KFC_demo.srv內容如下,我們定義了服務請求的內容,選單menu,單價price,和數量number,以及服務應答的內容賬單(總價bill)

  1. string menu  

  2. float64 price  

  3. int64 number  

  4. ---  

  5. float64 bill 

    • 配置package.xmlCMakeLists.txt檔案

建立好之後,我們需要確保srv檔案能夠被轉換為c++/python或者其他語言的程式碼。開啟service_rospy_demo資料夾裡的package.xml檔案(rosedgeditvim等都可以,rosed命令可以用來呼叫系統的編輯器直接開啟ros相關的檔案進行編輯,比起cd到相應目錄下面開啟更加快捷,可以設定它呼叫的編輯器,預設是vim),在構建階段我們需要 "message_generation",而在執行時我們需要 "message_runtime",所以需要在package.xml檔案裡新增相應的依賴項。確保裡面存在這兩行且去掉它們的註釋:

  1. <!--  <build_depend>message_generation</build_depend> -->    

  2. <!-- <exec_depend>message_runtime</exec_depend> -->  


後修改
CMakeLists.txt檔案。ROScatkin編譯系統會將自定義的msgsrv(甚至還有action)檔案自動編譯構建,生成對應的C++PythonLISP等語言下可用的庫或模組。許多初學者錯誤地以為,只要建立了一個msgsrv檔案,就可以直接在程式中使用,這是不對的,必須在CMakeLists.txt中新增關於訊息建立、指定訊息/服務檔案那幾個巨集命令。

然後開啟包目錄下的CMakeLists.txt檔案,在find_package呼叫中新增message_generation依賴,讓你可以生成ROS資訊。如下所示,括號裡新增一項message_generation即可:

find_package(catkinREQUIRED COMPONENTS

roscpp

rospy

std_msgs

message_generation

)

CMakeLists.txt檔案,增加服務檔案,取消#,並修改為:

add_service_files(
  FILES
  KFC_demo.srv
 )

CMakeLists.txt檔案,增加訊息生成包,取消#,並修改為:
generate_messages(
  DEPENDENCIES
   std_msgs
 )

回到工作空間,catkin_make一下

現在,srv目錄下的所有.srv檔案,都會生成ROS支援的語言的原始碼,C++的標頭檔案在~/catkin_ws/devel/include/service_rospy_demo/裡,Python基本在~/catkin_ws/devel/lib/python2.7/dist-packages/service_rospy_demo/srv/裡,lisp檔案則在~/catkin_ws/devel/share/common-lisp/ros/service_rospy_demo/srv/裡。

  • 子任務3 編寫服務端節點

    • 編寫KFC_server_demo.py檔案

service_rospy_demo/src/script資料夾下建立KFC_server_demo.py檔案。內容程式碼如下:

  1. #!/usr/bin/env python

  2. # coding:utf-8

  3. 上面指定編碼utf-8,使python能夠識別中文

  4. import rospy  

  5. from service_rospy_demo.srv import * #匯入定義的服務  

  6. def kfc_server_srv():  

  7.     # 初始化節點,命名為 "kfc_server"

  8.     rospy.init_node("kfc_server")  

  9.     #定義服務節點名稱,服務的型別,處理函式

  10.     s = rospy.Service("kfc_order", KFC_demo, handle_order_function)  

  11.     print "ready to order:"

  12.     rospy.spin()  

  13. #定義處理函式

  14. def handle_order_function(req):  

  15.     print "The guest wants %s %s. The unit price of the %s is %s."%(req.number,req.menu,req.menu,req.price)  

  16.     #計算客人的賬單

  17.     bill = req.price * req.number  

  18. return KFC_demoResponse(bill)  

  19. 如果單獨執行此檔案,則將上面定義的kfc_server_srv作為主函式執行   

  20. if __name__=="__main__":  

  21.     kfc_server_srv() 

  • 程式碼分析

  • #!/usr/bin/envpython

指定通過python解釋程式碼,這句話是所有Python指令碼必須有的。

  • #coding:utf-8

上面指定編碼utf-8,使python能夠識別中文,如果不加這個,編譯Python指令碼時會出現警告或者錯誤。

  • importrospy

匯入rospy包,rospyROSpython客戶端。參考

  • fromservice_rospy_demo.srv import *

匯入定義的服務,這裡我們定義的服務為service_rospy_demo.srv

  • defkfc_server_srv():

定義函式

  • rospy.init_node("kfc_server")

初始化節點,命名為 "kfc_server",服務端必須是節點,所以必須有節點初始化語句,但客戶端可以不是節點,所以不用必須加這個語句。

  • s= rospy.Service("kfc_order", KFC_demo,handle_order_function) 

定義服務節點名稱,服務的型別,處理函式。處理函式handle_order_function的具體實現將在後面進行定義。

  • print "ready to order:"

當我們啟動服務端節點後,將在終端看到readyto order: 的輸出資訊,加這句話的好處是可以清楚的知道我們的服務節點是否已經成功準備。

  • rospy.spin()

保持節點執行,直到節點關閉。不像roscpp,rospy.spin不影響訂閱的回撥函式,因為回撥函式有自己的執行緒。

  • defhandle_order_function(req):

print "The guest wants %s %s. The unit price of the %s is%s."%(req.number,req.menu,req.menu,req.price)

#計算客人的賬單

bill = req.price * req.number

return KFC_demoResponse(bill)

在任務一中,處理函式相對簡單,只是簡單的列印輸出,在這裡我們對請求部分的資料進行了簡單處理,bill= req.price * req.number,賬單等於請求輸入的單價乘以數量。

  • returnKFC_demoResponse("Hi%s"%req.name)

由服務生成的返回函式,這個函式是自動生成的。

  • if__name__=="__main__":

kfc_server_srv()

如果單獨執行此檔案,則將上面定義的kfc_server_srv作為主函式執行

    • 用命令列的方式呼叫服務

在編寫客戶端Python指令碼檔案之前,我們先用命令列的方式呼叫服務。回到工作空間catkin_ws,編譯程式碼。

$cd ~/catkin_ws
$ catkin_make

開啟終端,執行roscore

開啟新的終端,啟動服務節點:

$rosrun service_rospy_demo KFC_server_demo.py

開啟新終端,列出服務
$rosservice list
檢視服務引數
$rosservice args /kfc_order
呼叫服務
$rosservice call /kfc_order hamburger 11.5 3


識點:

$rosservicelist列出目前正執行的服務

$rosservice args /service_name檢視服務的引數

$rosservicecall/service_name service-args從命令列呼叫服務。

在此例子中,hamburger11.5 3 是我們輸入的引數,當然也可以輸入別的值。

  • 子任務4編寫客戶端節點

scripts目錄新建KFC_client_demo.py檔案。程式碼如下:

  1. #!/usr/bin/env python

  2. # coding:utf-8

  3. #sys模組包含了與Python直譯器和它的環境有關的函式。

  4. import sys  

  5. import rospy  

  6. from service_rospy_demo.srv import *  

  7. def kfc_client_srv(m,p,n):  

  8.     # 服務客戶端不必是節點,所以不用呼叫rospy.init_node

  9.     # 等待有可用的服務 "kfc_order"

  10.     rospy.wait_for_service("kfc_order")  

  11.     #呼叫服務求解結果並將結果返回 

  12. try:  

  13.         # 定義service客戶端,建立服務處理控制代碼.service名稱為kfc_order”service型別為KFC_demo

  14.         kfc_client = rospy.ServiceProxy("kfc_order",KFC_demo)       

  15.         resp = kfc_client(m,p,n)  

  16.         #上述為簡化風格,也可用正式的。

  17.         #resp = kfc_client.call(KFC_demoRequest(m,p,n)

  18. return resp.bill  

  19.     except rospy.ServiceException, e:  

  20.         print "Service call failed: %s"%e  

  21. def usage():  

  22. return"%s [m p n]"%sys.argv[0]  

  23. 如果單獨執行此檔案,則將上面函式kfc_client_srv()作為主函式執行

  24. if __name__=="__main__":  

  25.     #判斷客戶端輸入的引數是否符合條件 

  26. if len(sys.argv)==4:  

  27.        m=str(sys.argv[1])  

  28.        p=float(sys.argv[2])  

  29.        n=int(sys.argv[3])  

  30.        print "The guest wants %s %s. The unit price of the %s is %s."%(