--- tags : DIT 2023寒假 -- ROS教學 --- # DAY 5 -- Service 通訊格式介紹 {%hackmd @HungPin/BkVDWAea3 %} ## <font color="orange"> 01. ROS Service 介紹</font> ROS除了先前介紹過的topic通訊方式,還有一種很重要的通訊方式,就是`serivce`啦! ### <font color='yellow'> service 的架構</font> ![](https://i.imgur.com/PWhQbsE.png) * service的通訊機制是雙向且同步的,架構主要分成Client、Server這兩個部分,==client所在的node可以發出請求(request),調取server所在的node進行回覆(response)==。 * 當client發布請求後,會等待server回覆才會繼續執行。 * 一個service可以有==多個client對應到一個server==。 * 常用的指令為`rosservice`、`rossrv`,分別對應到`rostopic`和`rosmsg`。 ### <font color='yellow'> service 與 topic的不同</font> 這裡統整一下service與topic的差異: | | topic | service | |:----------:|:----------------------:|:---------------------:| | 通訊模式 | publisher + subscriber | server + client | | 是否有回授 | 無 | 有 | | 緩衝區 | 有 | 無 | | 節點關係 | 多對多 | 一個sever對多個client | | 傳輸格式 | .msg | .srv | | 使用時機 | 數據傳輸 | 邏輯處理 | 就topic而言,publisher只負責發布msg,subscriber負責接收資料,各自處理各自的事情,不管對方有沒有發布或接收,效率很高但缺乏回授;而service正好相反。 **可以==依據需求來選擇使用何種通訊格式==。** ## <font color="orange"> 02. 小烏龜的 Service</font> ### <font color='yellow'> Step 1. 開啟小烏龜</font> ```txt= rosrun turtlesim turtlesim_node [ INFO] [1674157151.611537720]: Starting turtlesim with node name /turtlesim [ INFO] [1674157151.617162807]: Spawning turtle [turtle1] at x=[5.544445], y=[5.544445], theta=[0.000000] //可以看到生成了一隻叫 turtle1 的烏龜在 (5.5444,5.544) 的位置 ``` ![](https://i.imgur.com/pbjxPIS.png) ### <font color='yellow'> Step 2. 查看現在正在運行的service</font> ```txt= rosservice list //與rostopic list很像,下方會列出正在運行的service /clear /kill /reset /rosout/get_loggers /rosout/set_logger_level /spawn /turtle1/set_pen /turtle1/teleport_absolute /turtle1/teleport_relative /turtlesim/get_loggers /turtlesim/set_logger_level ``` ### <font color='yellow'> Step 3. 試試看service功能</font> * **試試看一個叫做 `/spawn` 的 service** >使用 `rosservice args [service_name]` 看一下這個 service 有什麼 arguments 可以填入。 > >```txt= >rosservice args /spawn >x y theta name >``` >可以看到他有四個 arguments,這個 service 可以在指定 >`(x,y,theta)` 位置生成一隻turtle。 * **接下來call這個service (一次複製下面全部四句)** >```txt= >rosservice call /spawn "x: 10.0 >y: 10.0 >theta: 0.0 >name: 'turtle2' " >``` >`rosservice call [service_name]` 加上 argments 的值就可以調用>這個service,有點像是API的概念。 > >執行完call後,可以發現終端多出一個回傳值,這個是伺服器回傳生成的小烏龜的名稱。 >```txt= >rosservice call /spawn "x: 10.0 >y: 10.0 >theta: 0.0 >name: 'turtle2' " >name: "turtle2" // the response from server >``` ### <font color='yellow'> Step 4. service 執行結果</font> * `/spawn` 執行結果 >可以看到在 `(10.0,10.0)` 的位置生成了一隻名叫 `turtle2` 的小烏龜。 ![](https://i.imgur.com/e8o36Qf.png) 從終端也可以發現生成了一隻新烏龜。 >```txt= >rosrun turtlesim turtlesim_node >[ INFO] [1674157151.611537720]: Starting turtlesim with node name /turtlesim >[ INFO] [1674157151.617162807]: Spawning turtle [turtle1] at x=[5.544445], y=[5.544445], theta=[0.000000] >[ INFO] [1674158707.706775340]: Spawning turtle [turtle2] at x=[10.000000], y=[10.000000], theta=[0.000000] >``` * **查看 `srv` 通訊格式** >使用 `rosservice info` 確定 `/spawn` 是使用哪一個 `srv` 。 > >```= >rosservice info /spawn >Node: /turtlesim >URI: rosrpc://LAPTOP-7US9R6GP:53405 >Type: turtlesim/Spawn >Args: x y theta name >``` >可以看到Type那邊顯示使用的srv名稱。 > >接下來用 `rossrv show [srv_name]` 印出通訊格式。 > >```cpp= >rossrv show turtlesim/Spawn >float32 x >float32 y >float32 theta >string name >--- >string name >``` >`---`上面代表的是 client 發出的 request 內容,指定新烏龜的位置、朝向、名稱。 `---`下面則是 server 的 response ,==創建成功後==回覆新烏龜的名稱。 >:::info >**注意的是 response 只會在 client 發出的請求成功被執行後才會回覆!** >::: ## <font color="orange"> 03. 自定義 Service </font> ### <font color='yellow'> Step 1. 創建 srv 檔案</font> * **先在package目錄底下新增一個名叫 `srv` 的資料夾。** >**將自訂義的 `srv` 都放在該資料夾內。** >```cpp= >~/winter_ws/src/turtle_pkg$ mkdir srv >``` * **在資料夾內新增一個叫做 `test` 的 `srv` 文件。** >```cpp= >~/winter_ws/src/turtle_pkg/srv$ code test.srv >``` >![](https://i.imgur.com/XuBhSvG.png) * **編輯 `test.srv`** >在 `test.srv` 內輸入下列幾行。 >:::info >**`---`的上面代表的是 client 發出的 request 內容。 >`---`的下面則是 server 的 response 。** >::: >```cpp= >float32 a >float32 b >--- >float32 c >``` ### <font color='yellow'> Step 2. 修改文件</font> :bookmark_tabs: 基本上跟 [DAY 4.](/s/W1intuWdR7W301EcN0WHLQ#01-建立自定義的-message) 新增 msg 的內容很像,有不懂的地方再回去看看詳細的解釋。 * **編輯 `package.xml`** >將以下這兩句的註解打開 >```txt= ><build_depend>message_generation</build_depend> ><exec_depend>message_runtime</exec_depend> >``` * **編輯 `CMakeLists.txt`** >在下面這些地方內添加相應的句子。 >```cpp=10 >find_package(catkin REQUIRED COMPONENTS > geometry_msgs > roscpp > rospy > std_msgs > turtlesim > message_generation // add message_generation >) >``` > >```cpp=59 >## Generate services in the 'srv' folder >add_service_files( > FILES > test.srv // add test.srv > # Service1.srv > # Service2.srv >) >``` >```cpp=75 >## Generate added messages and services with any dependencies listed here >generate_messages( > DEPENDENCIES > geometry_msgs > std_msgs // 新增msg格式,這次自定義的 srv 使用的 float32 即是該pkg的內容 >) >``` >```cpp=110 >catkin_package( ># INCLUDE_DIRS include ># LIBRARIES turtle_pkg > //add message_runtime > CATKIN_DEPENDS geometry_msgs roscpp rospy std_msgs turtlesim message_runtime ># DEPENDS system_lib >) >``` ### <font color='yellow'> Step 3. 編譯 srv</font> 首先在 workspace 目錄下 `catkin_make` 。 ```cpp= ~/winter_ws$ catkin_make ``` 如編譯成功可在 `[workspace_name]/devel/include/[pkg_name]`路徑下找到三個標頭檔。 ![](https://i.imgur.com/QAxNY8k.png) :::danger :warning: **特別注意在還未編譯srv前,CMakeLists.txt編譯的程式中不可以用到該srv,否則會編譯不過,需先將含有srv的程式註解掉,在進行編譯。** ::: ## <font color="orange"> 04. Service 實作 for C++</font> ### <font color='yellow'> Server node </font> #### <font color='pink'>Step 1. 創建 Server node </font> 在 `/pkg/src` 目錄下創建`.cpp`檔案 ```cpp= ~/winter_ws/src/turtle_pkg/src$ code add_server.cpp ``` #### <font color='pink'>Step 2. 編輯Server node </font> ```cpp= #include <ros/ros.h> #include <turtle_pkg/test.h> bool add_callback(turtle_pkg::test::Request &req,turtle_pkg::test::Response &res) { res.c = req.a +req.b; std::cout<<"The server responsed\n"; return true; } int main(int argc, char **argv) { ros::init(argc,argv,"add_server"); ros::NodeHandle nh; ros::ServiceServer server = nh.advertiseService("add_two_number",add_callback); while(ros::ok()){ std::cout<<"Waiting to add\n"; ros::spin(); } return 0; } ``` #### <font color='pink'>Step 3. C++ Server 解析 </font> ```cpp=3 #include <turtle_pkg/test.h> ``` 需要引入service的標頭檔,也就是先前在 `/devel/include/[pkg_name]` 內看到的標頭檔。 ```cpp=16 ros::ServiceServer server = nh.advertiseService("add_two_number",add_callback); ``` * `advertiseServer()`會將建立的 service 資料告訴 master,並回傳一個 ServiceServer 物件(這裡為 server )。 * `"add_two_number"`為 service 名稱,可自命名,==但 server & client 必須要一樣。== * `add_callback` server 的 callback function 。 ```cpp=5 bool add_callback(turtle_pkg::test::Request &req,turtle_pkg::test::Response &res) ``` * `turtle_pkg::test::Request &req` 參數設置一,指定該 srv 的 Request 類別。 * `turtle_pkg::test::Response &res` 參數設置二,指定該 srv 的 Response 類別。 ### <font color='yellow'> Client node </font> #### <font color='pink'>Step 1. 創建 Client node </font> 在 `/pkg/src` 目錄下創建`.cpp`檔案 ```cpp= ~/winter_ws/src/turtle_pkg/src$ code add_client.cpp ``` #### <font color='pink'>Step 2. 編輯Client node </font> ```cpp= #include <ros/ros.h> #include <turtle_pkg/test.h> int main(int argc, char **argv) { ros::init(argc,argv,"add_client"); ros::NodeHandle nh; ros::ServiceClient client = nh.serviceClient<turtle_pkg::test>("add_two_number"); turtle_pkg::test srv; while(ros::ok()){ std::cout<<"a = "; std::cin>>srv.request.a; std::cout<<"b = "; std::cin>>srv.request.b; if(client.call(srv)){ std::cout<<"The response is "<<srv.response.c<<"\n"; } else{ std::cout<<"Failed to call service\n"; } } return 0; } ``` #### <font color='pink'>Step 3. C++ Client 解析 </font> ```cpp=9 ros::ServiceClient client = nh.serviceClient<turtle_pkg::test>("add_two_number"); ``` * `serviceClient` 會將建立的 client 資料告訴 master,並回傳一個 ServiceClient 物件(這裡為 client )。 * `<turtle_pkg::test>` 在 `serviceClient` 後需要填入使用的 service 類別。 ```cpp=10 turtle_pkg::test srv; ``` 宣告一個名叫 `srv` 的變數 , `turtle_pkg::test` 為service類別。 ```cpp=17 client.call(srv) ``` 以ServiceClient物件呼叫call(),傳入srv內,並回傳bool值。 ### <font color='yellow'> C++ 結果測試 </font> #### <font color='pink'>Step 1. 編輯CMakeLists.txt & 編譯</font> 加入下列語句後,要記得 `catkin_make` 編譯喔! ```cpp= add_executable(add_server src/add_server.cpp) target_link_libraries(add_server ${catkin_LIBRARIES}) add_executable(add_client src/add_client.cpp) target_link_libraries(add_client ${catkin_LIBRARIES}) ``` #### <font color='pink'>Step 2. 執行 node </font> 開兩個終端分別執行 `add_server` & `add_client` 這兩個 node。 執行前記得要 `roscore`! ```cpp= rosrun turtle_pkg add_server rosrun turtle_pkg add_client ``` 試試看執行結果! 利用 `rosservice` 指令查看 service 是否在執行。 ![](https://i.imgur.com/vue8JG9.png) :::info :page_with_curl: **試試看利用API直接使用 `/add_two_number` 這個 service!** ::: ## <font color="orange"> 05. Service 實作 for Python</font> ### <font color='yellow'> Server node </font> #### <font color='pink'>Step 1. 創建 Server node </font> 在 `/pkg/src` 目錄下創建`.py`檔案 ```cpp= ~/winter_ws/src/turtle_pkg/src$ code add_server.py ~/winter_ws/src/turtle_pkg/src$ chmod add_server.py ``` #### <font color='pink'>Step 2. 編輯Server node </font> ```python= #!/usr/bin/env python3 #coding:utf-8 import rospy from turtle_pkg.srv import * def server(): rospy.init_node("server_py") s = rospy.Service("add_two_number_py",test,handle_function) rospy.loginfo("waiting to request") rospy.spin() def handle_function(req): rospy.loginfo("two number are %lf and %lf" , req.a, req.b) return testResponse(req.a+req.b) if __name__=="__main__": server(); ``` #### <font color='pink'>Step 3. Python Server 解析 </font> ```python=9 s = rospy.Service("add_two_number_py",test,handle_function) ``` 定義為:`rospy.Service(name, service_class, handler, buff_size)` * `name` 為 service name 。 * `service_class` 為使用的 service 格式 。 * `handler` 為呼叫 service 時的 callback function 。 ```python=15 return testResponse(req.a+req.b) ``` return server 的 response,格式為 `[service_name]+Response` 。 ### <font color='yellow'> Client node </font> #### <font color='pink'>Step 1. 創建 Client node </font> 在 `/pkg/src` 目錄下創建`.py`檔案 ```cpp= ~/winter_ws/src/turtle_pkg/src$ code add_client.py ~/winter_ws/src/turtle_pkg/src$ chmod +x add_client.py ``` #### <font color='pink'>Step 2. 編輯Client node </font> ```python= #!/usr/bin/env python3 #coding:utf-8 import rospy from turtle_pkg.srv import * def client(a,b): rospy.init_node("client_py") rospy.loginfo("a = %lf, b = %lf",a,b) rospy.wait_for_service("add_two_number_py") try: test_client = rospy.ServiceProxy("add_two_number_py",test) resp = test_client(a,b) rospy.loginfo("The response is %lf", resp.c) except rospy.ServiceException: rospy.logwarn("Service call failde") if __name__=="__main__": a = 1 b = 2 client(a,b); ``` #### <font color='pink'>Step 3. Python Client 解析 </font> ```python=10 rospy.wait_for_service("add_two_number_py") ``` 定義為:`rospy.wait_for_service(service, timeout=None)` * `service` 是 service name 。 * `timout` 可以設定等待時間,如果超過則回傳一個 exception 。 ```python=12 test_client = rospy.ServiceProxy("add_two_number_py",test) ``` 有點像 client 的 API 連接 server。 定義為:`rospy.ServiceProxy(name, service_class, persistent=False, headers=None)` * `persistent` 為讓 client 與 server 一直持續的連接。 * `headers` 為呼叫時放在標頭的參數。