36 from smach_msgs.msg
import SmachContainerStatus,SmachContainerInitialStatusCmd,SmachContainerStructure
49 if wxversion.checkInstalled(
"2.8"):
50 wxversion.select(
"2.8")
52 print(
"wxversion 2.8 is not installed, installed versions are {}".format(wxversion.getInstalled()))
65 custom_name = custom_name
or name
67 path =
filter(
lambda x: x != os.path.dirname(os.path.abspath(__file__)), sys.path)
68 f, pathname, desc = imp.find_module(name, path)
70 module = imp.load_module(custom_name, f, pathname, desc)
77 from smach_viewer
import xdot
84 """Generate an xdot graph attribute string.""" 85 attrs_strs = [
'"'+str(k)+
'"="'+str(v)+
'"' for k,v
in attrs.iteritems()]
86 return ';\n'.join(attrs_strs)+
';\n' 89 """Generate an xdot node attribute string.""" 90 attrs_strs = [
'"'+str(k)+
'"="'+str(v)+
'"' for k,v
in attrs.iteritems()]
91 return ' ['+(
', '.join(attrs_strs))+
']' 94 """Get the parent path of an xdot node.""" 95 path_tokens = path.split(
'/')
96 if len(path_tokens) > 2:
97 parent_path =
'/'.join(path_tokens[0:-1])
99 parent_path =
'/'.join(path_tokens[0:1])
103 """Get the label of an xdot node.""" 104 path_tokens = path.split(
'/')
105 return path_tokens[-1]
108 """Convert a hexadecimal color strng into a color tuple.""" 109 color_tuple = [int(color_str[i:i+2],16)/255.0
for i
in range(1,len(color_str),2)]
114 This class represents a given container in a running SMACH system. 116 Its primary use is to generate dotcode for a SMACH container. It has 117 methods for responding to structure and status messages from a SMACH 118 introspection server, as well as methods for updating the styles of a 119 graph once it's been drawn. 125 splitpath = msg.path.split(
'/')
127 self.
_dir =
'/'.join(splitpath[0:-1])
144 """Update the structure of this container from a given message. Return True if anything changes.""" 165 """Update the known userdata and active state set and return True if the graph needs to be redrawn.""" 183 while not rospy.is_shutdown():
185 self._local_data._data = pickle.loads(msg.local_data)
187 except ImportError
as ie:
189 modulename = ie.args[0][16:]
190 packagename = modulename[0:modulename.find(
'.')]
191 roslib.load_manifest(packagename)
192 self._local_data._data = pickle.loads(msg.local_data)
195 self.
_info = msg.info
199 def get_dotcode(self, selected_paths, closed_paths, depth, max_depth, containers, show_all, label_wrapper, attrs={}):
200 """Generate the dotcode representing this container. 202 @param selected_paths: The paths to nodes that are selected 203 @closed paths: The paths that shouldn't be expanded 204 @param depth: The depth to start traversing the tree 205 @param max_depth: The depth to which we should traverse the tree 206 @param containers: A dict of containers keyed by their paths 207 @param show_all: True if implicit transitions should be shown 208 @param label_wrapper: A text wrapper for wrapping element names 209 @param attrs: A dict of dotcode attributes for this cluster 212 dotstr =
'subgraph "cluster_%s" {\n' % (self.
_path)
215 attrs[
'color'] =
'#00000000' 216 attrs[
'fillcolor'] =
'#0000000F' 231 proxy_attrs[
'label'] =
'\\n'.join(label_wrapper.wrap(self.
_label))
232 dotstr +=
'"%s" %s;\n' % (
233 '/'.join([self.
_path,
'__proxy__']),
237 if max_depth == -1
or depth <= max_depth:
239 dotstr +=
'subgraph "cluster_%s" {\n' %
'/'.join([self.
_path,
'__outcomes__'])
241 'style':
'rounded,filled',
244 'fillcolor':
'#FFFFFF00' 249 outcome_path =
':'.join([self.
_path,outcome_label])
253 'style':
'filled,rounded',
255 'fillcolor':
'#FE464f',
257 'fontcolor':
'#780006',
258 'label':
'\\n'.join(label_wrapper.wrap(outcome_label)),
259 'URL':
':'.join([self.
_path,outcome_label])
261 dotstr +=
'"%s" %s;\n' % (outcome_path,
attr_string(outcome_attrs))
267 'style':
'filled,setlinewidth(2)',
269 'fillcolor':
'#FFFFFF00' 272 child_path =
'/'.join([self.
_path,child_label])
274 if child_path
in containers:
275 child_attrs[
'style'] +=
',rounded' 286 child_attrs[
'label'] =
'\\n'.join(label_wrapper.wrap(child_label))
287 child_attrs[
'URL'] = child_path
288 dotstr +=
'"%s" %s;\n' % (child_path,
attr_string(child_attrs))
291 internal_edges = zip(
297 internal_edges += [(
'',
'__proxy__',initial_child)
for initial_child
in self.
_initial_states]
299 has_explicit_transitions = []
300 for (outcome_label,from_label,to_label)
in internal_edges:
301 if to_label !=
'None' or outcome_label == to_label:
302 has_explicit_transitions.append(from_label)
305 for (outcome_label,from_label,to_label)
in internal_edges:
307 from_path =
'/'.join([self.
_path, from_label])
310 or to_label !=
'None'\
311 or from_label
not in has_explicit_transitions \
312 or (outcome_label == from_label) \
313 or from_path
in containers:
315 if to_label ==
'None':
316 to_label = outcome_label
318 to_path =
'/'.join([self.
_path, to_label])
321 'URL':
':'.join([from_path,outcome_label,to_path]),
323 'label':
'\\n'.join(label_wrapper.wrap(outcome_label))}
324 edge_attrs[
'style'] =
'setlinewidth(2)' 330 from_key =
'"%s"' % from_path
331 if from_path
in containers:
332 if max_depth == -1
or depth+1 <= max_depth:
333 from_key =
'"%s:%s"' % ( from_path, outcome_label)
335 edge_attrs[
'ltail'] =
'cluster_'+from_path
336 from_path =
'/'.join([from_path,
'__proxy__'])
337 from_key =
'"%s"' % ( from_path )
341 to_key =
'"%s:%s"' % (self.
_path,to_label)
342 edge_attrs[
'color'] =
'#00000055' 344 if to_path
in containers:
345 edge_attrs[
'lhead'] =
'cluster_'+to_path
346 to_path =
'/'.join([to_path,
'__proxy__'])
347 to_key =
'"%s"' % to_path
349 dotstr +=
'%s -> %s %s;\n' % (
355 def set_styles(self, selected_paths, depth, max_depth, items, subgraph_shapes, containers):
356 """Update the styles for a list of containers without regenerating the dotcode. 358 This function is called recursively to update an entire tree. 360 @param selected_paths: A list of paths to nodes that are currently selected. 361 @param depth: The depth to start traversing the tree 362 @param max_depth: The depth to traverse into the tree 363 @param items: A dict of all the graph items, keyed by url 364 @param subgraph_shapes: A dictionary of shapes from the rendering engine 365 @param containers: A dict of all the containers 371 container_shapes = subgraph_shapes['cluster_'+self._path] 372 container_color = (0,0,0,0) 373 container_fillcolor = (0,0,0,0) 375 for shape in container_shapes: 376 shape.pen.color = container_color 377 shape.pen.fillcolor = container_fillcolor 383 if max_depth == -1
or depth <= max_depth:
386 child_path =
'/'.join([self.
_path,child_label])
388 child_color = [0.5,0.5,0.5,1]
389 child_fillcolor = [1,1,1,1]
392 active_color =
hex2t(
'#5C7600FF')
393 active_fillcolor =
hex2t(
'#C0F700FF')
395 initial_color =
hex2t(
'#000000FF')
396 initial_fillcolor =
hex2t(
'#FFFFFFFF')
400 child_color = active_color
401 child_fillcolor = active_fillcolor
406 child_color = initial_color
410 if child_path
in selected_paths:
411 child_color =
hex2t(
'#FB000DFF')
414 if child_path
in containers:
415 subgraph_id =
'cluster_'+child_path
416 if subgraph_id
in subgraph_shapes:
418 child_fillcolor[3] = 0.25
420 child_fillcolor[3] = 0.25
423 v = 1.0-0.25*((depth+1)/float(max_depth))
426 child_fillcolor = [v,v,v,1.0]
429 for shape
in subgraph_shapes[
'cluster_'+child_path]:
431 if len(pen.color) > 3:
432 pen_color_opacity = pen.color[3]
433 if pen_color_opacity < 0.01:
434 pen_color_opacity = 0
436 pen_color_opacity = 0.5
437 shape.pen.color = child_color[0:3]+[pen_color_opacity]
438 shape.pen.fillcolor = [child_fillcolor[i]
for i
in range(min(3,len(pen.fillcolor)))]
439 shape.pen.linewidth = child_linewidth
449 if child_path
in items:
450 for shape
in items[child_path].shapes:
452 shape.pen.color = child_color
453 shape.pen.fillcolor = child_fillcolor
454 shape.pen.linewidth = child_linewidth
461 This class provides a GUI application for viewing SMACH plans. 464 wx.Frame.__init__(self,
None, -1,
"Smach Viewer", size=(720,480))
473 vbox = wx.BoxSizer(wx.VERTICAL)
478 self.content_splitter.SetMinimumPaneSize(24)
479 self.content_splitter.SetSashGravity(0.85)
486 nb = wx.Notebook(viewer,-1,style=wx.NB_TOP | wx.WANTS_CHARS)
487 viewer_box = wx.BoxSizer()
488 viewer_box.Add(nb,1,wx.EXPAND | wx.ALL, 4)
489 viewer.SetSizer(viewer_box)
492 graph_view = wx.Panel(nb,-1)
493 gv_vbox = wx.BoxSizer(wx.VERTICAL)
494 graph_view.SetSizer(gv_vbox)
497 toolbar = wx.ToolBar(graph_view, -1)
499 toolbar.AddControl(wx.StaticText(toolbar,-1,
"Path: "))
502 self.
path_combo = wx.ComboBox(toolbar, -1, style=wx.CB_DROPDOWN)
504 self.path_combo.Append(
'/')
505 self.path_combo.SetValue(
'/')
514 self.depth_spinner.Bind(wx.EVT_SPINCTRL,self.
set_depth)
516 toolbar.AddControl(wx.StaticText(toolbar,-1,
" Depth: "))
527 toolbar.AddControl(wx.StaticText(toolbar,-1,
" Label Width: "))
531 toggle_all = wx.ToggleButton(toolbar,-1,
'Show Implicit')
535 toolbar.AddControl(wx.StaticText(toolbar,-1,
" "))
536 toolbar.AddControl(toggle_all)
538 toggle_auto_focus = wx.ToggleButton(toolbar, -1,
'Auto Focus')
542 toolbar.AddControl(wx.StaticText(toolbar, -1,
" "))
543 toolbar.AddControl(toggle_auto_focus)
545 toolbar.AddControl(wx.StaticText(toolbar,-1,
" "))
546 toolbar.AddLabelTool(wx.ID_HELP,
'Help',
547 wx.ArtProvider.GetBitmap(wx.ART_HELP,wx.ART_OTHER,(16,16)) )
548 toolbar.AddLabelTool(wx.ID_SAVE,
'Save',
549 wx.ArtProvider.GetBitmap(wx.ART_FILE_SAVE,wx.ART_OTHER,(16,16)) )
553 self.Bind(wx.EVT_TOOL, self.
SaveDotGraph, id=wx.ID_SAVE)
558 gv_vbox.Add(toolbar, 0, wx.EXPAND)
559 gv_vbox.Add(self.
widget, 1, wx.EXPAND)
562 self.
tree = wx.TreeCtrl(nb,-1,style=wx.TR_HAS_BUTTONS)
563 nb.AddPage(graph_view,
"Graph View")
564 nb.AddPage(self.
tree,
"Tree View")
568 borders = wx.LEFT | wx.RIGHT | wx.TOP
571 self.
ud_gs = wx.BoxSizer(wx.VERTICAL)
573 self.ud_gs.Add(wx.StaticText(self.
ud_win,-1,
"Path:"),0, borders, border)
577 self.ud_gs.Add(self.
path_input,0,wx.EXPAND | borders, border)
580 self.ud_gs.Add(wx.StaticText(self.
ud_win,-1,
"Userdata:"),0, borders, border)
582 self.
ud_txt = wx.TextCtrl(self.
ud_win,-1,style=wx.TE_MULTILINE | wx.TE_READONLY)
583 self.ud_gs.Add(self.
ud_txt,1,wx.EXPAND | borders, border)
588 self.is_button.Disable()
589 self.ud_gs.Add(self.
is_button,0,wx.EXPAND | wx.BOTTOM | borders, border)
591 self.ud_win.SetSizer(self.
ud_gs)
595 self.content_splitter.SplitVertically(viewer, self.
ud_win, 512)
608 self.
_client = smach_ros.IntrospectionClient()
616 self.Bind(wx.EVT_IDLE,self.
OnIdle)
617 self.Bind(wx.EVT_CLOSE,self.
OnQuit)
620 self.widget.register_select_callback(self.
select_cb)
628 self._server_list_thread.start()
631 self._update_graph_thread.start()
633 self._update_tree_thread.start()
636 """Quit Event: kill threads and wait for join.""" 639 self._update_cond.notify_all()
641 self._server_list_thread.join()
642 self._update_graph_thread.join()
643 self._update_tree_thread.join()
648 """Notify all that the graph needs to be updated.""" 650 self._update_cond.notify_all()
653 """Event: Change the initial state of the server.""" 658 server_name = self.
_containers[parent_path]._server_name
659 self._client.set_initial_state(server_name,parent_path,[state],timeout = rospy.Duration(60.0))
662 """Event: Change the viewable path and update the graph.""" 663 self.
_path = self.path_combo.GetValue()
670 self.path_combo.SetValue(path)
674 """Event: Change the maximum depth and update the graph.""" 675 self.
_max_depth = self.depth_spinner.GetValue()
681 self.depth_spinner.SetValue(max_depth)
686 """Event: Change the label wrapper width and update the graph.""" 687 self._label_wrapper.width = self.width_spinner.GetValue()
692 """Event: Change whether automatic transitions are hidden and update the graph.""" 698 """Event: Enable/Disable automatically focusing""" 708 """Event: Click to select a graph node to display user data and update the graph.""" 711 if not type(item.url)
is str:
714 self.statusbar.SetStatusText(item.url)
716 if event.ButtonUp(wx.MOUSE_BTN_LEFT):
720 self.path_input.SetValue(item.url)
722 self.path_input.GetEventHandler(),
723 wx.CommandEvent(wx.wxEVT_COMMAND_COMBOBOX_SELECTED,self.path_input.GetId()))
727 """Event: Selection dropdown changed.""" 728 path_input_str = self.path_input.GetValue()
731 if len(path_input_str) > 0:
733 path = path_input_str.split(
':')[0]
744 self.is_button.Enable()
750 pos = self.ud_txt.HitTestPos(wx.Point(0,0))
751 sel = self.ud_txt.GetSelection()
755 for (k,v)
in container._local_data._data.iteritems():
756 ud_str += str(k)+
": " 759 if vstr.find(
'\n') != -1:
764 self.ud_txt.SetValue(ud_str)
767 self.ud_txt.ShowPosition(pos[1])
769 self.ud_txt.SetSelection(sel[0],sel[1])
772 self.is_button.Disable()
775 """Update the structure of the SMACH plan (re-generate the dotcode).""" 783 pathsplit = path.split(
'/')
784 parent_path =
'/'.join(pathsplit[0:-1])
786 rospy.logdebug(
"RECEIVED: "+path)
787 rospy.logdebug(
"CONTAINERS: "+str(self._containers.keys()))
793 rospy.logdebug(
"UPDATING: "+path)
796 needs_redraw = self.
_containers[path].update_structure(msg)
798 rospy.logdebug(
"CONSTRUCTING: "+path)
805 if parent_path ==
'':
809 self.path_combo.Append(path)
810 self.path_input.Append(path)
821 self._update_cond.notify_all()
824 """Process status messages.""" 836 rospy.logdebug(
"STATUS MSG: "+path)
842 if container.update_status(msg):
844 self._update_cond.notify_all()
847 path_input_str = self.path_input.GetValue()
850 self.path_input.GetEventHandler(),
851 wx.CommandEvent(wx.wxEVT_COMMAND_COMBOBOX_SELECTED,self.path_input.GetId()))
854 """This thread continuously updates the graph when it changes. 856 The graph gets updated in one of two ways: 858 1: The structure of the SMACH plans has changed, or the display 859 settings have been changed. In this case, the dotcode needs to be 862 2: The status of the SMACH plans has changed. In this case, we only 863 need to change the styles of the graph. 868 self._update_cond.wait()
871 containers_to_update = {}
875 elif self.
_path ==
'/':
882 dotstr =
"digraph {\n\t" 885 "outputmode=nodesfirst",
900 for path,tc
in containers_to_update.iteritems():
901 dotstr += tc.get_dotcode(
908 dotstr +=
'"__empty__" [label="Path not available.", shape="plaintext"]' 917 for path,tc
in containers_to_update.iteritems():
921 self.widget.items_by_url,
922 self.widget.subgraph_shapes,
926 self.widget.Refresh()
929 """Set the xdot view's dotcode and refresh the display.""" 931 if self.widget.set_dotcode(dotcode,
None):
932 self.SetTitle(
'Smach Viewer')
935 self.widget.zoom_to_fit()
939 wx.PostEvent(self.GetEventHandler(), wx.IdleEvent())
942 """Update the tree view.""" 945 self._update_cond.wait()
946 self.tree.DeleteAllItems()
948 for path,tc
in self._top_containers.iteritems():
952 """Add a path to the tree view.""" 954 container = self.tree.AddRoot(
get_label(path))
956 container = self.tree.AppendItem(parent,
get_label(path))
960 child_path =
'/'.join([path,label])
961 if child_path
in self._containers.keys():
964 self.tree.AppendItem(container,label)
967 """Append an item to the tree view.""" 969 node = self.tree.AddRoot(container._label)
970 for child_label
in container._children:
971 self.tree.AppendItem(node,child_label)
974 """Event: On Idle, refresh the display if necessary, then un-set the flag.""" 981 """Update the list of known SMACH introspection servers.""" 984 server_names = self._client.get_servers()
985 new_server_names = [sn
for sn
in server_names
if sn
not in self.
_status_subs]
988 for server_name
in new_server_names:
990 server_name+smach_ros.introspection.STRUCTURE_TOPIC,
991 SmachContainerStructure,
993 callback_args = server_name,
997 server_name+smach_ros.introspection.STATUS_TOPIC,
998 SmachContainerStatus,
1015 dial = wx.MessageDialog(
None,
1016 "Pan: Arrow Keys\nZoom: PageUp / PageDown\nZoom To Fit: F\nRefresh: R",
1017 'Keyboard Controls', wx.OK)
1021 timestr = time.strftime(
"%Y%m%d-%H%M%S")
1022 directory = rospkg.get_ros_home()+
'/dotfiles/' 1023 if not os.path.exists(directory):
1024 os.makedirs(directory)
1025 filename = directory+timestr+
'.dot' 1026 print(
'Writing to file: %s' % filename)
1027 with open(filename,
'w')
as f:
1034 self.widget.set_filter(filter)
1037 from argparse
import ArgumentParser
1038 p = ArgumentParser()
1039 p.add_argument(
'-f',
'--auto-focus',
1040 action=
'store_true',
1041 help=
"Enable 'AutoFocus to subgraph' as default",
1042 dest=
'enable_auto_focus')
1043 args = p.parse_args()
1047 frame.set_filter(
'dot')
1051 if args.enable_auto_focus:
1052 frame.toggle_auto_focus(
None)
1056 if __name__ ==
'__main__':
1057 rospy.init_node(
'smach_viewer',anonymous=
False, disable_signals=
True,log_level=rospy.INFO)
1058 sys.argv = rospy.myargv()
def _update_server_list(self)
def append_tree(self, container, parent=None)
def select_cb(self, item, event)
def update_structure(self, msg)
def set_depth(self, event)
def get_dotcode(self, selected_paths, closed_paths, depth, max_depth, containers, show_all, label_wrapper, attrs={})
def __init__(self, server_name, msg)
def on_set_initial_state(self, event)
def update_status(self, msg)
def add_to_tree(self, path, parent)
def SaveDotGraph(self, event)
def graph_attr_string(attrs)
Helper Functions.
def import_non_local(name, custom_name=None)
this import system (or ros-released) xdot import xdot need to import currnt package, but not to load this file http://stackoverflow.com/questions/6031584/importing-from-builtin-library-when-module-with-same-name-exists
def set_dotcode(self, dotcode, zoom=True)
def get_parent_path(path)
def _status_msg_update(self, msg)
def _set_path(self, path)
def set_path(self, event)
def toggle_all_transitions(self, event)
def set_styles(self, selected_paths, depth, max_depth, items, subgraph_shapes, containers)
def set_filter(self, filter)
def _set_max_depth(self, max_depth)
def ShowControlsDialog(self, event)
def _structure_msg_update(self, msg, server_name)
def set_label_width(self, event)
def toggle_auto_focus(self, event)
def selection_changed(self, event)