Skip to content

Commit a56457c

Browse files
committed
Create a utility node that transforms input Wrench for a given list of frames
1 parent 61b3bc6 commit a56457c

File tree

9 files changed

+653
-1
lines changed

9 files changed

+653
-1
lines changed

force_torque_sensor_broadcaster/CHANGELOG.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ Changelog for package force_torque_sensor_broadcaster
66
------------------
77
* Controller interface api update to ros2_controller packages (`#1973 <https://github.com/ros-controls/ros2_controllers/issues/1973>`_)
88
* Don't use `msg\_` field of realtime publisher (`#1947 <https://github.com/ros-controls/ros2_controllers/issues/1947>`_)
9-
* Contributors: Anand Vardhan, Christoph Fröhlich
9+
* Add wrench transformer node for transforming wrench messages to different frames (`#1842 <https://github.com/ros-controls/ros2_controllers/issues/1842>`_)
10+
* Contributors: Anand Vardhan, Christoph Fröhlich, Julia Jia
1011

1112
5.8.0 (2025-10-02)
1213
------------------

force_torque_sensor_broadcaster/CMakeLists.txt

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ set(THIS_PACKAGE_INCLUDE_DEPENDS
1515
rclcpp
1616
rclcpp_lifecycle
1717
realtime_tools
18+
tf2
19+
tf2_ros
20+
tf2_geometry_msgs
1821
)
1922

2023
find_package(ament_cmake REQUIRED)
@@ -27,6 +30,10 @@ generate_parameter_library(force_torque_sensor_broadcaster_parameters
2730
src/force_torque_sensor_broadcaster_parameters.yaml
2831
)
2932

33+
generate_parameter_library(wrench_transformer_parameters
34+
src/wrench_transformer_parameters.yaml
35+
)
36+
3037
add_library(force_torque_sensor_broadcaster SHARED
3138
src/force_torque_sensor_broadcaster.cpp
3239
)
@@ -49,6 +56,37 @@ target_link_libraries(force_torque_sensor_broadcaster PUBLIC
4956
pluginlib_export_plugin_description_file(
5057
controller_interface force_torque_sensor_broadcaster.xml)
5158

59+
# Wrench transformer library
60+
add_library(wrench_transformer SHARED
61+
src/wrench_transformer.cpp
62+
)
63+
target_compile_features(wrench_transformer PUBLIC cxx_std_17)
64+
target_include_directories(wrench_transformer PUBLIC
65+
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
66+
$<INSTALL_INTERFACE:include/force_torque_sensor_broadcaster>
67+
)
68+
target_link_libraries(wrench_transformer PUBLIC
69+
wrench_transformer_parameters
70+
rclcpp::rclcpp
71+
tf2::tf2
72+
tf2_ros::tf2_ros
73+
tf2_geometry_msgs::tf2_geometry_msgs
74+
${geometry_msgs_TARGETS}
75+
)
76+
77+
# Wrench transformer executable
78+
add_executable(wrench_transformer_node
79+
src/wrench_transformer_main.cpp
80+
)
81+
target_compile_features(wrench_transformer_node PUBLIC cxx_std_17)
82+
target_include_directories(wrench_transformer_node PUBLIC
83+
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
84+
$<INSTALL_INTERFACE:include/force_torque_sensor_broadcaster>
85+
)
86+
target_link_libraries(wrench_transformer_node PUBLIC
87+
wrench_transformer
88+
)
89+
5290
if(BUILD_TESTING)
5391
find_package(ament_cmake_gmock REQUIRED)
5492
find_package(controller_manager REQUIRED)
@@ -73,6 +111,15 @@ if(BUILD_TESTING)
73111
force_torque_sensor_broadcaster
74112
)
75113

114+
ament_add_gmock(test_wrench_transformer test/test_wrench_transformer.cpp)
115+
target_include_directories(test_wrench_transformer PRIVATE include)
116+
target_compile_definitions(test_wrench_transformer PRIVATE
117+
TEST_FILES_DIRECTORY="${CMAKE_CURRENT_SOURCE_DIR}/test")
118+
target_link_libraries(test_wrench_transformer
119+
wrench_transformer
120+
tf2_ros::tf2_ros
121+
)
122+
76123
add_library(dummy_filter SHARED
77124
test/dummy_filter.cpp
78125
)
@@ -107,12 +154,22 @@ install(
107154
TARGETS
108155
force_torque_sensor_broadcaster
109156
force_torque_sensor_broadcaster_parameters
157+
wrench_transformer
158+
wrench_transformer_parameters
110159
EXPORT export_force_torque_sensor_broadcaster
111160
RUNTIME DESTINATION bin
112161
ARCHIVE DESTINATION lib
113162
LIBRARY DESTINATION lib
114163
)
115164

165+
# Install executable to lib/<package_name> for launch files to find it
166+
install(
167+
TARGETS
168+
wrench_transformer_node
169+
EXPORT export_force_torque_sensor_broadcaster
170+
RUNTIME DESTINATION lib/force_torque_sensor_broadcaster
171+
)
172+
116173
ament_export_targets(export_force_torque_sensor_broadcaster HAS_LIBRARY_TARGET)
117174
ament_export_dependencies(${THIS_PACKAGE_INCLUDE_DEPENDS})
118175
ament_package()

force_torque_sensor_broadcaster/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,9 @@ Controller to publish state of force-torque sensors.
66
Pluginlib-Library: force_torque_sensor_broadcaster
77

88
Plugin: force_torque_sensor_broadcaster/ForceTorqueSensorBroadcaster (controller_interface::ControllerInterface)
9+
10+
Wrench Transformer Node
11+
-----------------------
12+
The package also provides a standalone ROS2 node ``wrench_transformer_node`` that transforms wrench messages from the force_torque_sensor_broadcaster to different target frames using TF2. This allows applications to receive force/torque data in their preferred coordinate frames.
13+
14+
See the user documentation for details on configuration and usage.

force_torque_sensor_broadcaster/doc/userdoc.rst

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,39 @@ An example parameter file for this controller can be found in `the test director
3535

3636
.. literalinclude:: ../test/force_torque_sensor_broadcaster_params.yaml
3737
:language: yaml
38+
39+
Wrench Transformer Node
40+
-----------------------
41+
The package provides a standalone ROS2 node ``wrench_transformer_node`` that transforms wrench messages published by the ``ForceTorqueSensorBroadcaster`` controller to different target frames using TF2. This is useful when applications need force/torque data in coordinate frames other than the sensor frame.
42+
43+
The node subscribes to wrench messages from the broadcaster (either raw or filtered) and publishes transformed versions to separate topics for each target frame.
44+
45+
Usage
46+
^^^^^
47+
The wrench transformer node can be launched as a standalone executable:
48+
49+
.. code-block:: bash
50+
51+
ros2 run force_torque_sensor_broadcaster wrench_transformer_node
52+
53+
Parameters
54+
^^^^^^^^^^
55+
The wrench transformer uses the `generate_parameter_library <https://github.com/PickNikRobotics/generate_parameter_library>`_ to handle its parameters. The parameter `definition file for the wrench transformer <https://github.com/ros-controls/ros2_controllers/blob/{REPOS_FILE_BRANCH}/force_torque_sensor_broadcaster/src/wrench_transformer_parameters.yaml>`_ contains descriptions for all the parameters.
56+
57+
Full list of parameters:
58+
59+
.. generate_parameter_library_details:: ../src/wrench_transformer_parameters.yaml
60+
parameters_context.yaml
61+
62+
Topics
63+
^^^^^^
64+
The node subscribes to:
65+
- ``<broadcaster_namespace>/wrench`` (raw wrench messages) or ``<broadcaster_namespace>/wrench_filtered`` (filtered wrench messages) depending on the ``use_filtered_input`` parameter
66+
67+
The node publishes:
68+
- ``<output_topic_prefix>_<target_frame>`` for each target frame specified in ``target_frames``
69+
70+
Example parameter file:
71+
72+
.. literalinclude:: ../test/test_wrench_transformer.yaml
73+
:language: yaml
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// Copyright (c) 2025, ros2_control development team
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
/*
16+
* Authors: Julia Jia
17+
*/
18+
19+
#ifndef FORCE_TORQUE_SENSOR_BROADCASTER__WRENCH_TRANSFORMER_HPP_
20+
#define FORCE_TORQUE_SENSOR_BROADCASTER__WRENCH_TRANSFORMER_HPP_
21+
22+
#include <memory>
23+
#include <string>
24+
#include <unordered_map>
25+
#include <vector>
26+
27+
#include "geometry_msgs/msg/wrench_stamped.hpp"
28+
#include "rclcpp/rclcpp.hpp"
29+
#include "tf2_geometry_msgs/tf2_geometry_msgs.hpp"
30+
#include "tf2_ros/buffer.h"
31+
#include "tf2_ros/transform_listener.h"
32+
33+
// auto-generated by generate_parameter_library
34+
#include "force_torque_sensor_broadcaster/wrench_transformer_parameters.hpp"
35+
36+
namespace force_torque_sensor_broadcaster
37+
{
38+
39+
class WrenchTransformer : public rclcpp::Node
40+
{
41+
public:
42+
explicit WrenchTransformer(const rclcpp::NodeOptions & options = rclcpp::NodeOptions());
43+
44+
void init();
45+
46+
~WrenchTransformer() = default;
47+
48+
private:
49+
void wrench_callback(const geometry_msgs::msg::WrenchStamped::SharedPtr msg);
50+
bool transform_wrench(
51+
const geometry_msgs::msg::WrenchStamped & input_wrench,
52+
const std::string & target_frame, geometry_msgs::msg::WrenchStamped & output_wrench);
53+
54+
void setup_subscriber();
55+
void setup_publishers();
56+
57+
std::shared_ptr<force_torque_wrench_transformer::ParamListener> param_listener_;
58+
force_torque_wrench_transformer::Params params_;
59+
60+
rclcpp::Subscription<geometry_msgs::msg::WrenchStamped>::SharedPtr wrench_subscriber_;
61+
std::unordered_map<std::string, rclcpp::Publisher<geometry_msgs::msg::WrenchStamped>::SharedPtr>
62+
transformed_wrench_publishers_;
63+
64+
std::shared_ptr<tf2_ros::Buffer> tf_buffer_;
65+
std::shared_ptr<tf2_ros::TransformListener> tf_listener_;
66+
67+
std::string input_topic_;
68+
std::vector<std::string> target_frames_;
69+
};
70+
71+
} // namespace force_torque_sensor_broadcaster
72+
73+
#endif // FORCE_TORQUE_SENSOR_BROADCASTER__WRENCH_TRANSFORMER_HPP_
74+

force_torque_sensor_broadcaster/package.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
<depend>rclcpp_lifecycle</depend>
3434
<depend>realtime_tools</depend>
3535
<depend>generate_parameter_library</depend>
36+
<depend>tf2</depend>
37+
<depend>tf2_ros</depend>
38+
<depend>tf2_geometry_msgs</depend>
3639

3740
<test_depend>ament_cmake_gmock</test_depend>
3841
<test_depend>controller_manager</test_depend>
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Copyright (c) 2025, ros2_control development team
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
/*
16+
* Authors: Julia Jia
17+
*/
18+
19+
#include "force_torque_sensor_broadcaster/wrench_transformer.hpp"
20+
21+
#include <limits>
22+
#include <memory>
23+
#include <string>
24+
25+
#include "tf2/utils.hpp"
26+
27+
namespace force_torque_sensor_broadcaster
28+
{
29+
30+
WrenchTransformer::WrenchTransformer(const rclcpp::NodeOptions & options)
31+
: rclcpp::Node("fts_wrench_transformer", options)
32+
{
33+
// Initialize TF2
34+
tf_buffer_ = std::make_shared<tf2_ros::Buffer>(this->get_clock());
35+
tf_listener_ = std::make_shared<tf2_ros::TransformListener>(*tf_buffer_);
36+
}
37+
38+
void WrenchTransformer::init()
39+
{
40+
try
41+
{
42+
param_listener_ = std::make_shared<force_torque_wrench_transformer::ParamListener>(
43+
shared_from_this());
44+
params_ = param_listener_->get_params();
45+
}
46+
catch (const std::exception & e)
47+
{
48+
RCLCPP_ERROR(this->get_logger(), "Exception thrown during initialization: %s", e.what());
49+
return;
50+
}
51+
52+
// Setup subscriber and publishers
53+
setup_subscriber();
54+
setup_publishers();
55+
}
56+
57+
void WrenchTransformer::wrench_callback(const geometry_msgs::msg::WrenchStamped::SharedPtr msg)
58+
{
59+
60+
if (!msg || msg->header.frame_id.empty()) {
61+
RCLCPP_ERROR_THROTTLE(this->get_logger(), *this->get_clock(), 5000, "Invalid wrench message or frame_id is empty");
62+
return;
63+
}
64+
65+
for (const auto & target_frame : params_.target_frames) {
66+
geometry_msgs::msg::WrenchStamped output_wrench;
67+
// preserve timestamp
68+
output_wrench.header.stamp = msg->header.stamp;
69+
output_wrench.header.frame_id = target_frame;
70+
if (transform_wrench(*msg, target_frame, output_wrench)) {
71+
auto publisher = transformed_wrench_publishers_[target_frame];
72+
if (publisher) {
73+
publisher->publish(output_wrench);
74+
}
75+
}
76+
else {
77+
RCLCPP_WARN_THROTTLE(this->get_logger(), *this->get_clock(), 5000, "Failed to transform wrench for frame %s, skipping publication", target_frame.c_str());
78+
}
79+
}
80+
}
81+
82+
bool WrenchTransformer::transform_wrench(
83+
const geometry_msgs::msg::WrenchStamped & input_wrench, const std::string & target_frame,
84+
geometry_msgs::msg::WrenchStamped & output_wrench)
85+
{
86+
try {
87+
auto transform = tf_buffer_->lookupTransform(target_frame, input_wrench.header.frame_id, rclcpp::Time(0), rclcpp::Duration::from_seconds(params_.tf_timeout));
88+
output_wrench.header.frame_id = target_frame;
89+
tf2::doTransform(input_wrench, output_wrench, transform);
90+
// Preserve original timestamp after transform (doTransform may modify it)
91+
output_wrench.header.stamp = input_wrench.header.stamp;
92+
return true;
93+
}
94+
catch (const tf2::TransformException & e) {
95+
RCLCPP_ERROR_THROTTLE(this->get_logger(), *this->get_clock(), 5000, "Transform exception: %s", e.what());
96+
return false;
97+
}
98+
return false;
99+
}
100+
101+
void WrenchTransformer::setup_subscriber()
102+
{
103+
input_topic_ = params_.broadcaster_namespace.empty() ? "~/wrench" : params_.broadcaster_namespace + "/wrench";
104+
if (params_.use_filtered_input) {
105+
input_topic_ += "_filtered";
106+
}
107+
wrench_subscriber_ = this->create_subscription<geometry_msgs::msg::WrenchStamped>(
108+
input_topic_, rclcpp::SystemDefaultsQoS(), std::bind(&WrenchTransformer::wrench_callback, this, std::placeholders::_1));
109+
if (wrench_subscriber_ == nullptr) {
110+
RCLCPP_ERROR_THROTTLE(this->get_logger(), *this->get_clock(), 5000, "Failed to create wrench subscriber");
111+
return;
112+
}
113+
}
114+
115+
void WrenchTransformer::setup_publishers()
116+
{
117+
for (const auto & target_frame : params_.target_frames) {
118+
std::string topic_name = params_.output_topic_prefix + "_" + target_frame;
119+
transformed_wrench_publishers_[target_frame] = this->create_publisher<geometry_msgs::msg::WrenchStamped>(
120+
topic_name, rclcpp::SystemDefaultsQoS());
121+
if (transformed_wrench_publishers_[target_frame] == nullptr) {
122+
RCLCPP_ERROR_THROTTLE(this->get_logger(), *this->get_clock(), 5000, "Failed to create publisher for target frame %s", target_frame.c_str());
123+
return;
124+
}
125+
RCLCPP_INFO(this->get_logger(), "Created publisher for target frame %s", target_frame.c_str());
126+
}
127+
}
128+
129+
} // namespace force_torque_sensor_broadcaster
130+

0 commit comments

Comments
 (0)