Nav2 Navigation Stack - humble  humble
ROS 2 Navigation Stack
bt2img.py
1 #!/usr/bin/python3
2 # Copyright (c) 2019 Intel Corporation
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 # http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 
16 # This tool converts a behavior tree XML file to a PNG image. Run bt2img.py -h
17 # for instructions
18 
19 import argparse
20 import xml.etree.ElementTree
21 import graphviz # pip3 install graphviz
22 
23 control_nodes = [
24  "Fallback",
25  "Parallel",
26  "ReactiveFallback",
27  "ReactiveSequence",
28  "Sequence",
29  "SequenceStar",
30  "BlackboardCheckInt",
31  "BlackboardCheckDouble",
32  "BlackboardCheckString",
33  "ForceFailure",
34  "ForceSuccess",
35  "Inverter",
36  "Repeat",
37  "Subtree",
38  "Timeout",
39  "RecoveryNode",
40  "PipelineSequence",
41  "RoundRobin",
42  "Control",
43  ]
44 action_nodes = [
45  "AlwaysFailure",
46  "AlwaysSuccess",
47  "SetBlackboard",
48  "ComputePathToPose",
49  "FollowPath",
50  "BackUp",
51  "Spin",
52  "Wait",
53  "ClearEntireCostmap",
54  "ReinitializeGlobalLocalization",
55  "Action",
56  ]
57 condition_nodes = [
58  "IsStuck",
59  "GoalReached",
60  "initialPoseReceived",
61  "GoalUpdated",
62  "DistanceTraveled",
63  "TimeExpired",
64  "TransformAvailable",
65  "Condition",
66  ]
67 decorator_nodes = [
68  "Decorator",
69  "RateController",
70  "DistanceController",
71  "SpeedController",
72 ]
73 subtree_nodes = [
74  "SubTree",
75 ]
76 
77 global xml_tree
78 
79 def main():
80  global xml_tree
81  args = parse_command_line()
82  xml_tree = xml.etree.ElementTree.parse(args.behavior_tree)
83  root_tree_name = find_root_tree_name(xml_tree)
84  behavior_tree = find_behavior_tree(xml_tree, root_tree_name)
85  dot = convert2dot(behavior_tree)
86  if args.legend:
87  legend = make_legend()
88  legend.format = 'png'
89  legend.render(args.legend)
90  dot.format = 'png'
91  if args.save_dot:
92  print(f'Saving dot to {args.save_dot}')
93  args.save_dot.write(dot.source)
94  dot.render(args.image_out, view=args.display)
95 
96 def parse_command_line():
97  parser = argparse.ArgumentParser(description='Convert a behavior tree XML file to an image')
98  parser.add_argument('--behavior_tree', required=True,
99  help='the behavior tree XML file to convert to an image')
100  parser.add_argument('--image_out', required=True,
101  help='The name of the output image file. Leave off the .png extension')
102  parser.add_argument('--display', action="store_true",
103  help='If specified, opens the image in the default viewer')
104  parser.add_argument('--save_dot', type=argparse.FileType('w'),
105  help='Saves the intermediate dot source to the specified file')
106  parser.add_argument('--legend',
107  help='Generate a legend image as well')
108  return parser.parse_args()
109 
110 def find_root_tree_name(xml_tree):
111  return xml_tree.getroot().get('main_tree_to_execute')
112 
113 def find_behavior_tree(xml_tree, tree_name):
114  trees = xml_tree.findall('BehaviorTree')
115  if len(trees) == 0:
116  raise RuntimeError("No behavior trees were found in the XML file")
117 
118  for tree in trees:
119  if tree_name == tree.get('ID'):
120  return tree
121 
122  raise RuntimeError(f'No behavior tree for name {tree_name} found in the XML file')
123 
124 # Generate a dot description of the root of the behavior tree.
125 def convert2dot(behavior_tree):
126  dot = graphviz.Digraph()
127  root = behavior_tree
128  parent_dot_name = str(hash(root))
129  dot.node(parent_dot_name, root.get('ID'), shape='box')
130  convert_subtree(dot, root, parent_dot_name)
131  return dot
132 
133 # Recursive function. We add the children to the dot file, and then recursively
134 # call this function on the children. Nodes are given an ID that is the hash
135 # of the node to ensure each is unique.
136 def convert_subtree(dot, parent_node, parent_dot_name):
137  if parent_node.tag == "SubTree":
138  add_sub_tree(dot, parent_dot_name, parent_node)
139  else:
140  add_nodes(dot, parent_dot_name, parent_node)
141 
142 def add_sub_tree(dot, parent_dot_name, parent_node):
143  root_tree_name = parent_node.get('ID')
144  dot.node(parent_dot_name, root_tree_name, shape='box')
145  behavior_tree = find_behavior_tree(xml_tree, root_tree_name)
146  convert_subtree(dot, behavior_tree, parent_dot_name)
147 
148 def add_nodes(dot, parent_dot_name, parent_node):
149  for node in list(parent_node):
150  label = make_label(node)
151  dot.node(str(hash(node)), label, color=node_color(node.tag), style='filled', shape='box')
152  dot_name = str(hash(node))
153  dot.edge(parent_dot_name, dot_name)
154  convert_subtree(dot, node, dot_name)
155 
156 # The node label contains the:
157 # type, the name if provided, and the parameters.
158 def make_label(node):
159  label = '< <table border="0" cellspacing="0" cellpadding="0">'
160  label += '<tr><td align="text"><i>' + node.tag + '</i></td></tr>'
161  name = node.get('name')
162  if name:
163  label += '<tr><td align="text"><b>' + name + '</b></td></tr>'
164 
165  for (param_name, value) in node.items():
166  label += '<tr><td align="left"><sub>' + param_name + '=' + value + '</sub></td></tr>'
167  label += '</table> >'
168  return label
169 
170 def node_color(type):
171  if type in control_nodes:
172  return "chartreuse4"
173  if type in action_nodes:
174  return "cornflowerblue"
175  if type in condition_nodes:
176  return "yellow2"
177  if type in decorator_nodes:
178  return "darkorange1"
179  if type in subtree_nodes:
180  return "darkorchid1"
181  #else it's unknown
182  return "grey"
183 
184 # creates a legend which can be provided with the other images.
185 def make_legend():
186  legend = graphviz.Digraph(graph_attr={'rankdir': 'LR'})
187  legend.attr(label='Legend')
188  legend.node('Unknown', shape='box', style='filled', color="grey")
189  legend.node('Action', 'Action Node', shape='box', style='filled', color="cornflowerblue")
190  legend.node('Condition', 'Condition Node', shape='box', style='filled', color="yellow2")
191  legend.node('Control', 'Control Node', shape='box', style='filled', color="chartreuse4")
192 
193  return legend
194 
195 
196 if __name__ == '__main__':
197  main()