Nav2 Navigation Stack - jazzy  jazzy
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 
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 global xml_tree
79 
80 
81 def main():
82  global xml_tree
83  args = parse_command_line()
84  xml_tree = xml.etree.ElementTree.parse(args.behavior_tree)
85  root_tree_name = find_root_tree_name(xml_tree)
86  behavior_tree = find_behavior_tree(xml_tree, root_tree_name)
87  dot = convert2dot(behavior_tree)
88  if args.legend:
89  legend = make_legend()
90  legend.format = 'png'
91  legend.render(args.legend)
92  dot.format = 'png'
93  if args.save_dot:
94  print(f'Saving dot to {args.save_dot}')
95  args.save_dot.write(dot.source)
96  dot.render(args.image_out, view=args.display)
97 
98 
99 def parse_command_line():
100  parser = argparse.ArgumentParser(
101  description='Convert a behavior tree XML file to an image'
102  )
103  parser.add_argument(
104  '--behavior_tree',
105  required=True,
106  help='the behavior tree XML file to convert to an image',
107  )
108  parser.add_argument(
109  '--image_out',
110  required=True,
111  help='The name of the output image file. Leave off the .png extension',
112  )
113  parser.add_argument(
114  '--display',
115  action='store_true',
116  help='If specified, opens the image in the default viewer',
117  )
118  parser.add_argument(
119  '--save_dot',
120  type=argparse.FileType('w'),
121  help='Saves the intermediate dot source to the specified file',
122  )
123  parser.add_argument('--legend', help='Generate a legend image as well')
124  return parser.parse_args()
125 
126 
127 def find_root_tree_name(xml_tree):
128  return xml_tree.getroot().get('main_tree_to_execute')
129 
130 
131 def find_behavior_tree(xml_tree, tree_name):
132  trees = xml_tree.findall('BehaviorTree')
133  if len(trees) == 0:
134  raise RuntimeError('No behavior trees were found in the XML file')
135 
136  for tree in trees:
137  if tree_name == tree.get('ID'):
138  return tree
139 
140  raise RuntimeError(f'No behavior tree for name {tree_name} found in the XML file')
141 
142 
143 # Generate a dot description of the root of the behavior tree.
144 def convert2dot(behavior_tree):
145  dot = graphviz.Digraph()
146  root = behavior_tree
147  parent_dot_name = str(hash(root))
148  dot.node(parent_dot_name, root.get('ID'), shape='box')
149  convert_subtree(dot, root, parent_dot_name)
150  return dot
151 
152 
153 # Recursive function. We add the children to the dot file, and then recursively
154 # call this function on the children. Nodes are given an ID that is the hash
155 # of the node to ensure each is unique.
156 def convert_subtree(dot, parent_node, parent_dot_name):
157  if parent_node.tag == 'SubTree':
158  add_sub_tree(dot, parent_dot_name, parent_node)
159  else:
160  add_nodes(dot, parent_dot_name, parent_node)
161 
162 
163 def add_sub_tree(dot, parent_dot_name, parent_node):
164  root_tree_name = parent_node.get('ID')
165  dot.node(parent_dot_name, root_tree_name, shape='box')
166  behavior_tree = find_behavior_tree(xml_tree, root_tree_name)
167  convert_subtree(dot, behavior_tree, parent_dot_name)
168 
169 
170 def add_nodes(dot, parent_dot_name, parent_node):
171  for node in list(parent_node):
172  label = make_label(node)
173  dot.node(
174  str(hash(node)),
175  label,
176  color=node_color(node.tag),
177  style='filled',
178  shape='box',
179  )
180  dot_name = str(hash(node))
181  dot.edge(parent_dot_name, dot_name)
182  convert_subtree(dot, node, dot_name)
183 
184 
185 # The node label contains the:
186 # type, the name if provided, and the parameters.
187 def make_label(node):
188  label = "< <table border='0' cellspacing='0' cellpadding='0'>"
189  label += f"<tr><td align='text'><i>{node.tag}</i></td></tr>"
190  name = node.get('name')
191  if name:
192  label += f"<tr><td align='text'><b>{name}</b></td></tr>"
193 
194  for param_name, value in node.items():
195  label += f"<tr><td align='left'><sub>{param_name}={value}</sub></td></tr>"
196  label += '</table> >'
197  return label
198 
199 
200 def node_color(node_type):
201  if node_type in control_nodes:
202  return 'chartreuse4'
203  if node_type in action_nodes:
204  return 'cornflowerblue'
205  if node_type in condition_nodes:
206  return 'yellow2'
207  if node_type in decorator_nodes:
208  return 'darkorange1'
209  if node_type in subtree_nodes:
210  return 'darkorchid1'
211  # else it's unknown
212  return 'grey'
213 
214 
215 # creates a legend which can be provided with the other images.
216 def make_legend():
217  legend = graphviz.Digraph(graph_attr={'rankdir': 'LR'})
218  legend.attr(label='Legend')
219  legend.node('Unknown', shape='box', style='filled', color='grey')
220  legend.node(
221  'Action', 'Action Node', shape='box', style='filled', color='cornflowerblue'
222  )
223  legend.node(
224  'Condition', 'Condition Node', shape='box', style='filled', color='yellow2'
225  )
226  legend.node(
227  'Control', 'Control Node', shape='box', style='filled', color='chartreuse4'
228  )
229 
230  return legend
231 
232 
233 if __name__ == '__main__':
234  main()