smach_viewer.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 
00003 # Copyright (c) 2010, Willow Garage, Inc.
00004 # All rights reserved.
00005 # Copyright (c) 2013, Jonathan Bohren, The Johns Hopkins University
00006 # 
00007 # Redistribution and use in source and binary forms, with or without
00008 # modification, are permitted provided that the following conditions 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 copyright
00013 #       notice, this list of conditions and the following disclaimer in the
00014 #       documentation and/or other materials provided with the distribution.
00015 #   * Neither the name of the Willow Garage, Inc. nor the names of its
00016 #       contributors may be used to endorse or promote products derived from
00017 #       this software without specific prior written permission.
00018 # 
00019 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
00020 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
00021 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
00022 # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
00023 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
00024 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
00025 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
00026 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
00027 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
00028 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
00029 # POSSIBILITY OF SUCH DAMAGE.
00030 #
00031 # Author: Jonathan Bohren 
00032 
00033 import rospy
00034 import rospkg
00035 
00036 from smach_msgs.msg import SmachContainerStatus,SmachContainerInitialStatusCmd,SmachContainerStructure
00037 
00038 import sys
00039 import os
00040 import threading
00041 import pickle
00042 import pprint
00043 import copy
00044 import StringIO
00045 import colorsys
00046 import time
00047 
00048 import wxversion
00049 if wxversion.checkInstalled("2.8"):
00050     wxversion.select("2.8")
00051 else:
00052     print("wxversion 2.8 is not installed, installed versions are {}".format(wxversion.getInstalled()))
00053 import wx
00054 import wx.richtext
00055 
00056 import textwrap
00057 
00058 ## this import system (or ros-released) xdot
00059 # import xdot
00060 ## need to import currnt package, but not to load this file
00061 # http://stackoverflow.com/questions/6031584/importing-from-builtin-library-when-module-with-same-name-exists
00062 def import_non_local(name, custom_name=None):
00063     import imp, sys
00064 
00065     custom_name = custom_name or name
00066 
00067     path = filter(lambda x: x != os.path.dirname(os.path.abspath(__file__)), sys.path)
00068     f, pathname, desc = imp.find_module(name, path)
00069 
00070     module = imp.load_module(custom_name, f, pathname, desc)
00071     if f:
00072         f.close()
00073 
00074     return module
00075 
00076 smach_viewer = import_non_local('smach_viewer')
00077 from smach_viewer import xdot
00078 ##
00079 import smach
00080 import smach_ros
00081 
00082 ### Helper Functions
00083 def graph_attr_string(attrs):
00084     """Generate an xdot graph attribute string."""
00085     attrs_strs = ['"'+str(k)+'"="'+str(v)+'"' for k,v in attrs.iteritems()]
00086     return ';\n'.join(attrs_strs)+';\n'
00087 
00088 def attr_string(attrs):
00089     """Generate an xdot node attribute string."""
00090     attrs_strs = ['"'+str(k)+'"="'+str(v)+'"' for k,v in attrs.iteritems()]
00091     return ' ['+(', '.join(attrs_strs))+']'
00092 
00093 def get_parent_path(path):
00094     """Get the parent path of an xdot node."""
00095     path_tokens = path.split('/')
00096     if len(path_tokens) > 2:
00097         parent_path = '/'.join(path_tokens[0:-1])
00098     else:
00099         parent_path = '/'.join(path_tokens[0:1])
00100     return parent_path
00101 
00102 def get_label(path):
00103     """Get the label of an xdot node."""
00104     path_tokens = path.split('/')
00105     return path_tokens[-1]
00106 
00107 def hex2t(color_str):
00108     """Convert a hexadecimal color strng into a color tuple."""
00109     color_tuple = [int(color_str[i:i+2],16)/255.0    for i in range(1,len(color_str),2)]
00110     return color_tuple
00111 
00112 class ContainerNode():
00113     """
00114     This class represents a given container in a running SMACH system. 
00115 
00116     Its primary use is to generate dotcode for a SMACH container. It has
00117     methods for responding to structure and status messages from a SMACH
00118     introspection server, as well as methods for updating the styles of a 
00119     graph once it's been drawn.
00120     """
00121     def __init__(self, server_name, msg):
00122         # Store path info
00123         self._server_name = server_name
00124         self._path = msg.path
00125         splitpath = msg.path.split('/')
00126         self._label = splitpath[-1]
00127         self._dir = '/'.join(splitpath[0:-1])
00128 
00129         self._children = msg.children
00130         self._internal_outcomes = msg.internal_outcomes
00131         self._outcomes_from = msg.outcomes_from
00132         self._outcomes_to = msg.outcomes_to
00133 
00134         self._container_outcomes = msg.container_outcomes
00135 
00136         # Status
00137         self._initial_states = []
00138         self._active_states = []
00139         self._last_active_states = []
00140         self._local_data = smach.UserData()
00141         self._info = ''
00142 
00143     def update_structure(self, msg):
00144         """Update the structure of this container from a given message. Return True if anything changes."""
00145         needs_update = False
00146 
00147         if self._children != msg.children\
00148                 or self._internal_outcomes != msg.internal_outcomes\
00149                 or self._outcomes_from != msg.outcomes_from\
00150                 or self._outcomes_to != msg.outcomes_to\
00151                 or self._container_outcomes != msg.container_outcomes:
00152             needs_update = True
00153 
00154         if needs_update:
00155             self._children = msg.children
00156             self._internal_outcomes = msg.internal_outcomes
00157             self._outcomes_from = msg.outcomes_from
00158             self._outcomes_to = msg.outcomes_to
00159 
00160             self._container_outcomes = msg.container_outcomes
00161 
00162         return needs_update
00163 
00164     def update_status(self, msg):
00165         """Update the known userdata and active state set and return True if the graph needs to be redrawn."""
00166 
00167         # Initialize the return value
00168         needs_update = False
00169 
00170         # Check if the initial states or active states have changed
00171         if set(msg.initial_states) != set(self._initial_states):
00172             self._structure_changed = True
00173             needs_update = True
00174         if set(msg.active_states) != set(self._active_states):
00175             needs_update = True
00176 
00177         # Store the initial and active states
00178         self._initial_states = msg.initial_states
00179         self._last_active_states = self._active_states
00180         self._active_states = msg.active_states
00181 
00182         # Unpack the user data
00183         while not rospy.is_shutdown():
00184             try:
00185                 self._local_data._data = pickle.loads(msg.local_data)
00186                 break
00187             except ImportError as ie:
00188                 # This will only happen once for each package
00189                 modulename = ie.args[0][16:]
00190                 packagename = modulename[0:modulename.find('.')]
00191                 roslib.load_manifest(packagename)
00192                 self._local_data._data = pickle.loads(msg.local_data)
00193 
00194         # Store the info string
00195         self._info = msg.info
00196 
00197         return needs_update
00198 
00199     def get_dotcode(self, selected_paths, closed_paths, depth, max_depth, containers, show_all, label_wrapper, attrs={}):
00200         """Generate the dotcode representing this container.
00201         
00202         @param selected_paths: The paths to nodes that are selected
00203         @closed paths: The paths that shouldn't be expanded
00204         @param depth: The depth to start traversing the tree
00205         @param max_depth: The depth to which we should traverse the tree
00206         @param containers: A dict of containers keyed by their paths
00207         @param show_all: True if implicit transitions should be shown
00208         @param label_wrapper: A text wrapper for wrapping element names
00209         @param attrs: A dict of dotcode attributes for this cluster
00210         """
00211 
00212         dotstr = 'subgraph "cluster_%s" {\n' % (self._path)
00213         if depth == 0:
00214             #attrs['style'] = 'filled,rounded'
00215             attrs['color'] = '#00000000'
00216             attrs['fillcolor'] = '#0000000F'
00217         #attrs['rank'] = 'max'
00218 
00219         #,'succeeded','aborted','preempted'attrs['label'] = self._label
00220         dotstr += graph_attr_string(attrs)
00221 
00222         # Add start/terimate target
00223         proxy_attrs = {
00224                 'URL':self._path,
00225                 'shape':'plaintext',
00226                 'color':'gray',
00227                 'fontsize':'18',
00228                 'fontweight':'18',
00229                 'rank':'min',
00230                 'height':'0.01'}
00231         proxy_attrs['label'] = '\\n'.join(label_wrapper.wrap(self._label))
00232         dotstr += '"%s" %s;\n' % (
00233                 '/'.join([self._path,'__proxy__']),
00234                 attr_string(proxy_attrs))
00235 
00236         # Check if we should expand this container
00237         if max_depth == -1 or depth <= max_depth:
00238             # Add container outcomes
00239             dotstr += 'subgraph "cluster_%s" {\n' % '/'.join([self._path,'__outcomes__'])
00240             outcomes_attrs = {
00241                     'style':'rounded,filled',
00242                     'rank':'sink',
00243                     'color':'#FFFFFFFF',#'#871C34',
00244                     'fillcolor':'#FFFFFF00'#'#FE464f3F'#'#DB889A'
00245                     }
00246             dotstr += graph_attr_string(outcomes_attrs)
00247 
00248             for outcome_label in self._container_outcomes:
00249                 outcome_path = ':'.join([self._path,outcome_label])
00250                 outcome_attrs = {
00251                         'shape':'box',
00252                         'height':'0.3',
00253                         'style':'filled,rounded',
00254                         'fontsize':'12',
00255                         'fillcolor':'#FE464f',#'#EDC2CC',
00256                         'color':'#780006',#'#EBAEBB',
00257                         'fontcolor':'#780006',#'#EBAEBB',
00258                         'label':'\\n'.join(label_wrapper.wrap(outcome_label)),
00259                         'URL':':'.join([self._path,outcome_label])
00260                         }
00261                 dotstr += '"%s" %s;\n' % (outcome_path,attr_string(outcome_attrs))
00262             dotstr += "}\n"
00263 
00264             # Iterate over children
00265             for child_label in self._children:
00266                 child_attrs = {
00267                         'style':'filled,setlinewidth(2)',
00268                         'color':'#000000FF',
00269                         'fillcolor':'#FFFFFF00'
00270                         }
00271 
00272                 child_path = '/'.join([self._path,child_label])
00273                 # Generate dotcode for children
00274                 if child_path in containers:
00275                     child_attrs['style'] += ',rounded'
00276 
00277                     dotstr += containers[child_path].get_dotcode(
00278                             selected_paths,
00279                             closed_paths,
00280                             depth+1, max_depth,
00281                             containers,
00282                             show_all,
00283                             label_wrapper,
00284                             child_attrs)
00285                 else:
00286                     child_attrs['label'] = '\\n'.join(label_wrapper.wrap(child_label))
00287                     child_attrs['URL'] = child_path
00288                     dotstr += '"%s" %s;\n' % (child_path, attr_string(child_attrs))
00289 
00290             # Iterate over edges
00291             internal_edges = zip(
00292                     self._internal_outcomes,
00293                     self._outcomes_from,
00294                     self._outcomes_to)
00295 
00296             # Add edge from container label to initial state
00297             internal_edges += [('','__proxy__',initial_child) for initial_child in self._initial_states]
00298 
00299             has_explicit_transitions = []
00300             for (outcome_label,from_label,to_label) in internal_edges:
00301                 if to_label != 'None' or outcome_label == to_label:
00302                     has_explicit_transitions.append(from_label)
00303 
00304             # Draw internal edges
00305             for (outcome_label,from_label,to_label) in internal_edges:
00306 
00307                 from_path = '/'.join([self._path, from_label])
00308 
00309                 if show_all \
00310                         or to_label != 'None'\
00311                         or from_label not in has_explicit_transitions \
00312                         or (outcome_label == from_label) \
00313                         or from_path in containers:
00314                     # Set the implicit target of this outcome
00315                     if to_label == 'None':
00316                         to_label = outcome_label
00317 
00318                     to_path = '/'.join([self._path, to_label])
00319 
00320                     edge_attrs = {
00321                             'URL':':'.join([from_path,outcome_label,to_path]),
00322                             'fontsize':'12',
00323                             'label':'\\n'.join(label_wrapper.wrap(outcome_label))}
00324                     edge_attrs['style'] = 'setlinewidth(2)'
00325 
00326                     # Hide implicit
00327                     #if not show_all and to_label == outcome_label:
00328                     #    edge_attrs['style'] += ',invis'
00329 
00330                     from_key = '"%s"' % from_path
00331                     if from_path in containers:
00332                         if max_depth == -1 or depth+1 <= max_depth:
00333                             from_key = '"%s:%s"' % ( from_path, outcome_label)
00334                         else:
00335                             edge_attrs['ltail'] = 'cluster_'+from_path
00336                             from_path = '/'.join([from_path,'__proxy__'])
00337                             from_key = '"%s"' % ( from_path )
00338 
00339                     to_key = ''
00340                     if to_label in self._container_outcomes:
00341                         to_key = '"%s:%s"' % (self._path,to_label)
00342                         edge_attrs['color'] = '#00000055'# '#780006'
00343                     else:
00344                         if to_path in containers:
00345                             edge_attrs['lhead'] = 'cluster_'+to_path
00346                             to_path = '/'.join([to_path,'__proxy__'])
00347                         to_key = '"%s"' % to_path
00348 
00349                     dotstr += '%s -> %s %s;\n' % (
00350                             from_key, to_key, attr_string(edge_attrs))
00351 
00352         dotstr += '}\n'
00353         return dotstr
00354 
00355     def set_styles(self, selected_paths, depth, max_depth, items, subgraph_shapes, containers):
00356         """Update the styles for a list of containers without regenerating the dotcode.
00357 
00358         This function is called recursively to update an entire tree.
00359         
00360         @param selected_paths: A list of paths to nodes that are currently selected.
00361         @param depth: The depth to start traversing the tree
00362         @param max_depth: The depth to traverse into the tree
00363         @param items: A dict of all the graph items, keyed by url
00364         @param subgraph_shapes: A dictionary of shapes from the rendering engine
00365         @param containers: A dict of all the containers
00366         """
00367 
00368         # Color root container
00369         """
00370         if depth == 0:
00371             container_shapes = subgraph_shapes['cluster_'+self._path]
00372             container_color = (0,0,0,0)
00373             container_fillcolor = (0,0,0,0)
00374 
00375             for shape in container_shapes:
00376                 shape.pen.color = container_color
00377                 shape.pen.fillcolor = container_fillcolor
00378                 """
00379 
00380         # Color shapes for outcomes
00381 
00382         # Color children
00383         if max_depth == -1 or depth <= max_depth:
00384             # Iterate over children
00385             for child_label in self._children:
00386                 child_path = '/'.join([self._path,child_label])
00387 
00388                 child_color = [0.5,0.5,0.5,1]
00389                 child_fillcolor = [1,1,1,1]
00390                 child_linewidth = 2
00391 
00392                 active_color = hex2t('#5C7600FF')
00393                 active_fillcolor = hex2t('#C0F700FF')
00394 
00395                 initial_color = hex2t('#000000FF')
00396                 initial_fillcolor = hex2t('#FFFFFFFF')
00397 
00398                 if child_label in self._active_states:
00399                     # Check if the child is active
00400                     child_color = active_color
00401                     child_fillcolor = active_fillcolor
00402                     child_linewidth = 5
00403                 elif child_label in self._initial_states:
00404                     # Initial style
00405                     #child_fillcolor = initial_fillcolor
00406                     child_color = initial_color
00407                     child_linewidth = 2
00408 
00409                 # Check if the child is selected
00410                 if child_path in selected_paths:
00411                     child_color = hex2t('#FB000DFF')
00412 
00413                 # Generate dotcode for child containers 
00414                 if child_path in containers:
00415                     subgraph_id = 'cluster_'+child_path
00416                     if subgraph_id in subgraph_shapes:
00417                         if child_label in self._active_states:
00418                             child_fillcolor[3] = 0.25
00419                         elif 0 and child_label in self._initial_states:
00420                             child_fillcolor[3] = 0.25
00421                         else:
00422                             if max_depth > 0:
00423                                 v = 1.0-0.25*((depth+1)/float(max_depth))
00424                             else:
00425                                 v = 0.85
00426                             child_fillcolor = [v,v,v,1.0]
00427 
00428                         
00429                         for shape in subgraph_shapes['cluster_'+child_path]:
00430                             pen = shape.pen
00431                             if len(pen.color) > 3:
00432                                 pen_color_opacity = pen.color[3]
00433                                 if pen_color_opacity < 0.01:
00434                                     pen_color_opacity = 0
00435                             else:
00436                                 pen_color_opacity = 0.5
00437                             shape.pen.color = child_color[0:3]+[pen_color_opacity]
00438                             shape.pen.fillcolor = [child_fillcolor[i] for i in range(min(3,len(pen.fillcolor)))]
00439                             shape.pen.linewidth = child_linewidth
00440 
00441                         # Recurse on this child
00442                         containers[child_path].set_styles(
00443                                 selected_paths,
00444                                 depth+1, max_depth,
00445                                 items,
00446                                 subgraph_shapes,
00447                                 containers)
00448                 else:
00449                     if child_path in items:
00450                         for shape in items[child_path].shapes:
00451                             if not isinstance(shape,xdot.xdot.TextShape):
00452                                 shape.pen.color = child_color
00453                                 shape.pen.fillcolor = child_fillcolor
00454                                 shape.pen.linewidth = child_linewidth
00455                     else:
00456                         #print child_path+" NOT IN "+str(items.keys())
00457                         pass
00458 
00459 class SmachViewerFrame(wx.Frame):
00460     """
00461     This class provides a GUI application for viewing SMACH plans.
00462     """
00463     def __init__(self):
00464         wx.Frame.__init__(self, None, -1, "Smach Viewer", size=(720,480))
00465 
00466         # Create graph
00467         self._containers = {}
00468         self._top_containers = {}
00469         self._update_cond = threading.Condition()
00470         self._needs_refresh = True
00471         self.dotstr = ''
00472 
00473         vbox = wx.BoxSizer(wx.VERTICAL)
00474 
00475 
00476         # Create Splitter
00477         self.content_splitter = wx.SplitterWindow(self, -1,style = wx.SP_LIVE_UPDATE)
00478         self.content_splitter.SetMinimumPaneSize(24)
00479         self.content_splitter.SetSashGravity(0.85)
00480 
00481 
00482         # Create viewer pane
00483         viewer = wx.Panel(self.content_splitter,-1)
00484 
00485         # Create smach viewer 
00486         nb = wx.Notebook(viewer,-1,style=wx.NB_TOP | wx.WANTS_CHARS)
00487         viewer_box = wx.BoxSizer()
00488         viewer_box.Add(nb,1,wx.EXPAND | wx.ALL, 4)
00489         viewer.SetSizer(viewer_box)
00490 
00491         # Create graph view
00492         graph_view = wx.Panel(nb,-1)
00493         gv_vbox = wx.BoxSizer(wx.VERTICAL)
00494         graph_view.SetSizer(gv_vbox)
00495 
00496         # Construct toolbar
00497         toolbar = wx.ToolBar(graph_view, -1)
00498 
00499         toolbar.AddControl(wx.StaticText(toolbar,-1,"Path: "))
00500 
00501         # Path list
00502         self.path_combo = wx.ComboBox(toolbar, -1, style=wx.CB_DROPDOWN)
00503         self.path_combo .Bind(wx.EVT_COMBOBOX, self.set_path)
00504         self.path_combo.Append('/')
00505         self.path_combo.SetValue('/')
00506         toolbar.AddControl(self.path_combo)
00507 
00508         # Depth spinner
00509         self.depth_spinner = wx.SpinCtrl(toolbar, -1,
00510                 size=wx.Size(50,-1),
00511                 min=-1,
00512                 max=1337,
00513                 initial=-1)
00514         self.depth_spinner.Bind(wx.EVT_SPINCTRL,self.set_depth)
00515         self._max_depth = -1
00516         toolbar.AddControl(wx.StaticText(toolbar,-1,"    Depth: "))
00517         toolbar.AddControl(self.depth_spinner)
00518 
00519         # Label width spinner
00520         self.width_spinner = wx.SpinCtrl(toolbar, -1,
00521                 size=wx.Size(50,-1),
00522                 min=1,
00523                 max=1337,
00524                 initial=40)
00525         self.width_spinner.Bind(wx.EVT_SPINCTRL,self.set_label_width)
00526         self._label_wrapper = textwrap.TextWrapper(40,break_long_words=True)
00527         toolbar.AddControl(wx.StaticText(toolbar,-1,"    Label Width: "))
00528         toolbar.AddControl(self.width_spinner)
00529 
00530         # Implicit transition display
00531         toggle_all = wx.ToggleButton(toolbar,-1,'Show Implicit')
00532         toggle_all.Bind(wx.EVT_TOGGLEBUTTON, self.toggle_all_transitions)
00533         self._show_all_transitions = False
00534 
00535         toolbar.AddControl(wx.StaticText(toolbar,-1,"    "))
00536         toolbar.AddControl(toggle_all)
00537 
00538         toggle_auto_focus = wx.ToggleButton(toolbar, -1, 'Auto Focus')
00539         toggle_auto_focus.Bind(wx.EVT_TOGGLEBUTTON, self.toggle_auto_focus)
00540         self._auto_focus = False
00541 
00542         toolbar.AddControl(wx.StaticText(toolbar, -1, "    "))
00543         toolbar.AddControl(toggle_auto_focus)
00544 
00545         toolbar.AddControl(wx.StaticText(toolbar,-1,"    "))
00546         toolbar.AddLabelTool(wx.ID_HELP, 'Help',
00547                 wx.ArtProvider.GetBitmap(wx.ART_HELP,wx.ART_OTHER,(16,16)) )
00548         toolbar.AddLabelTool(wx.ID_SAVE, 'Save',
00549                 wx.ArtProvider.GetBitmap(wx.ART_FILE_SAVE,wx.ART_OTHER,(16,16)) )
00550         toolbar.Realize()
00551 
00552         self.Bind(wx.EVT_TOOL, self.ShowControlsDialog, id=wx.ID_HELP)
00553         self.Bind(wx.EVT_TOOL, self.SaveDotGraph, id=wx.ID_SAVE)
00554 
00555         # Create dot graph widget
00556         self.widget = xdot.wxxdot.WxDotWindow(graph_view, -1)
00557 
00558         gv_vbox.Add(toolbar, 0, wx.EXPAND)
00559         gv_vbox.Add(self.widget, 1, wx.EXPAND)
00560 
00561         # Create tree view widget
00562         self.tree = wx.TreeCtrl(nb,-1,style=wx.TR_HAS_BUTTONS)
00563         nb.AddPage(graph_view,"Graph View")
00564         nb.AddPage(self.tree,"Tree View")
00565 
00566 
00567         # Create userdata widget
00568         borders = wx.LEFT | wx.RIGHT | wx.TOP
00569         border = 4
00570         self.ud_win = wx.ScrolledWindow(self.content_splitter, -1)
00571         self.ud_gs = wx.BoxSizer(wx.VERTICAL)
00572 
00573         self.ud_gs.Add(wx.StaticText(self.ud_win,-1,"Path:"),0, borders, border)
00574 
00575         self.path_input = wx.ComboBox(self.ud_win,-1,style=wx.CB_DROPDOWN)
00576         self.path_input.Bind(wx.EVT_COMBOBOX,self.selection_changed)
00577         self.ud_gs.Add(self.path_input,0,wx.EXPAND | borders, border)
00578 
00579 
00580         self.ud_gs.Add(wx.StaticText(self.ud_win,-1,"Userdata:"),0, borders, border)
00581 
00582         self.ud_txt = wx.TextCtrl(self.ud_win,-1,style=wx.TE_MULTILINE | wx.TE_READONLY)
00583         self.ud_gs.Add(self.ud_txt,1,wx.EXPAND | borders, border)
00584         
00585         # Add initial state button
00586         self.is_button = wx.Button(self.ud_win,-1,"Set as Initial State")
00587         self.is_button.Bind(wx.EVT_BUTTON, self.on_set_initial_state)
00588         self.is_button.Disable()
00589         self.ud_gs.Add(self.is_button,0,wx.EXPAND | wx.BOTTOM | borders, border)
00590 
00591         self.ud_win.SetSizer(self.ud_gs)
00592 
00593 
00594         # Set content splitter
00595         self.content_splitter.SplitVertically(viewer, self.ud_win, 512)
00596 
00597         # Add statusbar
00598         self.statusbar = wx.StatusBar(self,-1)
00599 
00600         # Add elements to sizer
00601         vbox.Add(self.content_splitter, 1, wx.EXPAND | wx.ALL)
00602         vbox.Add(self.statusbar, 0, wx.EXPAND)
00603 
00604         self.SetSizer(vbox)
00605         self.Center()
00606 
00607         # smach introspection client
00608         self._client = smach_ros.IntrospectionClient()
00609         self._containers= {}
00610         self._selected_paths = []
00611 
00612         # Message subscribers
00613         self._structure_subs = {}
00614         self._status_subs = {}
00615 
00616         self.Bind(wx.EVT_IDLE,self.OnIdle)
00617         self.Bind(wx.EVT_CLOSE,self.OnQuit)
00618 
00619         # Register mouse event callback
00620         self.widget.register_select_callback(self.select_cb)
00621         self._path = '/'
00622         self._needs_zoom = True
00623         self._structure_changed = True
00624 
00625         # Start a thread in the background to update the server list
00626         self._keep_running = True
00627         self._server_list_thread = threading.Thread(target=self._update_server_list)
00628         self._server_list_thread.start()
00629 
00630         self._update_graph_thread = threading.Thread(target=self._update_graph)
00631         self._update_graph_thread.start()
00632         self._update_tree_thread = threading.Thread(target=self._update_tree)
00633         self._update_tree_thread.start()
00634 
00635     def OnQuit(self,event):
00636         """Quit Event: kill threads and wait for join."""
00637         with self._update_cond:
00638             self._keep_running = False
00639             self._update_cond.notify_all()
00640 
00641         self._server_list_thread.join()
00642         self._update_graph_thread.join()
00643         self._update_tree_thread.join()
00644         
00645         event.Skip()
00646 
00647     def update_graph(self):
00648         """Notify all that the graph needs to be updated."""
00649         with self._update_cond:
00650             self._update_cond.notify_all()
00651 
00652     def on_set_initial_state(self, event):
00653         """Event: Change the initial state of the server."""
00654         state_path = self._selected_paths[0]
00655         parent_path = get_parent_path(state_path)
00656         state = get_label(state_path)
00657 
00658         server_name = self._containers[parent_path]._server_name
00659         self._client.set_initial_state(server_name,parent_path,[state],timeout = rospy.Duration(60.0))
00660 
00661     def set_path(self, event):
00662         """Event: Change the viewable path and update the graph."""
00663         self._path = self.path_combo.GetValue()
00664         self._needs_zoom = True
00665         self.update_graph()
00666 
00667     def _set_path(self, path):
00668         self._path = path
00669         self._needs_zoom = True
00670         self.path_combo.SetValue(path)
00671         self.update_graph()
00672 
00673     def set_depth(self, event):
00674         """Event: Change the maximum depth and update the graph."""
00675         self._max_depth = self.depth_spinner.GetValue()
00676         self._needs_zoom = True
00677         self.update_graph()
00678 
00679     def _set_max_depth(self, max_depth):
00680         self._max_depth = max_depth
00681         self.depth_spinner.SetValue(max_depth)
00682         self._needs_zoom = True
00683         self.update_graph()
00684 
00685     def set_label_width(self, event):
00686         """Event: Change the label wrapper width and update the graph."""
00687         self._label_wrapper.width = self.width_spinner.GetValue()
00688         self._needs_zoom = True
00689         self.update_graph()
00690 
00691     def toggle_all_transitions(self, event):
00692         """Event: Change whether automatic transitions are hidden and update the graph."""
00693         self._show_all_transitions = not self._show_all_transitions
00694         self._structure_changed = True
00695         self.update_graph()
00696 
00697     def toggle_auto_focus(self, event):
00698         """Event: Enable/Disable automatically focusing"""
00699         self._auto_focus = not self._auto_focus
00700         self._needs_zoom = self._auto_focus
00701         self._structure_changed = True
00702         if not self._auto_focus:
00703             self._set_path('/')
00704             self._max_depth(-1)
00705         self.update_graph()
00706 
00707     def select_cb(self, item, event):
00708         """Event: Click to select a graph node to display user data and update the graph."""
00709 
00710         # Only set string status
00711         if not type(item.url) is str:
00712             return
00713 
00714         self.statusbar.SetStatusText(item.url)
00715         # Left button-up
00716         if event.ButtonUp(wx.MOUSE_BTN_LEFT):
00717             # Store this item's url as the selected path
00718             self._selected_paths = [item.url]
00719             # Update the selection dropdown
00720             self.path_input.SetValue(item.url)
00721             wx.PostEvent(
00722                     self.path_input.GetEventHandler(),
00723                     wx.CommandEvent(wx.wxEVT_COMMAND_COMBOBOX_SELECTED,self.path_input.GetId()))
00724             self.update_graph()
00725 
00726     def selection_changed(self, event):
00727         """Event: Selection dropdown changed."""
00728         path_input_str = self.path_input.GetValue()
00729 
00730         # Check the path is non-zero length
00731         if len(path_input_str) > 0:
00732             # Split the path (state:outcome), and get the state path
00733             path = path_input_str.split(':')[0]
00734 
00735             # Get the container corresponding to this path, since userdata is
00736             # stored in the containers
00737             if path not in self._containers:
00738                 parent_path = get_parent_path(path)
00739             else:
00740                 parent_path = path
00741 
00742             if parent_path in self._containers:
00743                 # Enable the initial state button for the selection
00744                 self.is_button.Enable()
00745 
00746                 # Get the container
00747                 container = self._containers[parent_path]
00748 
00749                 # Store the scroll position and selection
00750                 pos = self.ud_txt.HitTestPos(wx.Point(0,0))
00751                 sel = self.ud_txt.GetSelection()
00752 
00753                 # Generate the userdata string
00754                 ud_str = ''
00755                 for (k,v) in container._local_data._data.iteritems():
00756                     ud_str += str(k)+": "
00757                     vstr = str(v)
00758                     # Add a line break if this is a multiline value
00759                     if vstr.find('\n') != -1:
00760                         ud_str += '\n'
00761                     ud_str+=vstr+'\n\n'
00762 
00763                 # Set the userdata string
00764                 self.ud_txt.SetValue(ud_str)
00765 
00766                 # Restore the scroll position and selection
00767                 self.ud_txt.ShowPosition(pos[1])
00768                 if sel != (0,0):
00769                     self.ud_txt.SetSelection(sel[0],sel[1])
00770             else:
00771                 # Disable the initial state button for this selection
00772                 self.is_button.Disable()
00773 
00774     def _structure_msg_update(self, msg, server_name):
00775         """Update the structure of the SMACH plan (re-generate the dotcode)."""
00776 
00777         # Just return if we're shutting down
00778         if not self._keep_running:
00779             return
00780 
00781         # Get the node path
00782         path = msg.path
00783         pathsplit = path.split('/')
00784         parent_path = '/'.join(pathsplit[0:-1])
00785 
00786         rospy.logdebug("RECEIVED: "+path)
00787         rospy.logdebug("CONTAINERS: "+str(self._containers.keys()))
00788 
00789         # Initialize redraw flag
00790         needs_redraw = False
00791 
00792         if path in self._containers:
00793             rospy.logdebug("UPDATING: "+path)
00794 
00795             # Update the structure of this known container
00796             needs_redraw = self._containers[path].update_structure(msg)
00797         else: 
00798             rospy.logdebug("CONSTRUCTING: "+path)
00799 
00800             # Create a new container
00801             container = ContainerNode(server_name, msg)
00802             self._containers[path] = container
00803 
00804             # Store this as a top container if it has no parent
00805             if parent_path == '':
00806                 self._top_containers[path] = container
00807 
00808             # Append paths to selector
00809             self.path_combo.Append(path)
00810             self.path_input.Append(path)
00811 
00812             # We need to redraw thhe graph if this container's parent is already known
00813             if parent_path in self._containers:
00814                 needs_redraw = True
00815 
00816         # Update the graph if necessary
00817         if needs_redraw:
00818             with self._update_cond:
00819                 self._structure_changed = True
00820                 self._needs_zoom = True # TODO: Make it so you can disable this
00821                 self._update_cond.notify_all()
00822 
00823     def _status_msg_update(self, msg):
00824         """Process status messages."""
00825 
00826         # Check if we're in the process of shutting down
00827         if not self._keep_running:
00828             return
00829 
00830         if self._auto_focus and len(msg.info) > 0:
00831             self._set_path(msg.info)
00832             self._set_max_depth(msg.info.count('/')-1)
00833 
00834         # Get the path to the updating conainer
00835         path = msg.path
00836         rospy.logdebug("STATUS MSG: "+path)
00837 
00838         # Check if this is a known container
00839         if path in self._containers:
00840             # Get the container and check if the status update requires regeneration
00841             container = self._containers[path]
00842             if container.update_status(msg):
00843                 with self._update_cond:
00844                     self._update_cond.notify_all()
00845 
00846             # TODO: Is this necessary?
00847             path_input_str = self.path_input.GetValue()
00848             if path_input_str == path or get_parent_path(path_input_str) == path:
00849                 wx.PostEvent(
00850                         self.path_input.GetEventHandler(),
00851                         wx.CommandEvent(wx.wxEVT_COMMAND_COMBOBOX_SELECTED,self.path_input.GetId()))
00852 
00853     def _update_graph(self):
00854         """This thread continuously updates the graph when it changes.
00855 
00856         The graph gets updated in one of two ways:
00857 
00858           1: The structure of the SMACH plans has changed, or the display
00859           settings have been changed. In this case, the dotcode needs to be
00860           regenerated. 
00861 
00862           2: The status of the SMACH plans has changed. In this case, we only
00863           need to change the styles of the graph.
00864         """
00865         while self._keep_running and not rospy.is_shutdown():
00866             with self._update_cond:
00867                 # Wait for the update condition to be triggered
00868                 self._update_cond.wait()
00869 
00870                 # Get the containers to update
00871                 containers_to_update = {}
00872                 if self._path in self._containers:
00873                     # Some non-root path
00874                     containers_to_update = {self._path:self._containers[self._path]}
00875                 elif self._path == '/':
00876                     # Root path
00877                     containers_to_update = self._top_containers
00878 
00879                 # Check if we need to re-generate the dotcode (if the structure changed)
00880                 # TODO: needs_zoom is a misnomer
00881                 if self._structure_changed or self._needs_zoom:
00882                     dotstr = "digraph {\n\t"
00883                     dotstr += ';'.join([
00884                         "compound=true",
00885                         "outputmode=nodesfirst",
00886                         "labeljust=l",
00887                         "nodesep=0.5",
00888                         "minlen=2",
00889                         "mclimit=5",
00890                         "clusterrank=local",
00891                         "ranksep=0.75",
00892                         # "remincross=true",
00893                         # "rank=sink",
00894                         "ordering=\"\"",
00895                         ])
00896                     dotstr += ";\n"
00897 
00898                     # Generate the rest of the graph
00899                     # TODO: Only re-generate dotcode for containers that have changed
00900                     for path,tc in containers_to_update.iteritems():
00901                         dotstr += tc.get_dotcode(
00902                                 self._selected_paths,[],
00903                                 0,self._max_depth,
00904                                 self._containers,
00905                                 self._show_all_transitions,
00906                                 self._label_wrapper)
00907                     else:
00908                         dotstr += '"__empty__" [label="Path not available.", shape="plaintext"]'
00909 
00910                     dotstr += '\n}\n'
00911                     self.dotstr = dotstr
00912                     # Set the dotcode to the new dotcode, reset the flags
00913                     self.set_dotcode(dotstr,zoom=False)
00914                     self._structure_changed = False
00915 
00916                 # Update the styles for the graph if there are any updates
00917                 for path,tc in containers_to_update.iteritems():
00918                     tc.set_styles(
00919                             self._selected_paths,
00920                             0,self._max_depth,
00921                             self.widget.items_by_url,
00922                             self.widget.subgraph_shapes,
00923                             self._containers)
00924 
00925                 # Redraw
00926                 self.widget.Refresh()
00927 
00928     def set_dotcode(self, dotcode, zoom=True):
00929         """Set the xdot view's dotcode and refresh the display."""
00930         # Set the new dotcode
00931         if self.widget.set_dotcode(dotcode, None):
00932             self.SetTitle('Smach Viewer')
00933             # Re-zoom if necessary
00934             if zoom or self._needs_zoom:
00935                 self.widget.zoom_to_fit()
00936                 self._needs_zoom = False
00937             # Set the refresh flag
00938             self._needs_refresh = True
00939             wx.PostEvent(self.GetEventHandler(), wx.IdleEvent())
00940 
00941     def _update_tree(self):
00942         """Update the tree view."""
00943         while self._keep_running and not rospy.is_shutdown():
00944             with self._update_cond:
00945                 self._update_cond.wait()
00946                 self.tree.DeleteAllItems()
00947                 self._tree_nodes = {}
00948                 for path,tc in self._top_containers.iteritems():
00949                     self.add_to_tree(path, None)
00950 
00951     def add_to_tree(self, path, parent):
00952         """Add a path to the tree view."""
00953         if parent is None:
00954             container = self.tree.AddRoot(get_label(path))
00955         else:
00956             container = self.tree.AppendItem(parent,get_label(path))
00957 
00958         # Add children to tree
00959         for label in self._containers[path]._children:
00960             child_path = '/'.join([path,label])
00961             if child_path in self._containers.keys():
00962                 self.add_to_tree(child_path, container)
00963             else:
00964                 self.tree.AppendItem(container,label)
00965 
00966     def append_tree(self, container, parent = None):
00967         """Append an item to the tree view."""
00968         if not parent:
00969             node = self.tree.AddRoot(container._label)
00970             for child_label in container._children:
00971                 self.tree.AppendItem(node,child_label)
00972 
00973     def OnIdle(self, event):
00974         """Event: On Idle, refresh the display if necessary, then un-set the flag."""
00975         if self._needs_refresh:
00976             self.Refresh()
00977             # Re-populate path combo
00978             self._needs_refresh = False
00979 
00980     def _update_server_list(self):
00981         """Update the list of known SMACH introspection servers."""
00982         while self._keep_running:
00983             # Update the server list
00984             server_names = self._client.get_servers()
00985             new_server_names = [sn for sn in server_names if sn not in self._status_subs]
00986 
00987             # Create subscribers for new servers
00988             for server_name in new_server_names:
00989                 self._structure_subs[server_name] = rospy.Subscriber(
00990                         server_name+smach_ros.introspection.STRUCTURE_TOPIC,
00991                         SmachContainerStructure,
00992                         callback = self._structure_msg_update,
00993                         callback_args = server_name,
00994                         queue_size=50)
00995 
00996                 self._status_subs[server_name] = rospy.Subscriber(
00997                         server_name+smach_ros.introspection.STATUS_TOPIC,
00998                         SmachContainerStatus,
00999                         callback = self._status_msg_update,
01000                         queue_size=50)
01001 
01002             # This doesn't need to happen very often
01003             rospy.sleep(1.0)
01004             
01005             
01006             #self.server_combo.AppendItems([s for s in self._servers if s not in current_servers])
01007 
01008             # Grab the first server
01009             #current_value = self.server_combo.GetValue()
01010             #if current_value == '' and len(self._servers) > 0:
01011             #    self.server_combo.SetStringSelection(self._servers[0])
01012             #    self.set_server(self._servers[0])
01013 
01014     def ShowControlsDialog(self,event):
01015         dial = wx.MessageDialog(None,
01016                 "Pan: Arrow Keys\nZoom: PageUp / PageDown\nZoom To Fit: F\nRefresh: R",
01017                 'Keyboard Controls', wx.OK)
01018         dial.ShowModal()
01019 
01020     def SaveDotGraph(self,event):
01021         timestr = time.strftime("%Y%m%d-%H%M%S")
01022         directory = rospkg.get_ros_home()+'/dotfiles/'
01023         if not os.path.exists(directory):
01024                 os.makedirs(directory)
01025         filename = directory+timestr+'.dot'
01026         print('Writing to file: %s' % filename)
01027         with open(filename, 'w') as f:
01028             f.write(self.dotstr)
01029 
01030     def OnExit(self, event):
01031         pass
01032 
01033     def set_filter(self, filter):
01034         self.widget.set_filter(filter)
01035 
01036 def main():
01037     from argparse import ArgumentParser
01038     p = ArgumentParser()
01039     p.add_argument('-f', '--auto-focus',
01040                  action='store_true',
01041                  help="Enable 'AutoFocus to subgraph' as default",
01042                  dest='enable_auto_focus')
01043     args = p.parse_args()
01044     app = wx.App()
01045 
01046     frame = SmachViewerFrame()
01047     frame.set_filter('dot')
01048 
01049     frame.Show()
01050 
01051     if args.enable_auto_focus:
01052         frame.toggle_auto_focus(None)
01053 
01054     app.MainLoop()
01055 
01056 if __name__ == '__main__':
01057     rospy.init_node('smach_viewer',anonymous=False, disable_signals=True,log_level=rospy.INFO)
01058     sys.argv = rospy.myargv()
01059     main()


smach_viewer
Author(s): Jonathan Bohren
autogenerated on Thu Jun 6 2019 17:28:28