Skip to content

Commit 584197a

Browse files
authored
Let Task::preempt() cancel execute() (moveit#684)
1 parent bab7b04 commit 584197a

File tree

7 files changed

+255
-18
lines changed

7 files changed

+255
-18
lines changed

capabilities/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ install(TARGETS ${PROJECT_NAME}
3434
LIBRARY DESTINATION lib
3535
)
3636

37+
add_subdirectory(test)
38+
3739
pluginlib_export_plugin_description_file(moveit_ros_move_group capabilities_plugin_description.xml)
3840
ament_export_dependencies(moveit_core)
3941
ament_export_dependencies(moveit_ros_move_group)

capabilities/package.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@
2222
<depend>rclcpp_action</depend>
2323
<depend>std_msgs</depend>
2424

25+
<test_depend>ament_cmake_gtest</test_depend>
26+
<test_depend>moveit_configs_utils</test_depend>
27+
<test_depend>launch_testing</test_depend>
28+
<test_depend>launch_testing_ament_cmake</test_depend>
29+
<test_depend>moveit_resources_panda_moveit_config</test_depend>
30+
2531
<export>
2632
<build_type>ament_cmake</build_type>
2733
</export>

capabilities/test/CMakeLists.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
#############
2+
## Testing ##
3+
#############
4+
5+
## Add gtest based cpp test target
6+
if (BUILD_TESTING AND NOT (CMAKE_CXX_FLAGS MATCHES "-fsanitize"))
7+
find_package(ament_cmake_gtest REQUIRED)
8+
find_package(launch_testing_ament_cmake REQUIRED)
9+
find_package(moveit_task_constructor_core REQUIRED)
10+
11+
ament_add_gtest_executable(test_execution test_task_execution.cpp)
12+
add_launch_test(test_execution.launch.py TARGET test_execution
13+
ARGS "test_binary:=$<TARGET_FILE:test_execution>")
14+
ament_target_dependencies(test_execution moveit_task_constructor_core)
15+
endif()
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import unittest
2+
import os
3+
4+
from ament_index_python.packages import get_package_share_directory
5+
import launch_testing
6+
import pytest
7+
from launch import LaunchDescription
8+
from launch.actions import DeclareLaunchArgument
9+
from launch.substitutions import LaunchConfiguration
10+
from launch_ros.actions import Node
11+
from launch_testing.actions import ReadyToTest
12+
from launch_testing.util import KeepAliveProc
13+
from moveit_configs_utils import MoveItConfigsBuilder
14+
15+
16+
@pytest.mark.launch_test
17+
def generate_test_description():
18+
moveit_config = (
19+
MoveItConfigsBuilder("moveit_resources_panda")
20+
.planning_pipelines(pipelines=["ompl"])
21+
.robot_description(file_path="config/panda.urdf.xacro")
22+
.to_moveit_configs()
23+
)
24+
25+
# Load ExecuteTaskSolutionCapability so we can test executing solutions
26+
move_group_capabilities = {"capabilities": "move_group/ExecuteTaskSolutionCapability"}
27+
28+
# Start the actual move_group node/action server
29+
move_group_node = Node(
30+
package="moveit_ros_move_group",
31+
executable="move_group",
32+
output="screen",
33+
parameters=[
34+
moveit_config.to_dict(),
35+
move_group_capabilities,
36+
],
37+
)
38+
39+
# Static TF
40+
static_tf = Node(
41+
package="tf2_ros",
42+
executable="static_transform_publisher",
43+
name="static_transform_publisher",
44+
output="log",
45+
arguments=["0.0", "0.0", "0.0", "0.0", "0.0", "0.0", "world", "panda_link0"],
46+
)
47+
48+
# Publish joint states to TF
49+
robot_state_publisher = Node(
50+
package="robot_state_publisher",
51+
executable="robot_state_publisher",
52+
name="robot_state_publisher",
53+
output="both",
54+
parameters=[moveit_config.robot_description],
55+
)
56+
57+
# ros2_control using FakeSystem as hardware
58+
ros2_controllers_path = os.path.join(
59+
get_package_share_directory("moveit_resources_panda_moveit_config"),
60+
"config",
61+
"ros2_controllers.yaml",
62+
)
63+
ros2_control_node = Node(
64+
package="controller_manager",
65+
executable="ros2_control_node",
66+
parameters=[moveit_config.robot_description, ros2_controllers_path],
67+
output="both",
68+
)
69+
70+
controller_spawner = Node(
71+
package="controller_manager",
72+
executable="spawner",
73+
arguments=[
74+
"panda_arm_controller",
75+
"joint_state_broadcaster",
76+
"--controller-manager",
77+
"/controller_manager",
78+
],
79+
)
80+
81+
test_exec = Node(
82+
executable=[
83+
LaunchConfiguration("test_binary"),
84+
],
85+
output="screen",
86+
parameters=[
87+
moveit_config.robot_description,
88+
moveit_config.robot_description_semantic,
89+
moveit_config.robot_description_kinematics,
90+
moveit_config.joint_limits,
91+
],
92+
)
93+
94+
return (
95+
LaunchDescription(
96+
[
97+
static_tf,
98+
robot_state_publisher,
99+
move_group_node,
100+
ros2_control_node,
101+
controller_spawner,
102+
DeclareLaunchArgument(
103+
name="test_binary",
104+
description="Test executable",
105+
),
106+
test_exec,
107+
KeepAliveProc(),
108+
ReadyToTest(),
109+
]
110+
),
111+
{"test_exec": test_exec},
112+
)
113+
114+
115+
class TestTerminatingProcessStops(unittest.TestCase):
116+
def test_gtest_run_complete(self, proc_info, test_exec):
117+
proc_info.assertWaitForShutdown(process=test_exec, timeout=4000.0)
118+
119+
120+
@launch_testing.post_shutdown_test()
121+
class TaskModelTestAfterShutdown(unittest.TestCase):
122+
def test_exit_code(self, proc_info):
123+
# Check that all processes in the launch exit with code 0
124+
launch_testing.asserts.assertExitCodes(proc_info)
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#include <moveit/task_constructor/task.h>
2+
#include <moveit/task_constructor/stages/current_state.h>
3+
#include <moveit/task_constructor/stages/move_to.h>
4+
#include <moveit/task_constructor/solvers/joint_interpolation.h>
5+
#include <moveit_task_constructor_msgs/msg/solution.hpp>
6+
#include <moveit/robot_model/robot_model.hpp>
7+
8+
#include <gtest/gtest.h>
9+
10+
using namespace moveit::task_constructor;
11+
12+
// provide a basic test fixture that prepares a Task
13+
struct PandaMoveTo : public testing::Test
14+
{
15+
Task t;
16+
stages::MoveTo* move_to;
17+
rclcpp::Node::SharedPtr node;
18+
19+
PandaMoveTo() {
20+
node = rclcpp::Node::make_shared("test_task_execution");
21+
t.loadRobotModel(node);
22+
23+
auto group = t.getRobotModel()->getJointModelGroup("panda_arm");
24+
25+
auto initial = std::make_unique<stages::CurrentState>("current state");
26+
t.add(std::move(initial));
27+
28+
auto move = std::make_unique<stages::MoveTo>("move", std::make_shared<solvers::JointInterpolationPlanner>());
29+
move_to = move.get();
30+
move_to->setGroup(group->getName());
31+
t.add(std::move(move));
32+
}
33+
};
34+
35+
// The arm starts in the "ready" pose so make sure we can move to a different known location
36+
TEST_F(PandaMoveTo, successExecution) {
37+
move_to->setGoal("extended");
38+
ASSERT_TRUE(t.plan());
39+
auto result = t.execute(*t.solutions().front());
40+
EXPECT_EQ(result.val, moveit_msgs::msg::MoveItErrorCodes::SUCCESS);
41+
}
42+
43+
// After the arm successfully moved to "extended", move back to "ready" and make sure preempt() works as expected
44+
TEST_F(PandaMoveTo, preemptExecution) {
45+
move_to->setGoal("ready");
46+
ASSERT_TRUE(t.plan());
47+
// extract the expected execution time (for this task its in the last sub_trajectory)
48+
moveit_task_constructor_msgs::msg::Solution s;
49+
t.solutions().front()->toMsg(s, nullptr);
50+
rclcpp::Duration exec_duration{ s.sub_trajectory.back().trajectory.joint_trajectory.points.back().time_from_start };
51+
52+
moveit::core::MoveItErrorCode result;
53+
std::thread execute_thread{ [this, &result]() { result = t.execute(*t.solutions().front()); } };
54+
55+
// cancel the trajectory half way through the expected execution time
56+
rclcpp::sleep_for(exec_duration.to_chrono<std::chrono::milliseconds>() / 2);
57+
t.preempt();
58+
if (execute_thread.joinable()) {
59+
execute_thread.join();
60+
}
61+
62+
EXPECT_EQ(result.val, moveit_msgs::msg::MoveItErrorCodes::PREEMPTED);
63+
}
64+
65+
int main(int argc, char** argv) {
66+
testing::InitGoogleTest(&argc, argv);
67+
rclcpp::init(argc, argv);
68+
69+
// wait some time for move_group to come up
70+
rclcpp::sleep_for(std::chrono::seconds(5));
71+
72+
return RUN_ALL_TESTS();
73+
}

core/include/moveit/task_constructor/task.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,15 @@
4343

4444
#include <moveit/task_constructor/introspection.h>
4545
#include <moveit_task_constructor_msgs/msg/solution.hpp>
46+
#include <moveit_task_constructor_msgs/action/execute_task_solution.hpp>
4647

4748
#include <moveit/macros/class_forward.hpp>
4849

4950
#include <moveit_msgs/msg/move_it_error_codes.hpp>
5051
#include <moveit/utils/moveit_error_code.hpp>
5152

5253
#include <rclcpp/node.hpp>
54+
#include <rclcpp_action/rclcpp_action.hpp>
5355

5456
namespace moveit {
5557
namespace core {
@@ -167,6 +169,9 @@ class Task : protected WrapperBase
167169

168170
private:
169171
using WrapperBase::init;
172+
// persistent node and client to call the ExecuteTaskSolution action and is only created if execute() is called
173+
rclcpp::Node::SharedPtr execute_solution_node_;
174+
std::shared_ptr<rclcpp_action::Client<moveit_task_constructor_msgs::action::ExecuteTaskSolution>> execute_ac_;
170175
};
171176

172177
inline std::ostream& operator<<(std::ostream& os, const Task& task) {

core/src/task.cpp

Lines changed: 30 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,8 @@
3838
#include <moveit/task_constructor/container_p.h>
3939
#include <moveit/task_constructor/task_p.h>
4040
#include <moveit/task_constructor/introspection.h>
41-
#include <moveit_task_constructor_msgs/action/execute_task_solution.hpp>
4241

4342
#include <rclcpp/rclcpp.hpp>
44-
#include <rclcpp_action/rclcpp_action.hpp>
4543

4644
#include <moveit/robot_model_loader/robot_model_loader.hpp>
4745
#include <moveit/planning_pipeline/planning_pipeline.hpp>
@@ -283,13 +281,16 @@ void Task::resetPreemptRequest() {
283281
}
284282

285283
moveit::core::MoveItErrorCode Task::execute(const SolutionBase& s) {
286-
// Add random ID to prevent warnings about multiple publishers within the same node
287-
auto node = rclcpp::Node::make_shared("moveit_task_constructor_executor_" +
288-
std::to_string(reinterpret_cast<std::size_t>(this)));
289-
auto ac = rclcpp_action::create_client<moveit_task_constructor_msgs::action::ExecuteTaskSolution>(
290-
node, "execute_task_solution");
291-
if (!ac->wait_for_action_server(0.5s)) {
292-
RCLCPP_ERROR(node->get_logger(), "Failed to connect to the 'execute_task_solution' action server");
284+
// If this is the first call to execute create a persistent node that can be used to call the action server
285+
if (!execute_solution_node_) {
286+
execute_solution_node_ = rclcpp::Node::make_shared("moveit_task_constructor_executor_" +
287+
std::to_string(reinterpret_cast<std::size_t>(this)));
288+
execute_ac_ = rclcpp_action::create_client<moveit_task_constructor_msgs::action::ExecuteTaskSolution>(
289+
execute_solution_node_, "execute_task_solution");
290+
}
291+
if (!execute_ac_->wait_for_action_server(0.5s)) {
292+
RCLCPP_ERROR(execute_solution_node_->get_logger(),
293+
"Failed to connect to the 'execute_task_solution' action server");
293294
return moveit::core::MoveItErrorCode::FAILURE;
294295
}
295296

@@ -298,27 +299,38 @@ moveit::core::MoveItErrorCode Task::execute(const SolutionBase& s) {
298299

299300
moveit_msgs::msg::MoveItErrorCodes error_code;
300301
error_code.val = moveit_msgs::msg::MoveItErrorCodes::FAILURE;
301-
auto goal_handle_future = ac->async_send_goal(goal);
302-
if (rclcpp::spin_until_future_complete(node, goal_handle_future) != rclcpp::FutureReturnCode::SUCCESS) {
303-
RCLCPP_ERROR(node->get_logger(), "Send goal call failed");
302+
auto goal_handle_future = execute_ac_->async_send_goal(goal);
303+
if (rclcpp::spin_until_future_complete(execute_solution_node_, goal_handle_future) !=
304+
rclcpp::FutureReturnCode::SUCCESS) {
305+
RCLCPP_ERROR(execute_solution_node_->get_logger(), "Send goal call failed");
304306
return error_code;
305307
}
306308

307309
const auto& goal_handle = goal_handle_future.get();
308310
if (!goal_handle) {
309-
RCLCPP_ERROR(node->get_logger(), "Goal was rejected by server");
311+
RCLCPP_ERROR(execute_solution_node_->get_logger(), "Goal was rejected by server");
310312
return error_code;
311313
}
312314

313-
auto result_future = ac->async_get_result(goal_handle);
314-
if (rclcpp::spin_until_future_complete(node, result_future) != rclcpp::FutureReturnCode::SUCCESS) {
315-
RCLCPP_ERROR(node->get_logger(), "Get result call failed");
316-
return error_code;
315+
auto result_future = execute_ac_->async_get_result(goal_handle);
316+
while (result_future.wait_for(std::chrono::milliseconds(10)) != std::future_status::ready) {
317+
if (pimpl()->preempt_requested_) {
318+
auto cancel_future = execute_ac_->async_cancel_goal(goal_handle);
319+
if (rclcpp::spin_until_future_complete(execute_solution_node_, cancel_future) !=
320+
rclcpp::FutureReturnCode::SUCCESS) {
321+
RCLCPP_ERROR(execute_solution_node_->get_logger(), "Could not preempt execution");
322+
return error_code;
323+
} else {
324+
error_code.val = moveit_msgs::msg::MoveItErrorCodes::PREEMPTED;
325+
return error_code;
326+
}
327+
}
328+
rclcpp::spin_some(execute_solution_node_);
317329
}
318330

319331
auto result = result_future.get();
320332
if (result.code != rclcpp_action::ResultCode::SUCCEEDED) {
321-
RCLCPP_ERROR(node->get_logger(), "Goal was aborted or canceled");
333+
RCLCPP_ERROR(execute_solution_node_->get_logger(), "Goal was aborted or canceled");
322334
return error_code;
323335
}
324336

0 commit comments

Comments
 (0)