Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
<url type="repository">https://github.com/ros-visualization/rqt_tf_tree</url>
<url type="bugtracker">https://github.com/ros-visualization/rqt_tf_tree/issues</url>

<test_depend>ament_flake8</test_depend>
<test_depend>ament_pep257</test_depend>
<test_depend>python3-pytest</test_depend>

<exec_depend version_gte="0.2.19">python_qt_binding</exec_depend>
Expand Down
5 changes: 5 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@
script_dir=$base/lib/rqt_tf_tree
[install]
install_scripts=$base/lib/rqt_tf_tree
[tool:pytest]
filterwarnings =
# flake8 spawns multiprocessing 'fork' workers while colcon's test
# executor is multi-threaded, which CPython >=3.12 warns about. Harmless.
ignore:This process:DeprecationWarning
6 changes: 5 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@
'rqt_tf_tree provides a GUI plugin for visualizing the ROS TF frame tree.'
),
license='BSD',
tests_require=['pytest'],
extras_require={
'test': [
'pytest',
],
},
entry_points={
'console_scripts': [
'rqt_tf_tree = ' + package_name + '.main:main'
Expand Down
19 changes: 8 additions & 11 deletions src/rqt_tf_tree/dotcode_tf.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,17 @@
# POSSIBILITY OF SUCH DAMAGE.

import time
import rclpy
import yaml

from rclpy import clock
from rclpy.constants import S_TO_NS
from tf2_msgs.srv import FrameGraph
import yaml


class RosTfTreeDotcodeGenerator(object):

def __init__(self, initial_listen_duration=1):
"""
:param initial_listen_duration: how many secs to listen to tf initially.
"""
""":param initial_listen_duration: how many secs to listen to tf initially."""
self.last_drawargs = None
self.dotcode = None
self.firstcall = True
Expand All @@ -58,15 +55,13 @@ def __init__(self, initial_listen_duration=1):
def generate_dotcode(self,
dotcode_factory,
tf2_frame_srv,
timer=rclpy.clock.Clock(),
timer=clock.Clock(),
yaml_parser=yaml,
rank='same', # None, same, min, max, source, sink
ranksep=0.2, # vertical distance between layers
rankdir='TB', # direction of layout (TB top > bottom, LR left > right)
force_refresh=False):
"""
:param force_refresh: if False, may return same dotcode as last time
"""
""":param force_refresh: if False, may return same dotcode as last time."""
if self.firstcall is True:
self.firstcall = False
force_refresh = True
Expand Down Expand Up @@ -131,7 +126,8 @@ def generate(self, data, timestamp):
edge_label = '"Broadcaster: %s\\n' % str(tf_frame_values['broadcaster'])
edge_label += 'Average rate: %s\\n' % str(tf_frame_values['rate'])
edge_label += 'Buffer length: %s\\n' % str(tf_frame_values['buffer_length'])
edge_label += 'Most recent transform: %s\\n' % str(tf_frame_values['most_recent_transform'])
edge_label += 'Most recent transform: %s\\n' % \
str(tf_frame_values['most_recent_transform'])
edge_label += 'Oldest transform: %s"' % str(tf_frame_values['oldest_transform'])
self.dotcode_factory.add_edge_to_graph(graph,
str(tf_frame_values['parent']),
Expand All @@ -146,7 +142,8 @@ def generate(self, data, timestamp):
root,
style='invis')

# dot += ' subgraph cluster_legend { style=bold; color=black; label ="view_frames Result";\n'
# dot += ' subgraph cluster_legend { style=bold; color=black;'
# dot += ' label ="view_frames Result";\n'
# dot += '"Recorded at time: '+str(rospy.Time.now().to_sec())+'"[ shape=plaintext ] ;\n'
# dot += '}->"'+root+'"[style=invis];\n}'
return graph
17 changes: 9 additions & 8 deletions src/rqt_tf_tree/tf_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,17 @@

from ament_index_python import get_resource

from tf2_msgs.srv import FrameGraph
import tf2_ros

from python_qt_binding import loadUi
from python_qt_binding.QtCore import QFile, QIODevice, QObject, Qt, Signal
from python_qt_binding.QtGui import QIcon, QImage, QPainter
from python_qt_binding.QtWidgets import QFileDialog, QGraphicsScene, QWidget
from python_qt_binding.QtSvg import QSvgGenerator
from qt_dotgraph.pydotfactory import PydotFactory
# from qt_dotgraph.pygraphvizfactory import PygraphvizFactory
from python_qt_binding.QtWidgets import QFileDialog, QGraphicsScene, QWidget
from qt_dotgraph.dot_to_qt import DotToQtGenerator
# from qt_dotgraph.pygraphvizfactory import PygraphvizFactory
from qt_dotgraph.pydotfactory import PydotFactory
from rqt_graph.interactive_graphics_view import InteractiveGraphicsView
from tf2_msgs.srv import FrameGraph
import tf2_ros

from .dotcode_tf import RosTfTreeDotcodeGenerator

Expand Down Expand Up @@ -82,7 +81,8 @@ def __init__(self, context):
loadUi(ui_file, self._widget, {'InteractiveGraphicsView': InteractiveGraphicsView})
self._widget.setObjectName('RosTfTreeUi')
if context.serial_number() > 1:
self._widget.setWindowTitle(self._widget.windowTitle() + (' (%d)' % context.serial_number()))
self._widget.setWindowTitle(
self._widget.windowTitle() + (' (%d)' % context.serial_number()))

self._scene = QGraphicsScene()
self._scene.setBackgroundBrush(Qt.white)
Expand Down Expand Up @@ -126,7 +126,8 @@ def restore_settings(self, plugin_settings, instance_settings):
self._widget.auto_fit_graph_check_box.setChecked(
instance_settings.value('auto_fit_graph_check_box_state', True) in [True, 'true'])
self._widget.highlight_connections_check_box.setChecked(
instance_settings.value('highlight_connections_check_box_state', True) in [True, 'true'])
instance_settings.value(
'highlight_connections_check_box_state', True) in [True, 'true'])
self.initialized = True
self._refresh_tf_graph()

Expand Down
16 changes: 6 additions & 10 deletions test/dotcode_tf_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@
# POSSIBILITY OF SUCH DAMAGE.

import unittest

import rclpy.client

from unittest.mock import Mock

from rqt_tf_tree.dotcode_tf import RosTfTreeDotcodeGenerator
Expand All @@ -44,11 +41,11 @@ class DotcodeGeneratorTest(unittest.TestCase):

def test_generate_dotcode(self):
yaml_data = {'frame1': {'parent': 'fr_parent',
'broadcaster': 'fr_broadcaster',
'rate': 'fr_rate',
'buffer_length': 'fr_buffer_length',
'most_recent_transform': 'fr_most_recent_transform',
'oldest_transform': 'fr_oldest_transform',}}
'broadcaster': 'fr_broadcaster',
'rate': 'fr_rate',
'buffer_length': 'fr_buffer_length',
'most_recent_transform': 'fr_most_recent_transform',
'oldest_transform': 'fr_oldest_transform'}}
frameClientMock = Mock()
frameClientMock.call.return_value.frame_yaml = str(yaml_data)

Expand All @@ -60,7 +57,7 @@ def test_generate_dotcode(self):
yamlmock = Mock()
yamlmock.load.return_value = yaml_data

factoryMock.create_dot.return_value = "foo"
factoryMock.create_dot.return_value = 'foo'
factoryMock.get_graph.return_value = graphMock

gen = RosTfTreeDotcodeGenerator(0)
Expand All @@ -74,4 +71,3 @@ def test_generate_dotcode(self):

if __name__ == '__main__':
unittest.main()

41 changes: 41 additions & 0 deletions test/test_flake8.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Copyright (c) 2026, Open Source Robotics Foundation, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

from ament_flake8.main import main_with_errors
import pytest


@pytest.mark.flake8
@pytest.mark.linter
def test_flake8():
rc, errors = main_with_errors(argv=[])
assert rc == 0, \
'Found %d code style errors / warnings:\n' % len(errors) + \
'\n'.join(errors)
39 changes: 39 additions & 0 deletions test/test_pep257.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Copyright (c) 2026, Open Source Robotics Foundation, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
# * Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

from ament_pep257.main import main
import pytest


@pytest.mark.linter
@pytest.mark.pep257
def test_pep257():
rc = main(argv=['.', 'test'])
assert rc == 0, 'Found code style errors / warnings'
Loading