38 import rosgraph.impl.graph
53 NODE_NODE_GRAPH =
'node_node'
55 NODE_TOPIC_GRAPH =
'node_topic'
57 NODE_TOPIC_ALL_GRAPH =
'node_topic_all'
59 QUIET_NAMES = [
'/diag_agg',
'/runtime_logger',
'/pr2_dashboard',
'/rviz',
60 '/rosout',
'/cpu_monitor',
'/monitor',
'/hd_monitor',
61 '/rxloggerlevel',
'/clock',
'/rqt',
'/statistics']
65 """Convert a node name to a valid dot name, which can't contain the leading space"""
73 if patternlist
is None or len(patternlist) == 0:
75 for pattern
in patternlist:
76 if unicode(name).strip() == pattern:
78 if re.match(
"^[a-zA-Z0-9_/]+$", pattern)
is None:
79 if re.match(
unicode(pattern), name.strip())
is not None:
86 def __init__(self, incoming=None, outgoing=None):
101 from rosgraph_msgs.msg
import TopicStatistics
111 if msg.node_sub
not in self.
edges:
112 self.
edges[msg.node_sub] = dict([])
114 if msg.topic
not in self.
edges[msg.node_sub]:
115 self.
edges[msg.node_sub][msg.topic] = dict([])
117 self.
edges[msg.node_sub][msg.topic][msg.node_pub] = msg
121 for sub
in self.
edges:
122 for topic
in self.
edges[sub]:
123 for pub
in self.
edges[sub][topic]:
124 traffic = max(traffic, self.
edges[sub][topic][pub].traffic)
129 for sub
in self.
edges:
130 for topic
in self.
edges[sub]:
131 for pub
in self.
edges[sub][topic]:
132 age = max(age, self.
edges[sub][topic][pub].stamp_age_mean.to_sec())
137 for pub
in self.
edges[sub][topic]:
138 age = max(age, self.
edges[sub][topic][pub].stamp_age_mean.to_sec())
147 elif sub
in self.
edges and topic
in self.
edges[sub]
and pub
in self.
edges[sub][topic]:
148 age = self.
edges[sub][topic][pub].stamp_age_mean.to_sec()
162 red = int(heat * 255 * 2)
166 green = 255 - int((heat - 0.5) * 255 * 2)
170 return [red, green, 0]
173 if pub
is None and sub
in self.
edges and topic
in self.
edges[sub]:
175 for p
in self.
edges[sub][topic]:
176 if pub
is None or p == pub:
177 traffic += self.
edges[sub][topic][p].traffic
185 if pub
is None and sub
in self.
edges and topic
in self.
edges[sub]:
186 conns = len(self.
edges[sub][topic])
188 pub = next(iter(self.
edges[sub][topic].keys()))
192 label =
"(" +
unicode(conns) +
" connections)"
193 return [label, penwidth, color]
195 if sub
in self.
edges and topic
in self.
edges[sub]
and pub
in self.
edges[sub][topic]:
198 period = self.
edges[sub][topic][pub].period_mean.to_sec()
200 freq =
unicode(round(1.0 / period, 1))
203 age = self.
edges[sub][topic][pub].stamp_age_mean.to_sec()
206 age_string =
" // " +
unicode(round(age, 2) * 1000) +
" ms"
207 label = freq +
" Hz" + age_string
208 return [label, penwidth, color]
210 return [
None,
None,
None]
212 def _add_edge(self, edge, dotcode_factory, dotgraph, is_topic=False):
218 if stat_label
is not None:
219 temp_label = edge.label +
"\\n" + stat_label
220 dotcode_factory.add_edge_to_graph(
225 url=
'topic:%s' % edge.label,
229 dotcode_factory.add_edge_to_graph(
234 url=
'topic:%s' % edge.label)
236 sub = edge.end.strip()
237 topic = edge.start.strip()
239 if stat_label
is not None:
240 temp_label = edge.label +
"\\n" + stat_label
241 dotcode_factory.add_edge_to_graph(
249 dotcode_factory.add_edge_to_graph(
250 dotgraph,
_conv(edge.start),
_conv(edge.end), label=edge.label)
252 def _add_node(self, node, rosgraphinst, dotcode_factory, dotgraph, unreachable):
253 if node
in rosgraphinst.bad_nodes:
256 bn = rosgraphinst.bad_nodes[node]
257 if bn.type == rosgraph.impl.graph.BadNode.DEAD:
258 dotcode_factory.add_node_to_graph(
260 nodename=
_conv(node),
263 url=node +
" (DEAD)",
265 elif bn.type == rosgraph.impl.graph.BadNode.WONKY:
266 dotcode_factory.add_node_to_graph(
268 nodename=
_conv(node),
271 url=node +
" (WONKY)",
274 dotcode_factory.add_node_to_graph(
276 nodename=
_conv(node),
279 url=node +
" (UNKNOWN)",
282 dotcode_factory.add_node_to_graph(
284 nodename=
_conv(node),
290 label = rosgraph.impl.graph.node_topic(node)
291 dotcode_factory.add_node_to_graph(
293 nodename=
_conv(node),
296 url=
"topic:%s" % label)
299 label = rosgraph.impl.graph.node_topic(node)
300 dotcode_factory.add_node_to_graph(
302 nodename=
_conv(node),
305 url=
'topic:%s' % label)
309 for n
in QUIET_NAMES:
315 for quiet_label
in [
'/time',
'/clock',
'/rosout',
'/statistics']:
316 if quiet_label == edge.label:
322 Determine the namespaces of the nodes being displayed
325 if graph_mode == NODE_NODE_GRAPH:
326 nodes = graph.nn_nodes
328 nodes = [n
for n
in nodes
if n
not in QUIET_NAMES]
329 namespaces = list(set([roslib.names.namespace(n)
for n
in nodes]))
331 elif graph_mode == NODE_TOPIC_GRAPH
or \
332 graph_mode == NODE_TOPIC_ALL_GRAPH:
333 nn_nodes = graph.nn_nodes
334 nt_nodes = graph.nt_nodes
336 nn_nodes = [n
for n
in nn_nodes
if n
not in QUIET_NAMES]
337 nt_nodes = [n
for n
in nt_nodes
if n
not in QUIET_NAMES]
338 if nn_nodes
or nt_nodes:
339 namespaces = [roslib.names.namespace(n)
for n
in nn_nodes]
343 namespaces.extend([roslib.names.namespace(n[1:])
for n
in nt_nodes])
345 return list(set(namespaces))
348 nodenames = [
unicode(n).strip()
for n
in nodes]
350 return [e
for e
in edges
if e.start.strip()
in nodenames
and e.end.strip()
in nodenames]
353 '''remove topic graphnodes without connected ROS nodes'''
358 if e.start.strip() ==
unicode(n).strip()
or e.end.strip() ==
unicode(n).strip():
362 removal_nodes.append(n)
363 for n
in removal_nodes:
368 '''splits a string after each comma, and treats tokens with leading dash as exclusions.
369 Adds .* as inclusion if no other inclusion option was given'''
372 for name
in ns_filter.split(
','):
373 if name.strip().startswith(
'-'):
374 excludes.append(name.strip()[1:])
376 includes.append(name.strip())
377 if includes == []
or includes == [
'/']
or includes == [
'']:
379 return includes, excludes
383 returns a dict mapping node name to edge objects partitioned in incoming and outgoing edges
385 node_connections = {}
387 if edge.start
not in node_connections:
389 if edge.end
not in node_connections:
391 node_connections[edge.start].outgoing.append(edge)
392 node_connections[edge.end].incoming.append(edge)
393 return node_connections
400 hide_single_connection_topics,
401 hide_dead_end_topics):
403 removes certain ending topic nodes and their edges from list of nodes and edges
405 @param hide_single_connection_topics:
406 if true removes topics that are only published/subscribed by one node
407 @param hide_dead_end_topics: if true removes topics having only publishers
409 if not hide_dead_end_topics
and not hide_single_connection_topics:
410 return nodes_in, edges_in
412 nodes = copy.copy(nodes_in)
413 edges = copy.copy(edges_in)
416 if n
in node_connections:
418 has_out_edges =
False
419 node_edges.extend(node_connections[n].outgoing)
420 if len(node_connections[n].outgoing) > 0:
422 node_edges.extend(node_connections[n].incoming)
423 if (hide_single_connection_topics
and len(node_edges) < 2)
or \
424 (hide_dead_end_topics
and not has_out_edges):
425 removal_nodes.append(n)
429 for n
in removal_nodes:
434 '''takes topic nodes, edges and node connections.
435 Returns topic nodes where action topics have been removed,
436 edges where the edges to action topics have been removed, and
437 a map with the connection to each virtual action topic node'''
441 nodes = copy.copy(nodes_in)
442 edges = copy.copy(edges_in)
444 if unicode(n).endswith(
'/feedback'):
445 prefix =
unicode(n)[:-len(
'/feedback')].strip()
446 action_topic_nodes = []
447 action_topic_edges_out = list()
448 action_topic_edges_in = list()
449 for suffix
in [
'/status',
'/result',
'/goal',
'/cancel',
'/feedback']:
451 if unicode(n2).strip() == prefix + suffix:
452 action_topic_nodes.append(n2)
453 if n2
in node_connections:
454 action_topic_edges_out.extend(node_connections[n2].outgoing)
455 action_topic_edges_in.extend(node_connections[n2].incoming)
456 if len(action_topic_nodes) == 5:
458 removal_nodes.extend(action_topic_nodes)
459 for e
in action_topic_edges_out:
462 for e
in action_topic_edges_in:
465 action_nodes[prefix] = {
'topics': action_topic_nodes,
466 'outgoing': action_topic_edges_out,
467 'incoming': action_topic_edges_in}
468 for n
in removal_nodes:
470 return nodes, edges, action_nodes
474 cluster_namespaces_level,
481 namespace_clusters = {}
482 if cluster_namespaces_level > 0:
483 for node
in node_list:
484 if unicode(node.strip()).count(
'/') > 2:
486 2, min(2 + cluster_namespaces_level, len(node.strip().split(
'/')))):
487 namespace =
'/'.join(node.strip().split(
'/')[:i])
488 parent_namespace =
'/'.join(node.strip().split(
'/')[:i - 1])
489 if namespace
not in namespace_clusters:
490 if parent_namespace ==
'':
491 namespace_clusters[namespace] = \
492 dotcode_factory.add_subgraph_to_graph(
498 elif parent_namespace
in namespace_clusters:
499 namespace_clusters[namespace] = \
500 dotcode_factory.add_subgraph_to_graph(
501 namespace_clusters[parent_namespace],
506 elif unicode(node.strip()).count(
'/') == 2:
507 namespace =
'/'.join(node.strip().split(
'/')[0:2])
508 if namespace
not in namespace_clusters:
509 namespace_clusters[namespace] = dotcode_factory.add_subgraph_to_graph(
515 return namespace_clusters
518 '''takes topic nodes, edges and node connections.
519 Returns topic nodes where tf topics have been removed,
520 edges where the edges to tf topics have been removed, and
521 a map with all the connections to the resulting tf group node'''
523 tf_topic_edges_in = []
524 tf_topic_edges_out = []
526 nodes = copy.copy(nodes_in)
527 edges = copy.copy(edges_in)
529 if unicode(n).strip()
in [
'/tf',
'/tf_static']
and n
in node_connections:
530 tf_topic_edges_in.extend(
531 [x
for x
in node_connections[n].incoming
if x
in edges
and x.end
in nodes])
532 tf_topic_edges_out.extend(
533 [x
for x
in node_connections[n].outgoing
if x
in edges
and x.start
in nodes])
534 removal_nodes.append(n)
536 for e
in tf_topic_edges_out:
539 for e
in tf_topic_edges_in:
542 for n
in removal_nodes:
545 if not tf_topic_edges_in
and not tf_topic_edges_out:
546 return nodes, edges,
None
548 return nodes, edges, {
'outgoing': tf_topic_edges_out,
'incoming': tf_topic_edges_in}
551 '''takes topic nodes, edges and node connections.
552 Returns topic nodes where image topics have been removed,
553 edges where the edges to image topics have been removed, and
554 a map with the connection to each virtual image topic node'''
558 nodes = copy.copy(nodes_in)
559 edges = copy.copy(edges_in)
561 if unicode(n).endswith(
'/compressed'):
562 prefix =
unicode(n)[:-len(
'/compressed')].strip()
563 image_topic_nodes = []
564 image_topic_edges_out = list()
565 image_topic_edges_in = list()
566 for suffix
in [
'/compressed',
'/compressedDepth',
'/theora',
'']:
568 if unicode(n2).strip() == prefix + suffix:
569 image_topic_nodes.append(n2)
570 if n2
in node_connections:
571 image_topic_edges_out.extend(node_connections[n2].outgoing)
572 image_topic_edges_in.extend(node_connections[n2].incoming)
573 if len(image_topic_nodes) >= 3:
575 removal_nodes.extend(image_topic_nodes)
576 for e
in image_topic_edges_out:
579 for e
in image_topic_edges_in:
582 image_nodes[prefix] = {
'topics': image_topic_nodes,
583 'outgoing': image_topic_edges_out,
584 'incoming': image_topic_edges_in}
585 for n
in removal_nodes:
587 return nodes, edges, image_nodes
595 hide_dynamic_reconfigure):
596 if not hide_tf_nodes
and not hide_dynamic_reconfigure:
597 return nodes_in, edges_in
599 nodes = copy.copy(nodes_in)
600 edges = copy.copy(edges_in)
603 if hide_dynamic_reconfigure
and unicode(n).endswith(
'/parameter_updates'):
604 prefix =
unicode(n)[:-len(
'/parameter_updates')].strip()
605 dynamic_reconfigure_topic_nodes = []
606 for suffix
in [
'/parameter_updates',
'/parameter_descriptions']:
608 if unicode(n2).strip() == prefix + suffix:
609 dynamic_reconfigure_topic_nodes.append(n2)
610 if len(dynamic_reconfigure_topic_nodes) == 2:
611 for n1
in dynamic_reconfigure_topic_nodes:
612 if n1
in node_connections:
613 for e
in node_connections[n1].outgoing + node_connections[n1].incoming:
616 removal_nodes.append(n1)
618 if hide_tf_nodes
and unicode(n).strip()
in [
'/tf',
'/tf_static']:
619 if n
in node_connections:
620 for e
in node_connections[n].outgoing + node_connections[n].incoming:
623 removal_nodes.append(n)
625 for n
in removal_nodes:
637 hide_single_connection_topics=False,
638 hide_dead_end_topics=False,
639 cluster_namespaces_level=0,
640 accumulate_actions=True,
648 group_tf_nodes=False,
650 group_image_nodes=False,
651 hide_dynamic_reconfigure=False):
661 if graph_mode == NODE_NODE_GRAPH:
662 nn_nodes = rosgraphinst.nn_nodes
665 edges = rosgraphinst.nn_edges
668 e.label, topic_includes)
and not matches_any(e.label, topic_excludes)]
670 elif graph_mode == NODE_TOPIC_GRAPH
or \
671 graph_mode == NODE_TOPIC_ALL_GRAPH:
672 nn_nodes = rosgraphinst.nn_nodes
673 nt_nodes = rosgraphinst.nt_nodes
677 [n
for n
in nt_nodes
if matches_any(n, topic_includes)
and
681 if graph_mode == NODE_TOPIC_GRAPH:
682 edges = [e
for e
in rosgraphinst.nt_edges]
684 edges = [e
for e
in rosgraphinst.nt_all_edges]
689 if graph_mode == NODE_NODE_GRAPH:
697 tf_connections =
None
699 if graph_mode != NODE_NODE_GRAPH
and (hide_single_connection_topics
or
700 hide_dead_end_topics
or accumulate_actions
or
701 group_tf_nodes
or hide_tf_nodes
or
702 group_image_nodes
or hide_dynamic_reconfigure):
710 hide_single_connection_topics,
711 hide_dead_end_topics)
718 hide_dynamic_reconfigure)
720 if accumulate_actions:
722 nt_nodes, edges, node_connections)
723 if group_image_nodes:
725 nt_nodes, edges, node_connections)
726 if group_tf_nodes
and not hide_tf_nodes:
728 nt_nodes, edges, node_connections)
736 dotgraph = dotcode_factory.get_graph(
742 ACTION_TOPICS_SUFFIX =
'/action_topics'
743 IMAGE_TOPICS_SUFFIX =
'/image_topics'
745 node_list = (nt_nodes
or []) + \
746 [action_prefix + ACTION_TOPICS_SUFFIX
for (action_prefix, _)
in action_nodes.items()] + \
747 [image_prefix + IMAGE_TOPICS_SUFFIX
for (image_prefix, _)
in image_nodes.items()] + \
748 nn_nodes
if nn_nodes
is not None else []
751 cluster_namespaces_level,
759 for n
in nt_nodes
or []:
761 if cluster_namespaces_level > 0
and \
762 unicode(n).strip().count(
'/') > 1
and \
763 len(n.strip().split(
'/')[1]) > 0:
764 if n.count(
'/') <= cluster_namespaces_level:
765 namespace =
unicode(
'/'.join(n.strip().split(
'/')[:-1]))
767 namespace =
'/'.join(n.strip().split(
'/')[:cluster_namespaces_level + 1])
769 n, dotcode_factory=dotcode_factory, dotgraph=namespace_clusters[namespace], quiet=quiet)
772 n, dotcode_factory=dotcode_factory, dotgraph=dotgraph, quiet=quiet)
774 for n
in [action_prefix + ACTION_TOPICS_SUFFIX
for (action_prefix, _)
in action_nodes.items()] + \
775 [image_prefix + IMAGE_TOPICS_SUFFIX
for (image_prefix, _)
in image_nodes.items()]:
777 if cluster_namespaces_level > 0
and \
778 unicode(n).strip().count(
'/') > 1
and \
779 len(
unicode(n).strip().split(
'/')[1]) > 0:
780 if n.strip().count(
'/') <= cluster_namespaces_level:
781 namespace =
unicode(
'/'.join(n.strip().split(
'/')[:-1]))
783 namespace =
'/'.join(n.strip().split(
'/')[:cluster_namespaces_level + 1])
785 'n' + n, dotcode_factory=dotcode_factory, dotgraph=namespace_clusters[namespace], quiet=quiet)
788 'n' + n, dotcode_factory=dotcode_factory, dotgraph=dotgraph, quiet=quiet)
790 if tf_connections !=
None:
793 'n/tf', dotcode_factory=dotcode_factory, dotgraph=dotgraph, quiet=quiet)
794 for out_edge
in tf_connections.get(
'outgoing', []):
795 dotcode_factory.add_edge_to_graph(dotgraph,
_conv(
'n/tf'),
_conv(out_edge.end))
796 for in_edge
in tf_connections.get(
'incoming', []):
797 dotcode_factory.add_edge_to_graph(dotgraph,
_conv(in_edge.start),
_conv(
'n/tf'))
801 for n
in nn_nodes
or []:
802 if cluster_namespaces_level > 0
and \
803 n.strip().count(
'/') > 1
and \
804 len(n.strip().split(
'/')[1]) > 0:
805 if n.count(
'/') <= cluster_namespaces_level:
806 namespace =
unicode(
'/'.join(n.strip().split(
'/')[:-1]))
808 namespace =
'/'.join(n.strip().split(
'/')[:cluster_namespaces_level + 1])
811 rosgraphinst=rosgraphinst,
812 dotcode_factory=dotcode_factory,
813 dotgraph=namespace_clusters[namespace],
814 unreachable=unreachable)
818 rosgraphinst=rosgraphinst,
819 dotcode_factory=dotcode_factory,
821 unreachable=unreachable)
825 e, dotcode_factory, dotgraph=dotgraph, is_topic=(graph_mode == NODE_NODE_GRAPH))
827 for (action_prefix, node_connections)
in action_nodes.items():
828 for out_edge
in node_connections.get(
'outgoing', []):
829 dotcode_factory.add_edge_to_graph(
831 _conv(
'n' + action_prefix + ACTION_TOPICS_SUFFIX),
833 for in_edge
in node_connections.get(
'incoming', []):
834 dotcode_factory.add_edge_to_graph(
836 _conv(in_edge.start),
837 _conv(
'n' + action_prefix + ACTION_TOPICS_SUFFIX))
838 for (image_prefix, node_connections)
in image_nodes.items():
839 for out_edge
in node_connections.get(
'outgoing', []):
840 dotcode_factory.add_edge_to_graph(
842 _conv(
'n' + image_prefix + IMAGE_TOPICS_SUFFIX),
844 for in_edge
in node_connections.get(
'incoming', []):
845 dotcode_factory.add_edge_to_graph(
847 _conv(in_edge.start),
848 _conv(
'n' + image_prefix + IMAGE_TOPICS_SUFFIX))
859 hide_single_connection_topics=False,
860 hide_dead_end_topics=False,
861 cluster_namespaces_level=0,
862 accumulate_actions=True,
871 group_tf_nodes=False,
872 group_image_nodes=False,
873 hide_dynamic_reconfigure=False):
875 @param rosgraphinst: RosGraph instance
876 @param ns_filter: nodename filter
877 @type ns_filter: string
878 @param topic_filter: topicname filter
879 @type ns_filter: string
880 @param graph_mode str: NODE_NODE_GRAPH | NODE_TOPIC_GRAPH | NODE_TOPIC_ALL_GRAPH
881 @type graph_mode: str
882 @param orientation: rankdir value (see ORIENTATIONS dict)
883 @type dotcode_factory: object
884 @param dotcode_factory: abstract factory manipulating dot language objects
885 @param hide_single_connection_topics: if true remove topics with just one connection
886 @param hide_dead_end_topics: if true remove topics with publishers only
887 @param cluster_namespaces_level: if > 0 places box around members of same namespace
888 (TODO: multiple namespace layers)
889 @param accumulate_actions: if true each 5 action topic graph nodes are shown as single graph node
890 @return: dotcode generated from graph singleton
894 rosgraphinst=rosgraphinst,
896 topic_filter=topic_filter,
897 graph_mode=graph_mode,
898 dotcode_factory=dotcode_factory,
899 hide_single_connection_topics=hide_single_connection_topics,
900 hide_dead_end_topics=hide_dead_end_topics,
901 cluster_namespaces_level=cluster_namespaces_level,
902 accumulate_actions=accumulate_actions,
903 orientation=orientation,
909 unreachable=unreachable,
910 hide_tf_nodes=hide_tf_nodes,
911 group_tf_nodes=group_tf_nodes,
912 group_image_nodes=group_image_nodes,
913 hide_dynamic_reconfigure=hide_dynamic_reconfigure)
914 dotcode = dotcode_factory.create_dot(dotgraph)