From f527c12c77509ce5a21fcd7e647a264bc407c7f5 Mon Sep 17 00:00:00 2001 From: Christoph Froehlich Date: Fri, 31 Jan 2025 19:49:49 +0000 Subject: [PATCH 1/7] Revert "pygraphviz functions updated (#812)" This reverts commit fe1d47cb7ccb0d4077004cffa3d75dbdf1e16696. --- ros2controlcli/package.xml | 3 +- .../verb/view_controller_chains.py | 33 ++++++++++--------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/ros2controlcli/package.xml b/ros2controlcli/package.xml index 56e3501b71..17be8b409f 100644 --- a/ros2controlcli/package.xml +++ b/ros2controlcli/package.xml @@ -19,7 +19,8 @@ controller_manager controller_manager_msgs rosidl_runtime_py - python3-pygraphviz + python-graphviz-pip + graphviz ament_python diff --git a/ros2controlcli/ros2controlcli/verb/view_controller_chains.py b/ros2controlcli/ros2controlcli/verb/view_controller_chains.py index dafe067539..38905c242a 100644 --- a/ros2controlcli/ros2controlcli/verb/view_controller_chains.py +++ b/ros2controlcli/ros2controlcli/verb/view_controller_chains.py @@ -21,7 +21,7 @@ from ros2controlcli.api import add_controller_mgr_parsers -import pygraphviz as pgz +import graphviz def make_controller_node( @@ -73,7 +73,7 @@ def make_controller_node( "controller_start_" + output_controller, output_controller, deliminator ) - s.add_node(controller_name, label=f"{controller_name}|{{{{{inputs_str}}}|{{{outputs_str}}}}}") + s.node(controller_name, f"{controller_name}|{{{{{inputs_str}}}|{{{outputs_str}}}}}") def make_command_node(s, command_interfaces): @@ -87,9 +87,7 @@ def make_command_node(s, command_interfaces): "command_end_" + command_interface, command_interface, deliminator ) - s.add_node( - "command_interfaces", label="{}|{{{{{}}}}}".format("command_interfaces", outputs_str) - ) + s.node("command_interfaces", "{}|{{{{{}}}}}".format("command_interfaces", outputs_str)) def make_state_node(s, state_interfaces): @@ -103,7 +101,7 @@ def make_state_node(s, state_interfaces): "state_start_" + state_interface, state_interface, deliminator ) - s.add_node("state_interfaces", label="{}|{{{{{}}}}}".format("state_interfaces", inputs_str)) + s.node("state_interfaces", "{}|{{{{{}}}}}".format("state_interfaces", inputs_str)) def show_graph( @@ -115,9 +113,11 @@ def show_graph( state_interfaces, visualize, ): - s = pgz.AGraph(name="g", strict=False, directed=True, rankdir="LR") - s.node_attr["shape"] = "record" - s.node_attr["style"] = "rounded" + s = graphviz.Digraph( + "g", + filename="/tmp/controller_diagram.gv", + node_attr={"shape": "record", "style": "rounded"}, + ) port_map = dict() # get all controller names controller_names = set() @@ -142,27 +142,28 @@ def show_graph( for controller_name in controller_names: for connection in output_chain_connections[controller_name]: - s.add_edge( + s.edge( "{}:{}".format(controller_name, "controller_start_" + connection), "{}:{}".format( port_map["controller_end_" + connection], "controller_end_" + connection ), ) for state_connection in state_connections[controller_name]: - s.add_edge( + s.edge( "{}:{}".format("state_interfaces", "state_start_" + state_connection), "{}:{}".format(controller_name, "state_end_" + state_connection), ) for command_connection in command_connections[controller_name]: - s.add_edge( + s.edge( "{}:{}".format(controller_name, "command_start_" + command_connection), "{}:{}".format("command_interfaces", "command_end_" + command_connection), ) - s.graph_attr.update(ranksep="2") - s.layout(prog="dot") + s.attr(ranksep="2") + s.attr(rankdir="LR") + s.render(view=False) if visualize: - s.draw("/tmp/controller_diagram.gv.pdf", format="pdf") + s.view() def parse_response(list_controllers_response, list_hardware_response, visualize=True): @@ -205,5 +206,5 @@ def main(self, *, args): with NodeStrategy(args).direct_node as node: list_controllers_response = list_controllers(node, args.controller_manager) list_hardware_response = list_hardware_interfaces(node, args.controller_manager) - parse_response(list_controllers_response, list_hardware_response) + parse_response(list_controllers_response, list_hardware_response, visualize=False) return 0 From d877b587c173b517bfec9b060c69a537992cfa9d Mon Sep 17 00:00:00 2001 From: Christoph Froehlich Date: Fri, 31 Jan 2025 19:51:18 +0000 Subject: [PATCH 2/7] Update dep --- ros2controlcli/package.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ros2controlcli/package.xml b/ros2controlcli/package.xml index 17be8b409f..4309710c38 100644 --- a/ros2controlcli/package.xml +++ b/ros2controlcli/package.xml @@ -19,8 +19,7 @@ controller_manager controller_manager_msgs rosidl_runtime_py - python-graphviz-pip - graphviz + python3-graphviz ament_python From 31dca7b0bddaf62459e4c0d0ae6975d610b3f7c2 Mon Sep 17 00:00:00 2001 From: Christoph Froehlich Date: Fri, 31 Jan 2025 19:55:42 +0000 Subject: [PATCH 3/7] Rename variables --- .../ros2controlcli/verb/view_controller_chains.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ros2controlcli/ros2controlcli/verb/view_controller_chains.py b/ros2controlcli/ros2controlcli/verb/view_controller_chains.py index 38905c242a..9bc418f608 100644 --- a/ros2controlcli/ros2controlcli/verb/view_controller_chains.py +++ b/ros2controlcli/ros2controlcli/verb/view_controller_chains.py @@ -29,14 +29,14 @@ def make_controller_node( controller_name, state_interfaces, command_interfaces, - input_controllers, - output_controllers, + input_chain_connections, + output_chain_connections, port_map, ): state_interfaces = sorted(list(state_interfaces)) command_interfaces = sorted(list(command_interfaces)) - input_controllers = sorted(list(input_controllers)) - output_controllers = sorted(list(output_controllers)) + input_chain_connections = sorted(list(input_chain_connections)) + output_chain_connections = sorted(list(output_chain_connections)) inputs_str = "" for ind, state_interface in enumerate(state_interfaces): @@ -47,7 +47,7 @@ def make_controller_node( "state_end_" + state_interface, state_interface, deliminator ) - for ind, input_controller in enumerate(input_controllers): + for ind, input_controller in enumerate(input_chain_connections): deliminator = "|" if ind == len(input_controller) - 1: deliminator = "" @@ -65,7 +65,7 @@ def make_controller_node( "command_start_" + command_interface, command_interface, deliminator ) - for ind, output_controller in enumerate(output_controllers): + for ind, output_controller in enumerate(output_chain_connections): deliminator = "|" if ind == len(output_controller) - 1: deliminator = "" From 685d0dd594fb217c002e0bef14ee5ad63e4d14bc Mon Sep 17 00:00:00 2001 From: Christoph Froehlich Date: Fri, 31 Jan 2025 22:15:05 +0000 Subject: [PATCH 4/7] Add --save argument --- ros2controlcli/doc/userdoc.rst | 5 +++-- .../ros2controlcli/verb/view_controller_chains.py | 14 +++++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/ros2controlcli/doc/userdoc.rst b/ros2controlcli/doc/userdoc.rst index 8449583d61..6664e8ba8f 100644 --- a/ros2controlcli/doc/userdoc.rst +++ b/ros2controlcli/doc/userdoc.rst @@ -320,15 +320,16 @@ view_controller_chains .. code-block:: console $ ros2 control view_controller_chains -h - usage: ros2 control view_controller_chains [-h] [--spin-time SPIN_TIME] [-s] [-c CONTROLLER_MANAGER] [--include-hidden-nodes] [--ros-args ...] + usage: ros2 control view_controller_chains [-h] [--spin-time SPIN_TIME] [-s] [--save] [-c CONTROLLER_MANAGER] [--include-hidden-nodes] [--ros-args ...] - Generates a diagram of the loaded chained controllers into /tmp/controller_diagram.gv.pdf + Generates a diagram of the loaded chained controllers options: -h, --help show this help message and exit --spin-time SPIN_TIME Spin time in seconds to wait for discovery (only applies when not using an already running daemon) -s, --use-sim-time Enable ROS simulation time + --save Save PDF to controller_diagram.pdf instead of viewing image -c CONTROLLER_MANAGER, --controller-manager CONTROLLER_MANAGER Name of the controller manager ROS node (default: controller_manager) --include-hidden-nodes diff --git a/ros2controlcli/ros2controlcli/verb/view_controller_chains.py b/ros2controlcli/ros2controlcli/verb/view_controller_chains.py index 9bc418f608..dd82d1547d 100644 --- a/ros2controlcli/ros2controlcli/verb/view_controller_chains.py +++ b/ros2controlcli/ros2controlcli/verb/view_controller_chains.py @@ -161,9 +161,10 @@ def show_graph( s.attr(ranksep="2") s.attr(rankdir="LR") - s.render(view=False) if visualize: s.view() + else: + s.render(filename="controller_diagram", view=False, cleanup=True) def parse_response(list_controllers_response, list_hardware_response, visualize=True): @@ -196,15 +197,22 @@ def parse_response(list_controllers_response, list_hardware_response, visualize= class ViewControllerChainsVerb(VerbExtension): - """Generates a diagram of the loaded chained controllers into /tmp/controller_diagram.gv.pdf.""" + """Generates a diagram of the loaded chained controllers.""" def add_arguments(self, parser, cli_name): add_arguments(parser) + parser.add_argument( + "--save", + action="store_true", + help="Save PDF to controller_diagram.pdf instead of viewing image", + ) add_controller_mgr_parsers(parser) def main(self, *, args): with NodeStrategy(args).direct_node as node: list_controllers_response = list_controllers(node, args.controller_manager) list_hardware_response = list_hardware_interfaces(node, args.controller_manager) - parse_response(list_controllers_response, list_hardware_response, visualize=False) + parse_response( + list_controllers_response, list_hardware_response, visualize=not args.save + ) return 0 From fcce5fbdbaa35d4cd818f8d2a3685d229da5eeda Mon Sep 17 00:00:00 2001 From: Christoph Froehlich Date: Sat, 1 Feb 2025 09:14:46 +0000 Subject: [PATCH 5/7] Parse hardware components instead --- .../ros2controlcli/verb/view_controller_chains.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/ros2controlcli/ros2controlcli/verb/view_controller_chains.py b/ros2controlcli/ros2controlcli/verb/view_controller_chains.py index dd82d1547d..670a443442 100644 --- a/ros2controlcli/ros2controlcli/verb/view_controller_chains.py +++ b/ros2controlcli/ros2controlcli/verb/view_controller_chains.py @@ -12,8 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from controller_manager import list_controllers -from controller_manager import list_hardware_interfaces +from controller_manager import list_controllers, list_hardware_components from ros2cli.node.direct import add_arguments from ros2cli.node.strategy import NodeStrategy @@ -168,8 +167,12 @@ def show_graph( def parse_response(list_controllers_response, list_hardware_response, visualize=True): - command_interfaces = {x.name for x in list_hardware_response.command_interfaces} - state_interfaces = {x.name for x in list_hardware_response.state_interfaces} + command_interfaces = { + x.name for hw in list_hardware_response.component for x in hw.command_interfaces + } + state_interfaces = { + x.name for hw in list_hardware_response.component for x in hw.state_interfaces + } command_connections = dict() state_connections = dict() input_chain_connections = {x.name: set() for x in list_controllers_response.controller} @@ -211,7 +214,7 @@ def add_arguments(self, parser, cli_name): def main(self, *, args): with NodeStrategy(args).direct_node as node: list_controllers_response = list_controllers(node, args.controller_manager) - list_hardware_response = list_hardware_interfaces(node, args.controller_manager) + list_hardware_response = list_hardware_components(node, args.controller_manager) parse_response( list_controllers_response, list_hardware_response, visualize=not args.save ) From 672142a3ad646231d446c144ffeedfb1d2002036 Mon Sep 17 00:00:00 2001 From: Christoph Froehlich Date: Sat, 1 Feb 2025 09:36:13 +0000 Subject: [PATCH 6/7] Add labels for different interfaces --- .../ros2controlcli/verb/view_controller_chains.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/ros2controlcli/ros2controlcli/verb/view_controller_chains.py b/ros2controlcli/ros2controlcli/verb/view_controller_chains.py index 670a443442..a5a12dbb23 100644 --- a/ros2controlcli/ros2controlcli/verb/view_controller_chains.py +++ b/ros2controlcli/ros2controlcli/verb/view_controller_chains.py @@ -43,7 +43,7 @@ def make_controller_node( if ind == len(state_interface) - 1: deliminator = "" inputs_str += "<{}> {} {} ".format( - "state_end_" + state_interface, state_interface, deliminator + "state_end_" + state_interface, state_interface + " (state)", deliminator ) for ind, input_controller in enumerate(input_chain_connections): @@ -51,7 +51,7 @@ def make_controller_node( if ind == len(input_controller) - 1: deliminator = "" inputs_str += "<{}> {} {} ".format( - "controller_end_" + input_controller, input_controller, deliminator + "controller_end_" + input_controller, input_controller + " (exp ref)", deliminator ) port_map["controller_end_" + input_controller] = controller_name @@ -61,7 +61,7 @@ def make_controller_node( if ind == len(command_interface) - 1: deliminator = "" outputs_str += "<{}> {} {} ".format( - "command_start_" + command_interface, command_interface, deliminator + "command_start_" + command_interface, command_interface + " (cmd)", deliminator ) for ind, output_controller in enumerate(output_chain_connections): @@ -69,7 +69,9 @@ def make_controller_node( if ind == len(output_controller) - 1: deliminator = "" outputs_str += "<{}> {} {} ".format( - "controller_start_" + output_controller, output_controller, deliminator + "controller_start_" + output_controller, + output_controller + " (exp state)", + deliminator, ) s.node(controller_name, f"{controller_name}|{{{{{inputs_str}}}|{{{outputs_str}}}}}") @@ -86,7 +88,7 @@ def make_command_node(s, command_interfaces): "command_end_" + command_interface, command_interface, deliminator ) - s.node("command_interfaces", "{}|{{{{{}}}}}".format("command_interfaces", outputs_str)) + s.node("command_interfaces", "{}|{{{{{}}}}}".format("hw command_interfaces", outputs_str)) def make_state_node(s, state_interfaces): @@ -100,7 +102,7 @@ def make_state_node(s, state_interfaces): "state_start_" + state_interface, state_interface, deliminator ) - s.node("state_interfaces", "{}|{{{{{}}}}}".format("state_interfaces", inputs_str)) + s.node("state_interfaces", "{}|{{{{{}}}}}".format("hw state_interfaces", inputs_str)) def show_graph( From b91b5eeceb6ddaab7c571e9b37551e39c142b47f Mon Sep 17 00:00:00 2001 From: Christoph Froehlich Date: Sat, 1 Feb 2025 10:06:05 +0000 Subject: [PATCH 7/7] Fix test --- ros2controlcli/test/test_view_controller_chains.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/ros2controlcli/test/test_view_controller_chains.py b/ros2controlcli/test/test_view_controller_chains.py index 517c451faf..b4aa97e1fb 100644 --- a/ros2controlcli/test/test_view_controller_chains.py +++ b/ros2controlcli/test/test_view_controller_chains.py @@ -15,9 +15,10 @@ import unittest from controller_manager_msgs.msg import ControllerState from controller_manager_msgs.msg import HardwareInterface +from controller_manager_msgs.msg import HardwareComponentState from controller_manager_msgs.msg import ChainConnection from controller_manager_msgs.srv import ListControllers -from controller_manager_msgs.srv import ListHardwareInterfaces +from controller_manager_msgs.srv import ListHardwareComponents from ros2controlcli.verb.view_controller_chains import parse_response @@ -25,7 +26,7 @@ class TestViewControllerChains(unittest.TestCase): def test_expected(self): list_controllers_response = ListControllers.Response() - list_hardware_response = ListHardwareInterfaces.Response() + list_hardware_response = ListHardwareComponents.Response() chained_to_controller = ControllerState() chained_from_controller = ControllerState() @@ -72,8 +73,11 @@ def test_expected(self): controller_list = [chained_from_controller, chained_to_controller] list_controllers_response.controller = controller_list - list_hardware_response.state_interfaces = state_interfaces - list_hardware_response.command_interfaces = command_interfaces + + list_controllers_response_component = HardwareComponentState() + list_controllers_response_component.state_interfaces = state_interfaces + list_controllers_response_component.command_interfaces = command_interfaces + list_hardware_response.component.append(list_controllers_response_component) try: parse_response(list_controllers_response, list_hardware_response, visualize=False) except Exception: