$search
00001 # Software License Agreement (BSD License) 00002 # 00003 # Copyright (c) 2008, Willow Garage, Inc. 00004 # All rights reserved. 00005 # 00006 # Redistribution and use in source and binary forms, with or without 00007 # modification, are permitted provided that the following conditions 00008 # are met: 00009 # 00010 # * Redistributions of source code must retain the above copyright 00011 # notice, this list of conditions and the following disclaimer. 00012 # * Redistributions in binary form must reproduce the above 00013 # copyright notice, this list of conditions and the following 00014 # disclaimer in the documentation and/or other materials provided 00015 # with the distribution. 00016 # * Neither the name of Willow Garage, Inc. nor the names of its 00017 # contributors may be used to endorse or promote products derived 00018 # from this software without specific prior written permission. 00019 # 00020 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 00021 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 00022 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 00023 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 00024 # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 00025 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 00026 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 00027 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 00028 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 00029 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 00030 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 00031 # POSSIBILITY OF SUCH DAMAGE. 00032 00033 # this is a modified version of rx/rxgraph/src/rxgraph/dotcode.py 00034 00035 import re 00036 import copy 00037 00038 import rosgraph.impl.graph 00039 import roslib 00040 00041 # node/node connectivity 00042 NODE_NODE_GRAPH = 'node_node' 00043 # node/topic connections where an actual network connection exists 00044 NODE_TOPIC_GRAPH = 'node_topic' 00045 # all node/topic connections, even if no actual network connection 00046 NODE_TOPIC_ALL_GRAPH = 'node_topic_all' 00047 00048 QUIET_NAMES = ['/diag_agg', '/runtime_logger', '/pr2_dashboard', '/rviz', '/rosout', '/cpu_monitor', '/monitor', '/hd_monitor', '/rxloggerlevel', '/clock', '/rqt'] 00049 00050 00051 def matches_any(name, patternlist): 00052 if patternlist is None or len(patternlist) == 0: 00053 return False 00054 for pattern in patternlist: 00055 if str(name).strip() == pattern: 00056 return True 00057 if re.match("^[a-zA-Z0-9_/]+$", pattern) is None: 00058 if re.match(str(pattern), name.strip()) is not None: 00059 return True 00060 return False 00061 00062 00063 class NodeConnections: 00064 def __init__(self, incoming=None, outgoing=None): 00065 self.incoming = incoming or [] 00066 self.outgoing = outgoing or [] 00067 00068 00069 class RosGraphDotcodeGenerator: 00070 00071 def __init__(self): 00072 pass 00073 00074 def _add_edge(self, edge, dotcode_factory, dotgraph, is_topic=False): 00075 if is_topic: 00076 dotcode_factory.add_edge_to_graph(dotgraph, edge.start, edge.end, label=edge.label, url='topic:%s' % edge.label) 00077 else: 00078 dotcode_factory.add_edge_to_graph(dotgraph, edge.start, edge.end, label=edge.label) 00079 00080 def _add_node(self, node, rosgraphinst, dotcode_factory, dotgraph, quiet): 00081 if node in rosgraphinst.bad_nodes: 00082 if quiet: 00083 return '' 00084 bn = rosgraphinst.bad_nodes[node] 00085 if bn.type == rosgraph.impl.graph.BadNode.DEAD: 00086 dotcode_factory.add_node_to_graph(dotgraph, 00087 nodename=node, 00088 shape="doublecircle", 00089 url=node, 00090 color="red") 00091 else: 00092 dotcode_factory.add_node_to_graph(dotgraph, 00093 nodename=node, 00094 shape="doublecircle", 00095 url=node, 00096 color="orange") 00097 else: 00098 dotcode_factory.add_node_to_graph(dotgraph, 00099 nodename=node, 00100 shape='ellipse', 00101 url=node) 00102 00103 def _add_topic_node(self, node, dotcode_factory, dotgraph, quiet): 00104 label = rosgraph.impl.graph.node_topic(node) 00105 dotcode_factory.add_node_to_graph(dotgraph, 00106 nodename=label, 00107 nodelabel=label, 00108 shape='box', 00109 url="topic:%s" % label) 00110 00111 def _quiet_filter(self, name): 00112 # ignore viewers 00113 for n in QUIET_NAMES: 00114 if n in name: 00115 return False 00116 return True 00117 00118 def quiet_filter_topic_edge(self, edge): 00119 for quiet_label in ['/time', '/clock', '/rosout']: 00120 if quiet_label == edge.label: 00121 return False 00122 return self._quiet_filter(edge.start) and self._quiet_filter(edge.end) 00123 00124 def generate_namespaces(self, graph, graph_mode, quiet=False): 00125 """ 00126 Determine the namespaces of the nodes being displayed 00127 """ 00128 namespaces = [] 00129 if graph_mode == NODE_NODE_GRAPH: 00130 nodes = graph.nn_nodes 00131 if quiet: 00132 nodes = [n for n in nodes if not n in QUIET_NAMES] 00133 namespaces = list(set([roslib.names.namespace(n) for n in nodes])) 00134 00135 elif graph_mode == NODE_TOPIC_GRAPH or \ 00136 graph_mode == NODE_TOPIC_ALL_GRAPH: 00137 nn_nodes = graph.nn_nodes 00138 nt_nodes = graph.nt_nodes 00139 if quiet: 00140 nn_nodes = [n for n in nn_nodes if not n in QUIET_NAMES] 00141 nt_nodes = [n for n in nt_nodes if not n in QUIET_NAMES] 00142 if nn_nodes or nt_nodes: 00143 namespaces = [roslib.names.namespace(n) for n in nn_nodes] 00144 # an annoyance with the rosgraph library is that it 00145 # prepends a space to topic names as they have to have 00146 # different graph node namees from nodes. we have to strip here 00147 namespaces.extend([roslib.names.namespace(n[1:]) for n in nt_nodes]) 00148 00149 return list(set(namespaces)) 00150 00151 def _filter_orphaned_edges(self, edges, nodes): 00152 nodenames = [str(n).strip() for n in nodes] 00153 # currently using and rule as the or rule generates orphan nodes with the current logic 00154 return [e for e in edges if e.start.strip() in nodenames and e.end.strip() in nodenames] 00155 00156 def _filter_orphaned_topics(self, nt_nodes, edges): 00157 '''remove topic graphnodes without connected ROS nodes''' 00158 removal_nodes = [] 00159 for n in nt_nodes: 00160 keep = False 00161 for e in edges: 00162 if (e.start.strip() == str(n).strip() or e.end.strip() == str(n).strip()): 00163 keep = True 00164 break 00165 if not keep: 00166 removal_nodes.append(n) 00167 for n in removal_nodes: 00168 nt_nodes.remove(n) 00169 return nt_nodes 00170 00171 def _split_filter_string(self, ns_filter): 00172 '''splits a string after each comma, and treats tokens with leading dash as exclusions. 00173 Adds .* as inclusion if no other inclusion option was given''' 00174 includes = [] 00175 excludes = [] 00176 for name in ns_filter.split(','): 00177 if name.strip().startswith('-'): 00178 excludes.append(name.strip()[1:]) 00179 else: 00180 includes.append(name.strip()) 00181 if includes == [] or includes == ['/'] or includes == ['']: 00182 includes = ['.*'] 00183 return includes, excludes 00184 00185 def _get_node_edge_map(self, edges): 00186 '''returns a dict mapping node name to edge objects partitioned in incoming and outgoing edges''' 00187 node_connections = {} 00188 for edge in edges: 00189 if not edge.start in node_connections: 00190 node_connections[edge.start] = NodeConnections() 00191 if not edge.end in node_connections: 00192 node_connections[edge.end] = NodeConnections() 00193 node_connections[edge.start].outgoing.append(edge) 00194 node_connections[edge.end].incoming.append(edge) 00195 return node_connections 00196 00197 def _filter_leaf_topics(self, 00198 nodes_in, 00199 edges_in, 00200 node_connections, 00201 hide_single_connection_topics, 00202 hide_dead_end_topics): 00203 ''' 00204 removes certain ending topic nodes and their edges from list of nodes and edges 00205 00206 @param hide_single_connection_topics: if true removes topics that are only published/subscribed by one node 00207 @param hide_dead_end_topics: if true removes topics having only publishers 00208 ''' 00209 if not hide_dead_end_topics and not hide_single_connection_topics: 00210 return nodes_in, edges_in 00211 # do not manipulate incoming structures 00212 nodes = copy.copy(nodes_in) 00213 edges = copy.copy(edges_in) 00214 removal_nodes = [] 00215 for n in nodes: 00216 if n in node_connections: 00217 node_edges = [] 00218 has_out_edges = False 00219 node_edges.extend(node_connections[n].outgoing) 00220 if len(node_connections[n].outgoing) > 0: 00221 has_out_edges = True 00222 node_edges.extend(node_connections[n].incoming) 00223 if ((hide_single_connection_topics and len(node_edges) < 2) or 00224 (hide_dead_end_topics and not has_out_edges)): 00225 removal_nodes.append(n) 00226 for e in node_edges: 00227 if e in edges: 00228 edges.remove(e) 00229 for n in removal_nodes: 00230 nodes.remove(n) 00231 return nodes, edges 00232 00233 def _accumulate_action_topics(self, nodes_in, edges_in, node_connections): 00234 '''takes topic nodes, edges and node connections. 00235 Returns topic nodes where action topics have been removed, 00236 edges where the edges to action topics have been removed, and 00237 a map with the connection to each virtual action topic node''' 00238 removal_nodes = [] 00239 action_nodes = {} 00240 # do not manipulate incoming structures 00241 nodes = copy.copy(nodes_in) 00242 edges = copy.copy(edges_in) 00243 for n in nodes: 00244 if str(n).endswith('/feedback'): 00245 prefix = str(n)[:-len('/feedback')].strip() 00246 action_topic_nodes = [] 00247 action_topic_edges_out = set() 00248 action_topic_edges_in = set() 00249 for suffix in ['/status', '/result', '/goal', '/cancel', '/feedback']: 00250 for n2 in nodes: 00251 if str(n2).strip() == prefix + suffix: 00252 action_topic_nodes.append(n2) 00253 if n2 in node_connections: 00254 action_topic_edges_out.update(node_connections[n2].outgoing) 00255 action_topic_edges_in.update(node_connections[n2].incoming) 00256 if len(action_topic_nodes) == 5: 00257 # found action 00258 removal_nodes.extend(action_topic_nodes) 00259 for e in action_topic_edges_out: 00260 if e in edges: 00261 edges.remove(e) 00262 for e in action_topic_edges_in: 00263 if e in edges: 00264 edges.remove(e) 00265 action_nodes[prefix] = {'topics': action_topic_nodes, 00266 'outgoing': action_topic_edges_out, 00267 'incoming': action_topic_edges_in} 00268 for n in removal_nodes: 00269 nodes.remove(n) 00270 return nodes, edges, action_nodes 00271 00272 def generate_dotgraph(self, 00273 rosgraphinst, 00274 ns_filter, 00275 topic_filter, 00276 graph_mode, 00277 dotcode_factory, 00278 hide_single_connection_topics=False, 00279 hide_dead_end_topics=False, 00280 cluster_namespaces_level=0, 00281 accumulate_actions=True, 00282 orientation='LR', 00283 rank='same', # None, same, min, max, source, sink 00284 ranksep=0.2, # vertical distance between layers 00285 rankdir='TB', # direction of layout (TB top > bottom, LR left > right) 00286 simplify=True, # remove double edges 00287 quiet=False): 00288 """ 00289 See generate_dotcode 00290 """ 00291 includes, excludes = self._split_filter_string(ns_filter) 00292 topic_includes, topic_excludes = self._split_filter_string(topic_filter) 00293 00294 nn_nodes = [] 00295 nt_nodes = [] 00296 # create the node definitions 00297 if graph_mode == NODE_NODE_GRAPH: 00298 nn_nodes = rosgraphinst.nn_nodes 00299 nn_nodes = [n for n in nn_nodes if matches_any(n, includes) and not matches_any(n, excludes)] 00300 edges = rosgraphinst.nn_edges 00301 edges = [e for e in edges if matches_any(e.label, topic_includes) and not matches_any(e.label, topic_excludes)] 00302 00303 elif graph_mode == NODE_TOPIC_GRAPH or \ 00304 graph_mode == NODE_TOPIC_ALL_GRAPH: 00305 nn_nodes = rosgraphinst.nn_nodes 00306 nt_nodes = rosgraphinst.nt_nodes 00307 nn_nodes = [n for n in nn_nodes if matches_any(n, includes) and not matches_any(n, excludes)] 00308 nt_nodes = [n for n in nt_nodes if matches_any(n, topic_includes) and not matches_any(n, topic_excludes)] 00309 00310 # create the edge definitions, unwrap EdgeList objects into python lists 00311 if graph_mode == NODE_TOPIC_GRAPH: 00312 edges = [e for e in rosgraphinst.nt_edges] 00313 else: 00314 edges = [e for e in rosgraphinst.nt_all_edges] 00315 00316 if quiet: 00317 nn_nodes = filter(self._quiet_filter, nn_nodes) 00318 nt_nodes = filter(self._quiet_filter, nt_nodes) 00319 if graph_mode == NODE_NODE_GRAPH: 00320 edges = filter(self.quiet_filter_topic_edge, edges) 00321 00322 # for accumulating actions topics 00323 action_nodes = {} 00324 00325 if graph_mode != NODE_NODE_GRAPH and (hide_single_connection_topics or hide_dead_end_topics or accumulate_actions): 00326 # maps outgoing and incoming edges to nodes 00327 node_connections = self._get_node_edge_map(edges) 00328 00329 nt_nodes, edges = self._filter_leaf_topics(nt_nodes, 00330 edges, 00331 node_connections, 00332 hide_single_connection_topics, 00333 hide_dead_end_topics) 00334 00335 if accumulate_actions: 00336 nt_nodes, edges, action_nodes = self._accumulate_action_topics(nt_nodes, edges, node_connections) 00337 00338 edges = self._filter_orphaned_edges(edges, list(nn_nodes) + list(nt_nodes)) 00339 nt_nodes = self._filter_orphaned_topics(nt_nodes, edges) 00340 00341 # create the graph 00342 # result = "digraph G {\n rankdir=%(orientation)s;\n%(nodes_str)s\n%(edges_str)s}\n" % vars() 00343 dotgraph = dotcode_factory.get_graph(rank=rank, 00344 ranksep=ranksep, 00345 simplify=simplify, 00346 rankdir=orientation) 00347 00348 ACTION_TOPICS_SUFFIX = '/action_topics' 00349 namespace_clusters = {} 00350 for n in (nt_nodes or []) + [action_prefix + ACTION_TOPICS_SUFFIX for (action_prefix, _) in action_nodes.items()]: 00351 # cluster topics with same namespace 00352 if (cluster_namespaces_level > 0 and 00353 str(n).count('/') > 1 and 00354 len(str(n).split('/')[1]) > 0): 00355 namespace = str(n).split('/')[1] 00356 if namespace not in namespace_clusters: 00357 namespace_clusters[namespace] = dotcode_factory.add_subgraph_to_graph(dotgraph, namespace, rank=rank, rankdir=orientation, simplify=simplify) 00358 self._add_topic_node(n, dotcode_factory=dotcode_factory, dotgraph=namespace_clusters[namespace], quiet=quiet) 00359 else: 00360 self._add_topic_node(n, dotcode_factory=dotcode_factory, dotgraph=dotgraph, quiet=quiet) 00361 00362 # for ROS node, if we have created a namespace clusters for 00363 # one of its peer topics, drop it into that cluster 00364 if nn_nodes is not None: 00365 for n in nn_nodes: 00366 if (cluster_namespaces_level > 0 and 00367 str(n).count('/') >= 1 and 00368 len(str(n).split('/')[1]) > 0 and 00369 str(n).split('/')[1] in namespace_clusters): 00370 namespace = str(n).split('/')[1] 00371 self._add_node(n, rosgraphinst=rosgraphinst, dotcode_factory=dotcode_factory, dotgraph=namespace_clusters[namespace], quiet=quiet) 00372 else: 00373 self._add_node(n, rosgraphinst=rosgraphinst, dotcode_factory=dotcode_factory, dotgraph=dotgraph, quiet=quiet) 00374 00375 for e in edges: 00376 self._add_edge(e, dotcode_factory, dotgraph=dotgraph, is_topic=(graph_mode == NODE_NODE_GRAPH)) 00377 00378 for (action_prefix, node_connections) in action_nodes.items(): 00379 for out_edge in node_connections.get('outgoing', []): 00380 dotcode_factory.add_edge_to_graph(dotgraph, action_prefix[1:] + ACTION_TOPICS_SUFFIX, out_edge.end) 00381 for in_edge in node_connections.get('incoming', []): 00382 dotcode_factory.add_edge_to_graph(dotgraph, in_edge.start, action_prefix[1:] + ACTION_TOPICS_SUFFIX) 00383 return dotgraph 00384 00385 def generate_dotcode(self, 00386 rosgraphinst, 00387 ns_filter, 00388 topic_filter, 00389 graph_mode, 00390 dotcode_factory, 00391 hide_single_connection_topics=False, 00392 hide_dead_end_topics=False, 00393 cluster_namespaces_level=0, 00394 accumulate_actions=True, 00395 orientation='LR', 00396 rank='same', # None, same, min, max, source, sink 00397 ranksep=0.2, # vertical distance between layers 00398 rankdir='TB', # direction of layout (TB top > bottom, LR left > right) 00399 simplify=True, # remove double edges 00400 quiet=False): 00401 """ 00402 @param rosgraphinst: RosGraph instance 00403 @param ns_filter: nodename filter 00404 @type ns_filter: string 00405 @param topic_filter: topicname filter 00406 @type ns_filter: string 00407 @param graph_mode str: NODE_NODE_GRAPH | NODE_TOPIC_GRAPH | NODE_TOPIC_ALL_GRAPH 00408 @type graph_mode: str 00409 @param orientation: rankdir value (see ORIENTATIONS dict) 00410 @type dotcode_factory: object 00411 @param dotcode_factory: abstract factory manipulating dot language objects 00412 @param hide_single_connection_topics: if true remove topics with just one connection 00413 @param hide_dead_end_topics: if true remove topics with publishers only 00414 @param cluster_namespaces_level: if > 0 places box around members of same namespace (TODO: multiple namespace layers) 00415 @param accumulate_actions: if true each 5 action topic graph nodes are shown as single graph node 00416 @return: dotcode generated from graph singleton 00417 @rtype: str 00418 """ 00419 dotgraph = self.generate_dotgraph(rosgraphinst=rosgraphinst, 00420 ns_filter=ns_filter, 00421 topic_filter=topic_filter, 00422 graph_mode=graph_mode, 00423 dotcode_factory=dotcode_factory, 00424 hide_single_connection_topics=hide_single_connection_topics, 00425 hide_dead_end_topics=hide_dead_end_topics, 00426 cluster_namespaces_level=cluster_namespaces_level, 00427 accumulate_actions=accumulate_actions, 00428 orientation=orientation, 00429 rank=rank, 00430 ranksep=ranksep, 00431 rankdir=rankdir, 00432 simplify=simplify, 00433 quiet=quiet) 00434 dotcode = dotcode_factory.create_dot(dotgraph) 00435 return dotcode