Nav2 Navigation Stack - rolling  main
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 as ET
21 
22 import graphviz # pip3 install graphviz
23 
24 control_nodes = [
25  'Fallback',
26  'Parallel',
27  'ReactiveFallback',
28  'ReactiveSequence',
29  'Sequence',
30  'SequenceWithMemory',
31  'BlackboardCheckInt',
32  'BlackboardCheckDouble',
33  'BlackboardCheckString',
34  'ForceFailure',
35  'ForceSuccess',
36  'Inverter',
37  'Repeat',
38  'Subtree',
39  'Timeout',
40  'RecoveryNode',
41  'PipelineSequence',
42  'RoundRobin',
43  'Control',
44 ]
45 action_nodes = [
46  'AlwaysFailure',
47  'AlwaysSuccess',
48  'SetBlackboard',
49  'ComputePathToPose',
50  'FollowPath',
51  'BackUp',
52  'Spin',
53  'Wait',
54  'ClearEntireCostmap',
55  'ReinitializeGlobalLocalization',
56  'Action',
57 ]
58 condition_nodes = [
59  'IsStuck',
60  'GoalReached',
61  'initialPoseReceived',
62  'GoalUpdated',
63  'DistanceTraveled',
64  'TimeExpired',
65  'TransformAvailable',
66  'Condition',
67 ]
68 decorator_nodes = [
69  'Decorator',
70  'RateController',
71  'DistanceController',
72  'SpeedController',
73 ]
74 subtree_nodes = [
75  'SubTree',
76 ]
77 
78 
79 def main() -> None:
80  args = parse_command_line()
81  xml_tree = ET.parse(args.behavior_tree)
82  root_tree_name = find_root_tree_name(xml_tree)
83  behavior_tree = find_behavior_tree(xml_tree, root_tree_name)
84  dot = convert2dot(behavior_tree, xml_tree)
85  if args.legend:
86  legend = make_legend()
87  legend.format = 'png'
88  legend.render(args.legend)
89  dot.format = 'png'
90  if args.save_dot:
91  print(f'Saving dot to {args.save_dot}')
92  args.save_dot.write(dot.source)
93  dot.render(args.image_out, view=args.display)
94 
95 
96 def parse_command_line() -> argparse.Namespace:
97  parser = argparse.ArgumentParser(
98  description='Convert a behavior tree XML file to an image'
99  )
100  parser.add_argument(
101  '--behavior_tree',
102  required=True,
103  help='the behavior tree XML file to convert to an image',
104  )
105  parser.add_argument(
106  '--image_out',
107  required=True,
108  help='The name of the output image file. Leave off the .png extension',
109  )
110  parser.add_argument(
111  '--display',
112  action='store_true',
113  help='If specified, opens the image in the default viewer',
114  )
115  parser.add_argument(
116  '--save_dot',
117  type=argparse.FileType('w'),
118  help='Saves the intermediate dot source to the specified file',
119  )
120  parser.add_argument('--legend', help='Generate a legend image as well')
121  return parser.parse_args()
122 
123 
124 def find_root_tree_name(xml_tree: ET.ElementTree) -> str:
125  root = xml_tree.getroot()
126  main_tree = root.get('main_tree_to_execute')
127  if main_tree is None:
128  raise RuntimeError('No main_tree_to_execute attribute found in XML root')
129  return main_tree
130 
131 
132 def find_behavior_tree(xml_tree: ET.ElementTree, tree_name: str) -> ET.Element:
133  trees = xml_tree.findall('BehaviorTree')
134  if len(trees) == 0:
135  raise RuntimeError('No behavior trees were found in the XML file')
136 
137  for tree in trees:
138  if tree_name == tree.get('ID'):
139  return tree
140 
141  raise RuntimeError(f'No behavior tree for name {tree_name} found in the XML file')
142 
143 
144 # Generate a dot description of the root of the behavior tree.
145 def convert2dot(behavior_tree: ET.Element, xml_tree: ET.ElementTree) -> graphviz.Digraph:
146  dot = graphviz.Digraph()
147  root = behavior_tree
148  parent_dot_name = str(hash(root))
149  dot.node(parent_dot_name, root.get('ID'), shape='box')
150  convert_subtree(dot, root, parent_dot_name, xml_tree)
151  return dot
152 
153 
154 # Recursive function. We add the children to the dot file, and then recursively
155 # call this function on the children. Nodes are given an ID that is the hash
156 # of the node to ensure each is unique.
157 def convert_subtree(
158  dot: graphviz.Digraph,
159  parent_node: ET.Element,
160  parent_dot_name: str,
161  xml_tree: ET.ElementTree,
162 ) -> None:
163  if parent_node.tag == 'SubTree':
164  add_sub_tree(dot, parent_dot_name, parent_node, xml_tree)
165  else:
166  add_nodes(dot, parent_dot_name, parent_node, xml_tree)
167 
168 
169 def add_sub_tree(
170  dot: graphviz.Digraph,
171  parent_dot_name: str,
172  parent_node: ET.Element,
173  xml_tree: ET.ElementTree,
174 ) -> None:
175  root_tree_name = parent_node.get('ID')
176  if root_tree_name is None:
177  raise RuntimeError('SubTree node has no ID attribute')
178  dot.node(parent_dot_name, root_tree_name, shape='box')
179  behavior_tree = find_behavior_tree(xml_tree, root_tree_name)
180  convert_subtree(dot, behavior_tree, parent_dot_name, xml_tree)
181 
182 
183 def add_nodes(
184  dot: graphviz.Digraph,
185  parent_dot_name: str,
186  parent_node: ET.Element,
187  xml_tree: ET.ElementTree,
188 ) -> None:
189  for node in list(parent_node):
190  label = make_label(node)
191  dot.node(
192  str(hash(node)),
193  label,
194  color=node_color(node.tag),
195  style='filled',
196  shape='box',
197  )
198  dot_name = str(hash(node))
199  dot.edge(parent_dot_name, dot_name)
200  convert_subtree(dot, node, dot_name, xml_tree)
201 
202 
203 # The node label contains the:
204 # type, the name if provided, and the parameters.
205 def make_label(node: ET.Element) -> str:
206  label = "< <table border='0' cellspacing='0' cellpadding='0'>"
207  label += f"<tr><td align='text'><i>{node.tag}</i></td></tr>"
208  name = node.get('name')
209  if name:
210  label += f"<tr><td align='text'><b>{name}</b></td></tr>"
211 
212  for param_name, value in node.items():
213  label += f"<tr><td align='left'><sub>{param_name}={value}</sub></td></tr>"
214  label += '</table> >'
215  return label
216 
217 
218 def node_color(node_type: str) -> str:
219  if node_type in control_nodes:
220  return 'chartreuse4'
221  if node_type in action_nodes:
222  return 'cornflowerblue'
223  if node_type in condition_nodes:
224  return 'yellow2'
225  if node_type in decorator_nodes:
226  return 'darkorange1'
227  if node_type in subtree_nodes:
228  return 'darkorchid1'
229  # else it's unknown
230  return 'grey'
231 
232 
233 # creates a legend which can be provided with the other images.
234 def make_legend() -> graphviz.Digraph:
235  legend = graphviz.Digraph(graph_attr={'rankdir': 'LR'})
236  legend.attr(label='Legend')
237  legend.node('Unknown', shape='box', style='filled', color='grey')
238  legend.node(
239  'Action', 'Action Node', shape='box', style='filled', color='cornflowerblue'
240  )
241  legend.node(
242  'Condition', 'Condition Node', shape='box', style='filled', color='yellow2'
243  )
244  legend.node(
245  'Control', 'Control Node', shape='box', style='filled', color='chartreuse4'
246  )
247 
248  return legend
249 
250 
251 if __name__ == '__main__':
252  main()