-
Notifications
You must be signed in to change notification settings - Fork 958
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft: Simultaneous execution of trajectories #2810
Conversation
Thanks for helping in improving MoveIt and open source robotics! |
Fixes #2287 @JafarAbdi @henningkayser @tylerjw This is the project I mentioned in the call yesterday and the maintainer meeting. Some issues remain, but we have been running this for a few weeks and it should be ready for testing and feedback from a fresh set of eyes. There are infrequent crashes, probably related to thread safety in Some more notes:
Since the video above does not do this PR justice (slow PlanningScene update rate and ugly CollisionObjects 💩), I want to add some more to drive home the point how sweet and deserving of enthusiastic reviews this is:
simultaneous-rearrange.mp4
bearing-align.mp4
shaft-insert-3.mp4Anyway, I hope we can get the kinks ironed out soon. I think this PR is in everyone's interest. Thanks! PS: I originally thought that the collision check between two trajectories could be done more efficiently with gpu-voxels, so we wasted a bit of time on trying that. That was absolutely premature optimization though, the performance as is seems perfectly fine. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cambel @felixvd Whoa, this is a lot. Great work so far! I really like the general idea of having an ongoing loop in the TEM that keeps track of any active trajectories and controllers. By validating incoming trajectories with all active instances in the beginning it should be possible to ensure we are at least commanding collision-free trajectories (fingers crossed that the controllers are actually doing what we command ;)). I haven't fully understood all of the new logic yet, I'll follow up with more in-depth and structural feedback. I also still need to make up my mind about how concurrent actions could be handled in general.
So far, my biggest concern is that collision checking between trajectories is inherently unsafe if we do it the way it's implemented right now. Self collisions have reduced padding by default and the resolution of trajectory waypoints is usually not sufficient to really guarantee that different groups don't run into each other.
As already mentioned in one of the comments, I think that we need to:
- Increase padding for self collisions (= collisions between different groups)
- Ensure a minimal joint distance resolution similar to
longest_valid_fragment_section
- Use a "continuous" collision check that validates the full segment between two consecutive waypoints. I'm not sure if this is currently even supported for self-collisions, but I imagine that this would be the most secure and efficient solution long-term. This could make the minimal joint distance threshold redundant.
Using a very big padding with a reasonable trajectory resolution should be fine for most use cases, but you could still not really rule out collisions for all cases as small joint state jumps can produce larger Cartesian distances.
Do you have a testing setup that you could share for testing?
@@ -231,6 +231,7 @@ void RobotTrajectory::getRobotTrajectoryMsg(moveit_msgs::RobotTrajectory& trajec | |||
|
|||
std::vector<const moveit::core::JointModel*> onedof; | |||
std::vector<const moveit::core::JointModel*> mdof; | |||
trajectory.group_name = group_ ? group_->getName() : ""; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you open a PR to add group_name
to the message?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
moveit/moveit_msgs#133 for reference
root_node_handle_, "sequence_move_group", | ||
boost::bind(&MoveGroupSequenceAction::executeSequenceCallback, this, _1), false); | ||
move_action_server_->registerPreemptCallback(boost::bind(&MoveGroupSequenceAction::preemptMoveCallback, this)); | ||
// move_action_server_->registerPreemptCallback(boost::bind(&MoveGroupSequenceAction::preemptMoveCallback, this)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use registerCancelCallback()
?
move_state_ = state; | ||
move_feedback_.state = stateToStr(state); | ||
move_action_server_->publishFeedback(move_feedback_); | ||
// move_state_ = state; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We probably need to keep track of states for each goal separately and pass the goal handle into this function for publishing feedback
@@ -60,6 +60,8 @@ class BaseFakeController : public moveit_controller_manager::MoveItControllerHan | |||
protected: | |||
std::vector<std::string> joints_; | |||
const ros::Publisher& pub_; | |||
moveit_controller_manager::ExecutionStatus status_; | |||
std::string name_ = "fake_controllers"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better set the default with the constructor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, if this is only used for the logger names, I'd prefer adding a constexpr char LOGNAME[]
to the source file instead of a member. See code style guidelines
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay
else | ||
move_action_server_->setAborted(action_res, response); | ||
} | ||
goal_handle.setAborted(action_res, response); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also here, setCanceled()
could replace setPreempted()
TrajectoryExecutionContext* priority_context = it2->first; // Previous context in the backlog (earlier ones have priority) | ||
ROS_DEBUG_STREAM_NAMED(name_, "Backlog comparing item with duration: " << current_context->trajectory_parts_[0].joint_trajectory.points.back().time_from_start); | ||
ROS_DEBUG_STREAM_NAMED(name_, "vs item with duration: " << priority_context->trajectory_parts_[0].joint_trajectory.points.back().time_from_start); | ||
if (hasCommonHandles(*current_context, *priority_context)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this really a useful debug comment like this? It would help if the blocked resources (group, controller) were referenced here.
} | ||
if(controllers_not_used_earlier_in_backlog) | ||
{ | ||
ROS_DEBUG_STREAM_NAMED(name_, "Backlog item with duration " << current_context->trajectory_parts_[0].joint_trajectory.points.back().time_from_start << " will be checked and pushed to controller."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
time_from_start is used a couple times in this block.
Using const auto& time_from_start = current_context->trajectory_parts_[0].joint_trajectory.points.back().time_from_start
would make this function more readable imo
std::deque<std::pair<TrajectoryExecutionContext*, ros::Time>> backlog; | ||
int expiration_time = 60; // seconds (after this time, the trajectory is discarded) TODO (cambel): Make this less surprising | ||
|
||
ros::Rate r(10); | ||
while (run_continuous_execution_thread_) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This loop seems a little too convoluted to me. I'll take some more time to review this more in depth and see if this could be simplified somehow.
if (jointTrajPointToRobotState(active_trajectory.joint_trajectory, i, start_state)) | ||
{ | ||
robotStateToRobotStateMsg(start_state, start_state_msg); | ||
if(!ps->isPathValid(start_state_msg, new_trajectory, new_trajectory.group_name, true)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is pretty risky. isPathValid()
doesn't do any interpolation between the waypoints and for the most part the resolution of the planned and parameterized trajectories is just not high enough to guarantee validity. I think we must consider the following three things:
- Increase padding for self collisions (= collisions between different groups)
- Ensure a minimal joint distance resolution similar to longest_valid_fragment_section
- Use a "continuous" collision check that validates the full segment between two consecutive waypoints. I'm not sure if this is currently even supported for self-collisions, but I imagine that this would be the most secure and efficient solution long-term.
{ | ||
moveit_msgs::RobotState robot_state_msg; | ||
robotStateToRobotStateMsg(*current_state, robot_state_msg); | ||
if(!ps->isPathValid(robot_state_msg, trajectory, trajectory.group_name)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same story here
Just a thought, it might make sense to split this PR into two parts:
|
1ea23fc
to
067dded
Compare
@henningkayser Thanks for the comments. I will slowly check them and update the PR. I created a simple environment to test this PR here using the Panda dual-arm env. |
You can now also test this PR using the repository we originally wrote it for. |
I had a quick look at the current state of this PR as well and I think we should factor out at least two additional PRs:
Then we need to interpolate the current trajectory and check each point with the new (repeating myself 😉 , but please check out pre-commit https://moveit.ros.org/documentation/contributing/code/ ) |
- Trajectories that failed the validation right before execution, typically because the initial state is not the same as the robot's current state
validate() was being called for each trajectory controller, creating a delay between controllers
067dded
to
8958ef8
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi, interesting PR. I have the following questions in mind:
- The goal of this PR seems to be the non-unary manipulation systems, which are represented through a single robot model possibly with different groups. Is this correct?
- Assuming the answer to 1 is "yes", how is this method better than solving the coupled planning problem - i.e both arms(or all active joints)- at once?
// This is called when the trajectory finishes. | ||
bool execution_complete = false; | ||
auto goalId = goal.getGoalID(); | ||
// Copy goalId here so that it is still available when this lambda is called later (otherwise it is deleted when the parent function ends) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case, there shouldn't be any by-reference captures because they would suffer due to the same scoping problem, so you shouldn't need &
before goalId
👀
// This is called when the trajectory finishes. | ||
auto goalId = goal.getGoalID(); | ||
// Copy goalId here so that it is still available when this lambda is called later (otherwise it is deleted when the parent function ends) | ||
auto completedTrajectoryCallback = [&, goalId](const moveit_controller_manager::ExecutionStatus& status) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be a member function. It is too large for a lambda function
action_res.error_code.val = moveit_msgs::MoveItErrorCodes::PREEMPTED; | ||
return; | ||
} | ||
// TODO(cambel): add a way to cancel a planning request |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this needed? Are you going to also provide the simultaneous planning capability?
moveit_msgs::RobotTrajectory trajectory_; | ||
|
||
ExecutionCompleteCallback execution_complete_callback; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit -> docs
720b66a
to
04cec9b
Compare
Hi @tahsinkose,
|
@felixvd and I have been working on this PR
Description
This draft PR proposes a method to allow the execution of multiple controllers in several independent trajectory plans at the same time.
So far, MoveIt only supports the execution of one trajectory at a time. If a second trajectory is sent for execution, the first trajectory is aborted and the second one is attempted instead.
There are two main reasons for this limitation. First, all the action servers are of the type SimpleActionServer which can only serve one goal at a time and immediately reject any old goal as a new one is received. Second, the TrajectoryExecutionManager only supports the execution of one trajectory at a time by default, aborting any previous trajectory as a new one is pushed.
The first issue is addressed by converting the SimpleActionServers into (General) ActionServers. This allows the server to handle multiple goals at the same time.
The second issue is addressed by implementing a scheduling procedure to execute trajectories simultaneously as long as the following conditions are met:
If the trajectory does not meet the conditions mentioned above, the trajectory is stored in a backlog and periodically checked. The trajectories in the backlog and newly received trajectories are evaluated and executed in a strict FIFO order.
Our use case: Simultaneous control of robots with different sets of instructions that do not have/need to be synchronized in any particular way. See this simple equip/unequip routine where both robots can follow their independent set of instructions without waiting for the other one.
equip.unequip.tool.mp4
There are some issues pending to be fixed:
MoveGroupResult
) is corrupted because something cannot be accessedCannot access memory at address
.execute_trajectory_service
also needs to be changed over to simultaneous executionComments and contributions are highly appreciated!
Checklist