#!/usr/bin/python3 # Copyright (c) 2019 Intel Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # This tool converts a behavior tree XML file to a PNG image. Run bt2img.py -h # for instructions import argparse import xml.etree.ElementTree import graphviz # pip3 install graphviz control_nodes = [ "Fallback", "Parallel", "ReactiveFallback", "ReactiveSequence", "Sequence", "SequenceStar", "BlackboardCheckInt", "BlackboardCheckDouble", "BlackboardCheckString", "ForceFailure", "ForceSuccess", "Inverter", "Repeat", "Subtree", "Timeout", "RecoveryNode", "PipelineSequence", "RoundRobin", "Control", ] action_nodes = [ "AlwaysFailure", "AlwaysSuccess", "SetBlackboard", "ComputePathToPose", "FollowPath", "BackUp", "Spin", "Wait", "ClearEntireCostmap", "ReinitializeGlobalLocalization", "Action", ] condition_nodes = [ "IsStuck", "GoalReached", "initialPoseReceived", "GoalUpdated", "DistanceTraveled", "TimeExpired", "TransformAvailable", "Condition", ] decorator_nodes = [ "Decorator", "RateController", "DistanceController", "SpeedController", ] subtree_nodes = [ "SubTree", ] global xml_tree def main(): global xml_tree args = parse_command_line() xml_tree = xml.etree.ElementTree.parse(args.behavior_tree) root_tree_name = find_root_tree_name(xml_tree) behavior_tree = find_behavior_tree(xml_tree, root_tree_name) dot = convert2dot(behavior_tree) if args.legend: legend = make_legend() legend.format = 'png' legend.render(args.legend) dot.format = 'png' if args.save_dot: print(f'Saving dot to {args.save_dot}') args.save_dot.write(dot.source) dot.render(args.image_out, view=args.display) def parse_command_line(): parser = argparse.ArgumentParser(description='Convert a behavior tree XML file to an image') parser.add_argument('--behavior_tree', required=True, help='the behavior tree XML file to convert to an image') parser.add_argument('--image_out', required=True, help='The name of the output image file. Leave off the .png extension') parser.add_argument('--display', action="store_true", help='If specified, opens the image in the default viewer') parser.add_argument('--save_dot', type=argparse.FileType('w'), help='Saves the intermediate dot source to the specified file') parser.add_argument('--legend', help='Generate a legend image as well') return parser.parse_args() def find_root_tree_name(xml_tree): return xml_tree.getroot().get('main_tree_to_execute') def find_behavior_tree(xml_tree, tree_name): trees = xml_tree.findall('BehaviorTree') if len(trees) == 0: raise RuntimeError("No behavior trees were found in the XML file") for tree in trees: if tree_name == tree.get('ID'): return tree raise RuntimeError(f'No behavior tree for name {tree_name} found in the XML file') # Generate a dot description of the root of the behavior tree. def convert2dot(behavior_tree): dot = graphviz.Digraph() root = behavior_tree parent_dot_name = str(hash(root)) dot.node(parent_dot_name, root.get('ID'), shape='box') convert_subtree(dot, root, parent_dot_name) return dot # Recursive function. We add the children to the dot file, and then recursively # call this function on the children. Nodes are given an ID that is the hash # of the node to ensure each is unique. def convert_subtree(dot, parent_node, parent_dot_name): if parent_node.tag == "SubTree": add_sub_tree(dot, parent_dot_name, parent_node) else: add_nodes(dot, parent_dot_name, parent_node) def add_sub_tree(dot, parent_dot_name, parent_node): root_tree_name = parent_node.get('ID') dot.node(parent_dot_name, root_tree_name, shape='box') behavior_tree = find_behavior_tree(xml_tree, root_tree_name) convert_subtree(dot, behavior_tree, parent_dot_name) def add_nodes(dot, parent_dot_name, parent_node): for node in list(parent_node): label = make_label(node) dot.node(str(hash(node)), label, color=node_color(node.tag), style='filled', shape='box') dot_name = str(hash(node)) dot.edge(parent_dot_name, dot_name) convert_subtree(dot, node, dot_name) # The node label contains the: # type, the name if provided, and the parameters. def make_label(node): label = '< ' label += '' name = node.get('name') if name: label += '' for (param_name, value) in node.items(): label += '' label += '
' + node.tag + '
' + name + '
' + param_name + '=' + value + '
>' return label def node_color(type): if type in control_nodes: return "chartreuse4" if type in action_nodes: return "cornflowerblue" if type in condition_nodes: return "yellow2" if type in decorator_nodes: return "darkorange1" if type in subtree_nodes: return "darkorchid1" #else it's unknown return "grey" # creates a legend which can be provided with the other images. def make_legend(): legend = graphviz.Digraph(graph_attr={'rankdir': 'LR'}) legend.attr(label='Legend') legend.node('Unknown', shape='box', style='filled', color="grey") legend.node('Action', 'Action Node', shape='box', style='filled', color="cornflowerblue") legend.node('Condition', 'Condition Node', shape='box', style='filled', color="yellow2") legend.node('Control', 'Control Node', shape='box', style='filled', color="chartreuse4") return legend if __name__ == '__main__': main()