The practice of action service in ROS communication mechanism

1. Action overview

After talking about services  , the next step is the third mechanism of communication, actions . In the previous section, we know that service application scenarios need to be completed within a limited time, and for some more complex tasks that take a long time, or even when the time is uncertain, we need to use actions as a communication method.
In principle, actions are implemented using topics , and feedback is introduced to provide progress information of the operation. That is to say, during the execution of the task, feedback can be obtained periodically to help us understand which step the operation has reached, which is essentially equivalent to A high-level protocol that specifies how to use a combination of topics (goals, feedback, results, etc.).
The definition and use of actions are similar to services, a little more complicated, but very powerful and flexible.

2. Definition of action

Create an action. The format inside is divided into three parts: goal, result, and feedback . They are also separated by three dashes . It is similar to the service (request and response, that is, input and output) introduced earlier, but with more A feedback message item is also compiled after the action is defined, and the .action file will also be packaged as a message type.

2.1. .action definition

Let’s first look at the definition of action. The suffix is ​​.action . First go to the test package in the workspace and create a new action directory  .

cd ~/catkin_ws/src/test
mkdir action
cd action

Let's define a timer , Timer.action , to count down, and send out a signal when the countdown stops. In the process, we will be told the remaining time regularly, and after the count is over, the total duration will be returned.

gedit Timer.action

duration time_to_wait
---
duration time_elapsed
uint32 updates_sent
---
duration time_elapsed
duration time_remaining

Goal: The total waiting time sent by the client.
Result: The waiting time and update times sent by the server.
Feedback: Periodic feedback from the server, elapsed time and remaining time. 

Add execution permission: chmod u+x Timer.action

2.2. Modify CMakeLists.txt

After the action file is defined, the relevant dependencies for compilation also need to be written out.

cd ~/catkin_ws/src/test
gedit CMakeLists.txt

find_package adds actionlib_msgs

find_package(catkin REQUIRED COMPONENTS roscpp rospy std_msgs message_generation actionlib_msgs)

Inform catkin of the action files that need to be compiled

add_action_files(
  DIRECTORY action 
  FILES Timer.action
)

generate_messages explicitly lists actionlib_msgs message dependencies

generate_messages(DEPENDENCIES actionlib_msgs std_msgs)

Add actionlib_msgs dependency in catkin_package

catkin_package(CATKIN_DEPENDS message_runtime actionlib_msgs) 

2.3. Modify package.xml

cd ~/catkin_ws/src/test
gedit package.xml

Add actionlib_msgs

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

2.4. Compile

After the relevant preparations are completed, compile them and create the code and class definitions actually used by the action:

cd ~/catkin_ws
catkin_make

You can also specify the Python version for compilation:

cd ~/catkin_ws
catkin_make -DPYTHON_EXECUTABLE=/usr/bin/python2
cd ~/catkin_ws
catkin_make -DPYTHON_EXECUTABLE=/usr/bin/python3

Of course, if catkin_pkg is not installed in a certain version, an error will be reported:

ImportError: "from catkin_pkg.package import parse_package" failed: No module named 'catkin_pkg'
Make sure that you have installed "catkin_pkg", it is up to date and on the PYTHONPATH.

For example, I have not installed Python3 in it. Reinstall it as follows:

sudo apt install python3-pip
pip3 install --user catkin_pkg
pip3 install --user empy

Check it out, catkin_pkg is installed in both versions.

sudo find / -name "catkin_pkg"
/usr/lib/python2.7/dist-packages/catkin_pkg
/home/yahboom/.local/lib/python3.6/site-packages/catkin_pkg

After the compilation is successful, let's check which message files were generated:

ls ~/catkin_ws/devel/share/test/msg
TimerActionFeedback.msg  TimerAction.msg        TimerFeedback.msg  TimerResult.msg
TimerActionGoal.msg      TimerActionResult.msg  TimerGoal.msg

 These msg files contain the types defined by these actions. Let’s take a look at their details. The screenshots are as follows:

If you specify a Python version to compile, the corresponding .msg message file above will be generated into the corresponding class definition file under the corresponding version:

cd ~/catkin_ws/devel/lib/python2.7/dist-packages/test/msg
cd ~/catkin_ws/devel/lib/python3/dist-packages/test/msg

Use ls to see which files were generated ( _Complex.py is the message type customized in the previous topic chapter):

_Complex.py   __init__.py   _TimerActionFeedback.py  _TimerAction.py        _TimerFeedback.py  _TimerResult.py
_Complex.pyc  __init__.pyc  _TimerActionGoal.py      _TimerActionResult.py  _TimerGoal.py

Judging from these generated directories and files, it once again shows that the essence of this action is a topic, and the essence of the topic is a type of message flow.

3. Action server

The action-related classes have been generated. Next, we will write the action server. Actions, like topics and services, all use the callback mechanism , that is, the callback function will be awakened and called when a message is received. Here we directly use the SimpleActionServer class from actionlib to simplify the writing process. We only need to define the callback function when receiving the target, and the callback function will operate the timer according to the target and return a result after the operation is completed. In addition to the target and result, we will add feedback later. Let’s look at it first. A basic action service.

cd ~/catkin_ws/src/test/src
gedit action_server1.py
#!/usr/bin/env python
import rospy
import time
import actionlib
from test.msg import TimerGoal,TimerResult,TimerAction

def do_timer(goal):
    start_time = time.time()
    time.sleep(goal.time_to_wait.to_sec())

    result = TimerResult()
    result.time_elapsed = rospy.Duration.from_sec(time.time()-start_time)
    result.updates_sent = 0
    server.set_succeeded(result)

rospy.init_node('timer_action_server')
server = actionlib.SimpleActionServer('timer',TimerAction,do_timer,False)
server.start()
rospy.spin()

Add execution permissions: chmod u+x action_server1.py

First, import related classes and the classes automatically generated by Timer.action , and then define the callback function. The parameter goal is essentially an object of type TimerGoal . The time_to_wait object in goal returns the duration type, so it needs to be converted into to_sec() seconds, and vice versa. To get the duration, time_elapsed here is of Duration type, use from_sec() to convert. The last step is to use the result as a parameter and call set_succeeded() to tell SimpleActionServer that the target has been successfully executed. Let's take a look at the SimpleActionServer constructor:

__init__(self, name, ActionSpec, execute_cb=None, auto_start=True)

name: The name of the action server
ActionSpec: Action type
execute_cb: The callback function is called in a separate thread, receiving a new target, allowing the user to block the callback. Adding an execution callback will also invalidate the target callback.
auto_start: Tells ActionServer whether to start publishing immediately when it appears. This should always be set to False to prevent race conditions from occurring, causing some errors to occur, and then explicitly call start() to start the action server. So why does it not default to Fasle ? This may be because when the problem was discovered, a lot of code already relied on this default behavior, making modifications very troublesome.

4. Start checking

After the action server is written, let's check whether it is working properly.

First start: roscore
runs the action server: rosrun test action_server1.py
View the topic list: rostopic list

/rosout
/rosout_agg
/timer/cancel
/timer/feedback
/timer/goal
/timer/result
/timer/status

The topics under the timer namespace are correctly displayed . There are 5 topics in total. Let’s check a topic.

rostopic info /timer/goal
'''
Type: test/TimerActionGoal

Publishers: None

Subscribers: 
 * /timer_action_server (http://YAB:42723/)

'''

The type is TimerActionGoal , let’s check its information

rosmsg show TimerActionGoal
'''
[test/TimerActionGoal]:
std_msgs/Header header
  uint32 seq
  time stamp
  string frame_id
actionlib_msgs/GoalID goal_id
  time stamp
  string id
test/TimerGoal goal
  duration time_to_wait
'''

It is also verified here that the goal mentioned earlier is of type TimerGoal , which contains the time_to_wait field. The rest is used by the server and client to track the action execution status. However, before the target is passed into the callback function on the server side, this information is will be removed, and the last thing left is this TimerGoal message. Let’s check out TimerGoal :

rosmsg show TimerGoal
'''
[test/TimerGoal]:
duration time_to_wait
'''

Judging from these printed results, if you are using some libraries in the actionlib package, you do not need to access the types whose names contain Action . Simple Goal, Result, and Feedback are completely sufficient.

5. Action client

We have defined the server, then we define the client, send a target to the server, and wait for the result to arrive.

cd ~/catkin_ws/src/test/src
gedit action_client1.py
#!/usr/bin/env python
import rospy
import actionlib
from test.msg import TimerAction,TimerGoal,TimerResult

rospy.init_node('timer_action_client')
client = actionlib.SimpleActionClient('timer',TimerAction)
client.wait_for_server()
goal = TimerGoal()
goal.time_to_wait = rospy.Duration.from_sec(5)
client.send_goal(goal)
client.wait_for_result()
print(client.get_result().time_elapsed.to_sec())

Add executable permissions: chmod u+x action_client1.py

For the client code, after importing the relevant modules, the two parameters in the created SimpleActionClient are the same as those on the server side. Waiting for the action server to start is similar to the previous service rospy.wait_for_service , here it is rospy.wait_for_server . The next step is to create the goal, construct a TimerGoal object, fill in the time we want the timer to wait, here it is set to 5 seconds, and then send it to the server. The last step is to wait for the processing result of the server. If everything is normal, print out the duration of the obtained result get_result .

Run client

rosrun test action_client1.py
#5.005053997

The client sends 5 seconds to the target, and the server performs a sleep simulation. This blocking time is slightly longer than the request time, so it will be greater than 5 seconds.

6. Complex action server

After having a basic understanding of actions, we will focus on its characteristics, that is, the difference between actions and services , mainly the asynchronous characteristics of actions. Next, we will modify the above code to achieve functions such as termination goals, processing interrupt requests, and real-time feedback. , highlighting the advantages of action.

cd ~/catkin_ws/src/test/src
gedit action_server2.py
#!/usr/bin/env python
# -*- coding: utf-8

import rospy
import time
import actionlib
from test.msg import TimerAction,TimerGoal,TimerResult,TimerFeedback

def do_timer(goal):
    start_time = time.time()
    update_count = 0
    
    if goal.time_to_wait.to_sec() > 60:
        result = TimerResult()
        result.time_elapsed = rospy.Duration.from_sec(time.time()-start_time)
        result.updates_sent = update_count
        server.set_aborted(result,"超时终止")
        return
    while (time.time()-start_time) < goal.time_to_wait.to_sec():
        if server.is_preempt_requested():
            result = TimerResult()
            result.time_elapsed = rospy.Duration.from_sec(time.time()-start_time)
            result.updates_sent = update_count
            server.set_preempted(result,"中断")
            return

        feedback = TimerFeedback()
        feedback.time_elapsed = rospy.Duration.from_sec(time.time()-start_time)
        feedback.time_remaining = goal.time_to_wait - feedback.time_elapsed
        server.publish_feedback(feedback)
        update_count += 1
        time.sleep(1)

    result = TimerResult()
    result.time_elapsed = rospy.Duration.from_sec(time.time()-start_time)
    result.updates_sent = update_count
    server.set_succeeded(result,"成功完成")

rospy.init_node("timer_action_server2")
server = actionlib.SimpleActionServer('timer',TimerAction,do_timer,False)
server.start()
rospy.spin()

Add execution permissions: chmod u+x action_server2.py

Since feedback needs to be provided, the TimerFeedback message type is imported. For requests with time_to_wait exceeding 60 seconds, explicitly call set_aborted to terminate the current target request. This call will send a message to the client to inform the client that the target has been terminated. If it does not time out, it enters a loop and performs intermittent sleep waiting in the loop. During this process, it checks whether there is an interrupt such as is_preempt_requested and provides feedback. When the sleep duration exceeds the requested target duration, it exits the loop and tells the client that the target has been successfully executed. 

7. Complex action client

After the server has been adjusted, our client tests it, such as processing feedback, interrupting the executing target, and triggering termination:

cd ~/catkin_ws/src/test/src
gedit action_client2.py
#!/usr/bin/env python
# -*- coding: utf-8

import rospy
import time
import actionlib
from test.msg import TimerAction,TimerGoal,TimerResult,TimerFeedback

def feedback_cb(feedback):
    print("持续时间:%f"%(feedback.time_elapsed.to_sec()))
    print("剩余时间:%f"%(feedback.time_remaining.to_sec()))

rospy.init_node('timer_action_client2')
client = actionlib.SimpleActionClient('timer',TimerAction)
client.wait_for_server()

goal = TimerGoal()
goal.time_to_wait = rospy.Duration.from_sec(5)
#goal.time_to_wait = rospy.Duration.from_sec(100)
client.send_goal(goal,feedback_cb=feedback_cb)

time.sleep(3)
client.cancel_goal()

client.wait_for_result()
print("状态码:%d"%(client.get_state()))
print("状态描述:%s"%(client.get_goal_status_text()))
print("持续时间:%f"%(client.get_result().time_elapsed.to_sec()))
print("反馈次数:%d"%(client.get_result().updates_sent))

Add execution permissions: chmod u+x action_client2.py

View information about the feedback topic

rostopic info /timer/feedback

'''
Type: test/TimerActionFeedback

Publishers: 
 * /timer_action_server (http://YAB:33125/)

Subscribers: None
'''

 Its message type is TimerActionFeedback , let’s check the details

rosmsg show TimerActionFeedback
'''
[test/TimerActionFeedback]:
std_msgs/Header header
  uint32 seq
  time stamp
  string frame_id
actionlib_msgs/GoalStatus status
  uint8 PENDING=0
  uint8 ACTIVE=1
  uint8 PREEMPTED=2
  uint8 SUCCEEDED=3
  uint8 ABORTED=4
  uint8 REJECTED=5
  uint8 PREEMPTING=6
  uint8 RECALLING=7
  uint8 RECALLED=8
  uint8 LOST=9
  actionlib_msgs/GoalID goal_id
    time stamp
    string id
  uint8 status
  string text
test/TimerFeedback feedback
  duration time_elapsed
  duration time_remaining
'''

In addition to the two defined durations and remaining time, we also see that there are 10 possible current states, focusing on three of them: PREEMPTED =2, SUCCEEDED=3, ABORTED=4

After understanding some definitions of the client, let's run the test (provided that roscore and action_server2.py are enabled first ) 

 

7.1. Normal test

Run the client: rosrun test action_client2.py

Duration: 0.000023
Remaining time: 4.999977
Duration: 1.001364
Remaining time: 3.998636 Duration: 2.002950
Remaining time: 2.997050 Duration: 3.004577 Remaining time: 1.995423 Duration :
4.005102 Remaining time : 0.994898 Status code: 3 Status description: Successful completion Duration Time: 5.007248 Number of feedbacks: 5







7.2. Terminate the test

Adjust the time to more than 60 seconds, such as 100 seconds

goal.time_to_wait = rospy.Duration.from_sec(100)

Run the client: rosrun test action_client2.py

Status code: 4
Status description: Timeout terminated
Duration: 0.000014
Number of feedbacks: 0

7.3. Interruption test 

Remove the comments in the following code:

time.sleep(3)
client.cancel_goal()

Run the client: rosrun test action_client2.py

Duration: 0.000042
Remaining time: 4.999958
Duration: 1.002338
Remaining time: 3.997662 Duration
: 2.005545
Remaining time: 2.994455
Status code: 2
Status description: Interrupted
Duration: 3.007982
Number of feedbacks: 3

8. Summary

This completes the introduction of the three mechanisms of node communication. The action mechanism mentioned in this section is a powerful and widely used communication tool in ROS. So in which scenarios to choose which kind of communication, here is a comparison:

type Scenes
topic There are situations where there are multiple receivers, such as sensor data and simplex communication.
Serve Request/response interactive scenarios with short calculation time, such as asking for the current status of a node
action Most request/response interactive scenarios, especially when the execution process cannot be completed immediately, such as navigating to a target point

The main thing here is that services and actions are a bit ambiguous. So to understand it this way, at any time, when you perform a task and want to use a service, it is worth considering whether to use actions. Although actions require more code to be written, they are much more powerful and scalable than services. 

In addition, Chinese appears in the code, so you need to add: # -*- coding: utf-8

In this section, we have created two new nodes. After opening them, let’s check the node list.

rosnode list
'''
/rosout
/timer_action_server
/timer_action_server2
'''

 You can see that the two nodes defined in the code appear.

Guess you like

Origin blog.csdn.net/weixin_41896770/article/details/132666976