00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
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
00059
00060
00061
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
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
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
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
00168 needs_update = False
00169
00170
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
00178 self._initial_states = msg.initial_states
00179 self._last_active_states = self._active_states
00180 self._active_states = msg.active_states
00181
00182
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
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
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
00215 attrs['color'] = '#00000000'
00216 attrs['fillcolor'] = '#0000000F'
00217
00218
00219
00220 dotstr += graph_attr_string(attrs)
00221
00222
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
00237 if max_depth == -1 or depth <= max_depth:
00238
00239 dotstr += 'subgraph "cluster_%s" {\n' % '/'.join([self._path,'__outcomes__'])
00240 outcomes_attrs = {
00241 'style':'rounded,filled',
00242 'rank':'sink',
00243 'color':'#FFFFFFFF',
00244 'fillcolor':'#FFFFFF00'
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',
00256 'color':'#780006',
00257 'fontcolor':'#780006',
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
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
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
00291 internal_edges = zip(
00292 self._internal_outcomes,
00293 self._outcomes_from,
00294 self._outcomes_to)
00295
00296
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
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
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
00327
00328
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'
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
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
00381
00382
00383 if max_depth == -1 or depth <= max_depth:
00384
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
00400 child_color = active_color
00401 child_fillcolor = active_fillcolor
00402 child_linewidth = 5
00403 elif child_label in self._initial_states:
00404
00405
00406 child_color = initial_color
00407 child_linewidth = 2
00408
00409
00410 if child_path in selected_paths:
00411 child_color = hex2t('#FB000DFF')
00412
00413
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
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
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
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
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
00483 viewer = wx.Panel(self.content_splitter,-1)
00484
00485
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
00492 graph_view = wx.Panel(nb,-1)
00493 gv_vbox = wx.BoxSizer(wx.VERTICAL)
00494 graph_view.SetSizer(gv_vbox)
00495
00496
00497 toolbar = wx.ToolBar(graph_view, -1)
00498
00499 toolbar.AddControl(wx.StaticText(toolbar,-1,"Path: "))
00500
00501
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
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
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
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
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
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
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
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
00595 self.content_splitter.SplitVertically(viewer, self.ud_win, 512)
00596
00597
00598 self.statusbar = wx.StatusBar(self,-1)
00599
00600
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
00608 self._client = smach_ros.IntrospectionClient()
00609 self._containers= {}
00610 self._selected_paths = []
00611
00612
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
00620 self.widget.register_select_callback(self.select_cb)
00621 self._path = '/'
00622 self._needs_zoom = True
00623 self._structure_changed = True
00624
00625
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
00711 if not type(item.url) is str:
00712 return
00713
00714 self.statusbar.SetStatusText(item.url)
00715
00716 if event.ButtonUp(wx.MOUSE_BTN_LEFT):
00717
00718 self._selected_paths = [item.url]
00719
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
00731 if len(path_input_str) > 0:
00732
00733 path = path_input_str.split(':')[0]
00734
00735
00736
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
00744 self.is_button.Enable()
00745
00746
00747 container = self._containers[parent_path]
00748
00749
00750 pos = self.ud_txt.HitTestPos(wx.Point(0,0))
00751 sel = self.ud_txt.GetSelection()
00752
00753
00754 ud_str = ''
00755 for (k,v) in container._local_data._data.iteritems():
00756 ud_str += str(k)+": "
00757 vstr = str(v)
00758
00759 if vstr.find('\n') != -1:
00760 ud_str += '\n'
00761 ud_str+=vstr+'\n\n'
00762
00763
00764 self.ud_txt.SetValue(ud_str)
00765
00766
00767 self.ud_txt.ShowPosition(pos[1])
00768 if sel != (0,0):
00769 self.ud_txt.SetSelection(sel[0],sel[1])
00770 else:
00771
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
00778 if not self._keep_running:
00779 return
00780
00781
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
00790 needs_redraw = False
00791
00792 if path in self._containers:
00793 rospy.logdebug("UPDATING: "+path)
00794
00795
00796 needs_redraw = self._containers[path].update_structure(msg)
00797 else:
00798 rospy.logdebug("CONSTRUCTING: "+path)
00799
00800
00801 container = ContainerNode(server_name, msg)
00802 self._containers[path] = container
00803
00804
00805 if parent_path == '':
00806 self._top_containers[path] = container
00807
00808
00809 self.path_combo.Append(path)
00810 self.path_input.Append(path)
00811
00812
00813 if parent_path in self._containers:
00814 needs_redraw = True
00815
00816
00817 if needs_redraw:
00818 with self._update_cond:
00819 self._structure_changed = True
00820 self._needs_zoom = True
00821 self._update_cond.notify_all()
00822
00823 def _status_msg_update(self, msg):
00824 """Process status messages."""
00825
00826
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
00835 path = msg.path
00836 rospy.logdebug("STATUS MSG: "+path)
00837
00838
00839 if path in self._containers:
00840
00841 container = self._containers[path]
00842 if container.update_status(msg):
00843 with self._update_cond:
00844 self._update_cond.notify_all()
00845
00846
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
00868 self._update_cond.wait()
00869
00870
00871 containers_to_update = {}
00872 if self._path in self._containers:
00873
00874 containers_to_update = {self._path:self._containers[self._path]}
00875 elif self._path == '/':
00876
00877 containers_to_update = self._top_containers
00878
00879
00880
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
00893
00894 "ordering=\"\"",
00895 ])
00896 dotstr += ";\n"
00897
00898
00899
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
00913 self.set_dotcode(dotstr,zoom=False)
00914 self._structure_changed = False
00915
00916
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
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
00931 if self.widget.set_dotcode(dotcode, None):
00932 self.SetTitle('Smach Viewer')
00933
00934 if zoom or self._needs_zoom:
00935 self.widget.zoom_to_fit()
00936 self._needs_zoom = False
00937
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
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
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
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
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
01003 rospy.sleep(1.0)
01004
01005
01006
01007
01008
01009
01010
01011
01012
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
01059 main()