Trajectory blending with new trajectory deferral#2401
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #2401 +/- ##
==========================================
+ Coverage 86.70% 86.83% +0.13%
==========================================
Files 148 148
Lines 15858 16091 +233
Branches 1351 1361 +10
==========================================
+ Hits 13749 13973 +224
+ Misses 1613 1612 -1
- Partials 496 506 +10
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
christophfroehlich
left a comment
There was a problem hiding this comment.
Partial port of the ROS 1 "update existing trajectory" behavior to ROS 2 joint_trajectory_controller.
Fixes #84
For documentation here, what is different from the ROS 1 behavior/what is maybe missing to port?
Please also update the release_notes and update the documentation.
|
I'll review the logic and tests soon. I need time |
498c4ed to
e7db781
Compare
saikishor
left a comment
There was a problem hiding this comment.
The main logic looks good.
I still need to verify the tests though
| std::this_thread::sleep_for(std::chrono::milliseconds(150)); // well before 500 ms fire time | ||
| auto goal_handle_b = gh_b.get(); | ||
| ASSERT_TRUE(goal_handle_b); | ||
| action_client_->async_cancel_goal(goal_handle_b); | ||
|
|
||
| controller_hw_thread_.join(); | ||
|
|
||
| EXPECT_EQ(rclcpp_action::ResultCode::CANCELED, resultcode_b); |
There was a problem hiding this comment.
once the goal_b is sent you are not verifying the status of goal_a handle
There was a problem hiding this comment.
That had a separate test testing it, but I'll add it here as well.
| EXPECT_EQ(rclcpp_action::ResultCode::ABORTED, resultcode_a); | ||
| EXPECT_EQ(rclcpp_action::ResultCode::SUCCEEDED, resultcode_b); |
There was a problem hiding this comment.
I see that this is being triggered, but in this test shouldn't the A start immediately instead of future timestamp and then when B enter it is aborted. This is a nice test
Maybe it is interesting to get the time_now and verify it continuously during the cycle?
There was a problem hiding this comment.
Goal A already starts immediately (no header.stamp set). Goal B is the future-stamped one that gets deferred. So the behaviour is exactly as you described: A starts immediately, then when B is accepted, A gets preempted and ABORTED while B's trajectory is deferred until its stamp
There was a problem hiding this comment.
The final ABORTED/SUCCEEDED check already proves the essential behaviour: A was preempted when B was accepted, and B successfully executed after its deferred start time. Happy to add intermediate checks in a follow-up if you feel it's important for the coverage.
| /** | ||
| * @brief A stamp=0 topic trajectory preempts a deferred action goal at the trajectory level: | ||
| * the topic trajectory installs immediately and the deferred goal never fires. | ||
| * NOTE: the action goal handle is not formally canceled on topic preemption — that |
There was a problem hiding this comment.
Maybe it should cancel?
We shall discuss about that behaviour today
There was a problem hiding this comment.
This is a pre-existing issue in JTC, topic_callback has never explicitly aborted the active action goal on topic preemption, even before this PR. The goal handle survives and eventually gets ABORTED maybe via tolerance violation when the robot follows the topic trajectory instead.
should we abort it when a topic traj comes on top of action goal ?
If Yes we can open a separate PR resolving this.
| { | ||
| std::vector<JointTrajectoryPoint> points_init(1); | ||
| points_init[0].time_from_start = rclcpp::Duration::from_seconds(2.0); | ||
| points_init[0].positions = {4.0, 5.0, 6.0}; | ||
| control_msgs::action::FollowJointTrajectory_Goal goal_init; | ||
| goal_init.goal_time_tolerance = rclcpp::Duration::from_seconds(1.0); | ||
| goal_init.trajectory.joint_names = joint_names_; | ||
| goal_init.trajectory.points = points_init; | ||
| action_client_->async_send_goal(goal_init, goal_options_); | ||
| } | ||
| std::this_thread::sleep_for(std::chrono::milliseconds(200)); | ||
|
|
||
| { | ||
| std::vector<JointTrajectoryPoint> points_a(1); | ||
| points_a[0].time_from_start = rclcpp::Duration::from_seconds(0.5); | ||
| points_a[0].positions = {7.0, 8.0, 9.0}; | ||
| control_msgs::action::FollowJointTrajectory_Goal goal_a; | ||
| goal_a.goal_time_tolerance = rclcpp::Duration::from_seconds(1.0); | ||
| goal_a.trajectory.joint_names = joint_names_; | ||
| goal_a.trajectory.points = points_a; | ||
| goal_a.trajectory.header.stamp = | ||
| traj_controller_->get_node()->now() + rclcpp::Duration::from_seconds(1.0); | ||
| action_client_->async_send_goal(goal_a, goal_options_); |
There was a problem hiding this comment.
Any reasons for sending 2 goals for this test?
There was a problem hiding this comment.
goal_a creates the active trajectory that causes goal_b to be deferred. Without goal_a running, goal_b (future-stamped) would install immediately since has_active_trajectory() would be false.
| { | ||
| auto clock = rclcpp::Clock(RCL_ROS_TIME); | ||
| auto now_time = clock.now(); | ||
| auto last_time = now_time; | ||
| const auto end_time = now_time + rclcpp::Duration::from_seconds(0.3); | ||
| while (clock.now() < end_time) | ||
| { | ||
| now_time = clock.now(); | ||
| traj_controller_->update(now_time, now_time - last_time); | ||
| last_time = now_time; | ||
| } | ||
| traj_controller_->get_node()->deactivate(); | ||
| std::this_thread::sleep_for(std::chrono::milliseconds(200)); | ||
| }); | ||
| std::this_thread::sleep_for(std::chrono::milliseconds(100)); |
There was a problem hiding this comment.
This may end up flaky test. Increase the time and reduce the sleep?
There was a problem hiding this comment.
Okay will do that.
| { | ||
| std::vector<JointTrajectoryPoint> points_a(1); | ||
| points_a[0].time_from_start = rclcpp::Duration::from_seconds(1.0); | ||
| points_a[0].positions = {4.0, 5.0, 6.0}; | ||
| control_msgs::action::FollowJointTrajectory_Goal goal_a; | ||
| goal_a.goal_time_tolerance = rclcpp::Duration::from_seconds(1.0); | ||
| goal_a.trajectory.joint_names = joint_names_; | ||
| goal_a.trajectory.points = points_a; | ||
| action_client_->async_send_goal(goal_a, goal_options_); | ||
| } | ||
| std::this_thread::sleep_for(std::chrono::milliseconds(50)); | ||
|
|
||
| { | ||
| std::vector<JointTrajectoryPoint> points_b(1); | ||
| points_b[0].time_from_start = rclcpp::Duration::from_seconds(0.4); | ||
| points_b[0].positions = {7.0, 8.0, 9.0}; | ||
| control_msgs::action::FollowJointTrajectory_Goal goal_b; | ||
| goal_b.goal_time_tolerance = rclcpp::Duration::from_seconds(1.0); | ||
| goal_b.trajectory.joint_names = joint_names_; | ||
| goal_b.trajectory.points = points_b; | ||
| goal_b.trajectory.header.stamp = | ||
| traj_controller_->get_node()->now() + rclcpp::Duration::from_seconds(0.5); | ||
| action_client_->async_send_goal(goal_b, goal_options_); | ||
| } |
There was a problem hiding this comment.
Again why 2 goals here? I don't understand the reasoning behind it
There was a problem hiding this comment.
Same reason: goal_a creates the active trajectory that causes goal_b to be deferred. Without goal_a running, goal_b (future-stamped) would install immediately since has_active_trajectory() would be false.
| EXPECT_NEAR(4.55, state.positions[2], 0.25); | ||
| EXPECT_GT(state.positions[2], 4.1); // clearly above receive-time value ≈3.73 |
There was a problem hiding this comment.
Isn't this condition redundant?
There was a problem hiding this comment.
Okay right, it was just to check if the joint moved beyond that point. But agreed, it's redundant, I'll remove the EXPECT_GT.
|
@saikishor I have addressed your comments and also made requested changes. While going through I also realised that there is a helper API for sending goals. I've committed that as well. |
…blending-with-deferring-the-new-traj
Description
Partial port of the ROS 1 "update existing trajectory" behavior to ROS 2 joint_trajectory_controller.
Related to #84
What this PR does:
What this PR does NOT target: ( Compared to ROS1 blending feature )
Is this user-facing behavior change?
Yes. When allow_trajectory_blending is true (default), the active trajectory will continue running until the new trajectory's start time.
Did you use Generative AI?
Yes, Google Gemini and Claude were used to make improvements in manually written code, architectural analysis, edge-case review.
Additional Information
TODOs
To send us a pull request, please:
colcon testandpre-commit run(requires you to install pre-commit bypip3 install pre-commit)