---
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>

* 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) 的位置
```

### <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` 的小烏龜。

從終端也可以發現生成了一隻新烏龜。
>```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
>```
>
* **編輯 `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]`路徑下找到三個標頭檔。

:::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 是否在執行。

:::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` 為呼叫時放在標頭的參數。