diff --git a/sr_utilities_common/config/frames_to_remap.yaml b/sr_utilities_common/config/frames_to_remap.yaml new file mode 100644 index 00000000..33058a0f --- /dev/null +++ b/sr_utilities_common/config/frames_to_remap.yaml @@ -0,0 +1,24 @@ +# Copyright 2024 Shadow Robot Company Ltd. +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation version 2 of the License. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program. If not, see . + +--- +sr_tf_live_republisher: + output_topic: '/tf_filtered_record' + frequency: 100 + parent_to_child_frames: + - rh_palm: rh_fftip + - rh_palm: rh_mftip + - rh_palm: rh_rftip + - rh_palm: rh_lftip + - rh_palm: rh_thtip diff --git a/sr_utilities_common/launch/live_tf_republisher.launch b/sr_utilities_common/launch/live_tf_republisher.launch new file mode 100644 index 00000000..51f276c2 --- /dev/null +++ b/sr_utilities_common/launch/live_tf_republisher.launch @@ -0,0 +1,20 @@ + + + + + + + + + \ No newline at end of file diff --git a/sr_utilities_common/scripts/sr_utilities_common/live_tf_republisher.py b/sr_utilities_common/scripts/sr_utilities_common/live_tf_republisher.py new file mode 100755 index 00000000..0f6acbe6 --- /dev/null +++ b/sr_utilities_common/scripts/sr_utilities_common/live_tf_republisher.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 + +# Copyright 2024 Shadow Robot Company Ltd. +# +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation version 2 of the License. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along +# with this program. If not, see . + +import os +import yaml +import rospy +import tf2_ros +import tf2_msgs + + +class SrLiveTfRepublisher: + def __init__(self, config_file): + loaded_config = self._load_and_validate_config(config_file) + self._parent_child_frames = [] + for parent_child_pair in loaded_config['parent_to_child_frames']: + for parent, child in parent_child_pair.items(): + self._parent_child_frames.append((parent, child)) + + output_topic = loaded_config['output_topic'] + frequency = loaded_config['frequency'] + self._tf_buffer = tf2_ros.Buffer() + self._tf_listener = tf2_ros.TransformListener(self._tf_buffer) + self._broadcaster = tf2_ros.TransformBroadcaster() + self._broadcaster.pub_tf.name = output_topic + self._broadcaster.pub_tf.__init__(self._broadcaster.pub_tf.name, # pylint: disable=W0233 + tf2_msgs.msg.TFMessage, + queue_size=100) + self._timer = rospy.Timer(rospy.Duration(1/frequency), self._timer_callback) + + @staticmethod + def _load_and_validate_config(config_file): + if not os.path.exists(config_file): + raise FileNotFoundError(f'File {config_file} not found') + with open(config_file, 'r', encoding='utf-8') as opened_file: + loaded_config = yaml.safe_load(opened_file) + if 'sr_tf_live_republisher' not in loaded_config: + raise ValueError("Config file must have 'sr_tf_live_republisher' section at the top level") + if 'parent_to_child_frames' not in loaded_config['sr_tf_live_republisher']: + raise ValueError('Config file must contain parent_to_child_frames under sr_tf_live_republisher') + if len(loaded_config['sr_tf_live_republisher']['parent_to_child_frames']) == 0: + raise ValueError('parent_to_child_frames section must contain at least one parent to child frame mapping') + if 'output_topic' not in loaded_config['sr_tf_live_republisher']: + raise ValueError('Config file must contain output_topic section under sr_tf_live_republisher') + if 'frequency' not in loaded_config['sr_tf_live_republisher']: + raise ValueError('Config file must contain frequency section under sr_tf_live_republisher') + return loaded_config['sr_tf_live_republisher'] + + def _timer_callback(self, _): + for parent_child_pair in self._parent_child_frames: + try: + transform = self._tf_buffer.lookup_transform(parent_child_pair[0], parent_child_pair[1], rospy.Time()) + except (tf2_ros.LookupException, tf2_ros.ConnectivityException, tf2_ros.ExtrapolationException) as error: + rospy.logwarn_throttle(1.0, f'{error}') + else: + self._broadcaster.sendTransform(transform) + + +if __name__ == '__main__': + rospy.init_node('tf_live_filter', anonymous=True) + input_config_file = rospy.get_param('~config_file') + republisher = SrLiveTfRepublisher(input_config_file) + rospy.spin()