Skip to content

Add motion_primitives_forward_controller for interfacing motion primitive messages with hardware interfaces #1636

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

Open
wants to merge 40 commits into
base: master
Choose a base branch
from

Conversation

mathias31415
Copy link

This PR adds the motion_primitives_forward_controller, a controller for forwarding motion primitive commands to motion-primitive-capable hardware interfaces.
It was developed alongside the motion_primitive_ur_driver and is intended to support future hardware interfaces for additional robot manufacturers.
The corresponding PR for integrating the motion_primitive_ur_driver into the official Universal Robots ROS 2 driver can be found here: UniversalRobots/Universal_Robots_ROS2_Driver#1341.

The controller subscribes to MotionPrimitive.msg from the industrial_robot_motion_interfaces package. The MotionPrimitive.msg has been extended with additional helper types:

  • STOP_MOTION: Immediately interrupts execution and clears all queued primitives.
  • MOTION_SEQUENCE_START and MOTION_SEQUENCE_END: Define a sequence of primitives to be executed as a blended motion block.

The controller uses the following interfaces:

Command Interfaces

  • motion_type: Type of motion primitive (e.g., LINEAR_JOINT, LINEAR_CARTESIAN, CIRCULAR_CARTESIAN, etc.)
  • q1q6: Target joint positions for joint-based motion
  • pos_x, pos_y, pos_z: Target Cartesian position
  • pos_qx, pos_qy, pos_qz, pos_qw: Orientation quaternion of the target pose
  • pos_via_x, pos_via_y, pos_via_z: Intermediate via-point position for circular motion
  • pos_via_qx, pos_via_qy, pos_via_qz, pos_via_qw: Orientation quaternion of via-point
  • blend_radius: Blending radius for smooth transitions
  • velocity: Desired motion velocity
  • acceleration: Desired motion acceleration
  • move_time: Optional duration for time-based execution

State Interfaces

  • execution_status: Indicates the current execution state of the primitive.
  • ready_for_new_primitive: Boolean flag indicating whether the interface is ready to receive a new motion primitive

I'd appreciate any feedback or suggestions – thanks in advance!

Copy link
Member

@bmagyar bmagyar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a lot of stuff coming from templates in this PR and you either used some AI tool for coding or you are very good at writing AI-style comments.

Please try removing all commented code or fix them and remove all comments that add nothing to the code itself (which in this code is almost all comments...)

@mathias31415 mathias31415 requested a review from bmagyar May 5, 2025 13:21
@mathias31415
Copy link
Author

Thanks @bmagyar for your review! I've addressed and resolved all the comments. Looking forward to any further feedback!

Copy link
Contributor

@fmauch fmauch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an incomplete early review. I just noticed that I needed to do this to get it built locally.

@mathias31415
Copy link
Author

It would also be great if the actions and messages from industrial_robot_motion_interfaces could be merged into the appropriate ROS 2 repositories if possible @fmauch / @urfeex :)

@mathias31415 mathias31415 requested a review from fmauch June 10, 2025 15:02
Copy link
Contributor

@fmauch fmauch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work so far!

In my opinion this lacks a bit of a threading concept as the RT thread must not wait under any circumstances which is not given by this implementation as far as I can see.

"$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>"
"$<INSTALL_INTERFACE:include/${PROJECT_NAME}>")
target_link_libraries(motion_primitives_forward_controller motion_primitives_forward_controller_parameters)
ament_target_dependencies(motion_primitives_forward_controller
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ament_target_dependencies is deprecated and was even temporarily removed from Rolling. See ament/ament_cmake#584 (comment) for details.


install(
DIRECTORY include/
DESTINATION include
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
DESTINATION include
DESTINATION include/${PROJECT_NAME}

See https://colcon.readthedocs.io/en/released/user/overriding-packages.html#install-headers-to-a-unique-include-directory

Comment on lines +70 to +72
target_include_directories(
test_load_motion_primitives_forward_controller PRIVATE include
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it make more sense to add motion_primitives_forward_controller to the target_link_libraries` instead of manually adding the include dir?

target_include_directories(
test_load_motion_primitives_forward_controller PRIVATE include
)
ament_target_dependencies(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

target_link_libraries

target_link_libraries(
test_motion_primitives_forward_controller motion_primitives_forward_controller
)
ament_target_dependencies(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

target_link_libraries

Comment on lines +478 to +488
// Check if queue has enough space
if (moprim_queue_.size() + required_queue_space > queue_size_)
{
RCLCPP_ERROR(
get_node()->get_logger(),
"Received more motion primitives than queue can store. "
"Current queue size: %zu, required space: %zu. "
"Please increase the queue size in the controller parameters.",
moprim_queue_.size(), required_queue_space);
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned above, I don't quite see the necessity of restricting this.

Comment on lines +499 to +502
for (const auto & primitive : primitives)
{
moprim_queue_.push(std::make_shared<MotionPrimitiveMsg>(primitive));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could wrap that into a lambda and then only have one conditional block:

auto add_motions = [this](primitives){
  for (const auto & primitive : primitives)
  {
    moprim_queue_.push(std::make_shared<MotionPrimitiveMsg>(primitive));
  }
};

if (primitives.size() > 1)
{
  std::shared_ptr<MotionPrimitiveMsg> start_marker = std::make_shared<MotionPrimitiveMsg>();
  start_marker->type = MotionType::MOTION_SEQUENCE_START;
  moprim_queue_.push(start_marker);

  add_motions(primitives);

  std::shared_ptr<MotionPrimitiveMsg> end_marker = std::make_shared<MotionPrimitiveMsg>();
  end_marker->type = MotionType::MOTION_SEQUENCE_END;
  moprim_queue_.push(end_marker);
}
else
{
  add_motions(primitives);
}

rclcpp::Rate rate(50);
while (rclcpp::ok())
{
auto execution_status = static_cast<uint8_t>(std::round(state_interfaces_[0].get_value()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are basically accessing a shared resource (state_interfaces_ gets set inside the control thread) without thread synchronization. One more point against using the execute thread.

}
uint8_t ready_for_new_primitive = static_cast<int8_t>(std::round(opt_value_ready.value()));

if (!moprim_queue_.empty()) // check if new command is available
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Access to shared resource without thread synchronization. I am not sure whether empty() is a thread-safe call, but I wouldn't expect it.

}
case ReadyForNewPrimitive::READY:
{
if (!set_command_interfaces())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where you potentially block the control thread as this will wait for aquiring the lock for accessing the queue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants