test_tools
headless_smach_viewer.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 
3 # Copyright (c) 2010, Willow Garage, Inc.
4 # All rights reserved.
5 # Copyright (c) 2013, Jonathan Bohren, The Johns Hopkins University
6 #
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions are met:
9 #
10 # * Redistributions of source code must retain the above copyright
11 # notice, this list of conditions and the following disclaimer.
12 # * Redistributions in binary form must reproduce the above copyright
13 # notice, this list of conditions and the following disclaimer in the
14 # documentation and/or other materials provided with the distribution.
15 # * Neither the name of the Willow Garage, Inc. nor the names of its
16 # contributors may be used to endorse or promote products derived from
17 # this software without specific prior written permission.
18 #
19 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22 # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
23 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 # POSSIBILITY OF SUCH DAMAGE.
30 #
31 # Author: Jonathan Bohren
32 
33 from __future__ import print_function
34 
35 import pickle
36 import threading
37 import wxversion
38 
39 import rospy
40 from smach_msgs.msg import SmachContainerStatus, SmachContainerStructure
41 
42 wxversion.select("2.8")
43 import wx
44 import wx.richtext
45 
46 import textwrap
47 
48 import xdot
49 import smach
50 import smach_ros
51 
52 
53 def graph_attr_string(attrs):
54  """Generate an xdot graph attribute string."""
55  attrs_strs = ['"'+str(k)+'"="'+str(v)+'"' for k, v in attrs.items()]
56  return ';\n'.join(attrs_strs)+';\n'
57 
58 def attr_string(attrs):
59  """Generate an xdot node attribute string."""
60  attrs_strs = ['"'+str(k)+'"="'+str(v)+'"' for k, v in attrs.items()]
61  return ' ['+(', '.join(attrs_strs))+']'
62 
63 def get_parent_path(path):
64  """Get the parent path of an xdot node."""
65  path_tokens = path.split('/')
66  if len(path_tokens) > 2:
67  parent_path = '/'.join(path_tokens[0:-1])
68  else:
69  parent_path = '/'.join(path_tokens[0:1])
70  return parent_path
71 
72 def get_label(path):
73  """Get the label of an xdot node."""
74  path_tokens = path.split('/')
75  return path_tokens[-1]
76 
77 def hex2t(color_str):
78  """Convert a hexadecimal color strng into a color tuple."""
79  color_tuple = [int(color_str[i:i+2],16)/255.0 for i in range(1,len(color_str),2)]
80  return color_tuple
81 
82 class ContainerNode():
83  """
84  This class represents a given container in a running SMACH system.
85 
86  Its primary use is to generate dotcode for a SMACH container. It has
87  methods for responding to structure and status messages from a SMACH
88  introspection server, as well as methods for updating the styles of a
89  graph once it's been drawn.
90  """
91  def __init__(self, server_name, msg):
92  # Store path info
93  self._server_name = server_name
94  self._path = msg.path
95  splitpath = msg.path.split('/')
96  self._label = splitpath[-1]
97  self._dir = '/'.join(splitpath[0:-1])
98 
99  self._children = msg.children
100  self._internal_outcomes = msg.internal_outcomes
101  self._outcomes_from = msg.outcomes_from
102  self._outcomes_to = msg.outcomes_to
103 
104  self._container_outcomes = msg.container_outcomes
105 
106  # Status
107  self._initial_states = []
108  self._active_states = []
110  self._local_data = smach.UserData()
111  self._info = ''
112 
113  def update_structure(self, msg):
114  """Update the structure of this container from a given message. Return True if anything changes."""
115  needs_update = False
116 
117  if self._children != msg.children\
118  or self._internal_outcomes != msg.internal_outcomes\
119  or self._outcomes_from != msg.outcomes_from\
120  or self._outcomes_to != msg.outcomes_to\
121  or self._container_outcomes != msg.container_outcomes:
122  needs_update = True
123 
124  if needs_update:
125  self._children = msg.children
126  self._internal_outcomes = msg.internal_outcomes
127  self._outcomes_from = msg.outcomes_from
128  self._outcomes_to = msg.outcomes_to
129 
130  self._container_outcomes = msg.container_outcomes
131 
132  return needs_update
133 
134  def update_status(self, msg):
135  """Update the known userdata and active state set and return True if the graph needs to be redrawn."""
136 
137  # Initialize the return value
138  needs_update = False
139 
140  # Check if the initial states or active states have changed
141  if set(msg.initial_states) != set(self._initial_states):
142  self._structure_changed = True
143  needs_update = True
144  if set(msg.active_states) != set(self._active_states):
145  needs_update = True
146 
147  # Store the initial and active states
148  self._initial_states = msg.initial_states
150  self._active_states = msg.active_states
151 
152  # Unpack the user data
153  while not rospy.is_shutdown():
154  try:
155  self._local_data._data = pickle.loads(msg.local_data)
156  break
157  except ImportError as ie:
158  # This will only happen once for each package
159  modulename = ie.args[0][16:]
160  packagename = modulename[0:modulename.find('.')]
161  roslib.load_manifest(packagename)
162  self._local_data._data = pickle.loads(msg.local_data)
163 
164  # Store the info string
165  self._info = msg.info
166 
167  return needs_update
168 
169  def get_dotcode(self, selected_paths, closed_paths, depth, max_depth, containers, show_all, label_wrapper, attrs={}):
170  """Generate the dotcode representing this container.
171 
172  @param selected_paths: The paths to nodes that are selected
173  @closed paths: The paths that shouldn't be expanded
174  @param depth: The depth to start traversing the tree
175  @param max_depth: The depth to which we should traverse the tree
176  @param containers: A dict of containers keyed by their paths
177  @param show_all: True if implicit transitions should be shown
178  @param label_wrapper: A text wrapper for wrapping element names
179  @param attrs: A dict of dotcode attributes for this cluster
180  """
181 
182  dotstr = 'subgraph "cluster_%s" {\n' % (self._path)
183  if depth == 0:
184  #attrs['style'] = 'filled,rounded'
185  attrs['color'] = '#00000000'
186  attrs['fillcolor'] = '#0000000F'
187  #attrs['rank'] = 'max'
188 
189  #,'succeeded','aborted','preempted'attrs['label'] = self._label
190  dotstr += graph_attr_string(attrs)
191 
192  # Add start/terimate target
193  proxy_attrs = {
194  'URL':self._path,
195  'shape':'plaintext',
196  'color':'gray',
197  'fontsize':'18',
198  'fontweight':'18',
199  'rank':'min',
200  'height':'0.01'}
201  proxy_attrs['label'] = '\\n'.join(label_wrapper.wrap(self._label))
202  dotstr += '"%s" %s;\n' % (
203  '/'.join([self._path,'__proxy__']),
204  attr_string(proxy_attrs))
205 
206  # Check if we should expand this container
207  if max_depth == -1 or depth <= max_depth:
208  # Add container outcomes
209  dotstr += 'subgraph "cluster_%s" {\n' % '/'.join([self._path,'__outcomes__'])
210  outcomes_attrs = {
211  'style':'rounded,filled',
212  'rank':'sink',
213  'color':'#FFFFFFFF',#'#871C34',
214  'fillcolor':'#FFFFFF00'#'#FE464f3F'#'#DB889A'
215  }
216  dotstr += graph_attr_string(outcomes_attrs)
217 
218  for outcome_label in self._container_outcomes:
219  outcome_path = ':'.join([self._path,outcome_label])
220  outcome_attrs = {
221  'shape':'box',
222  'height':'0.3',
223  'style':'filled,rounded',
224  'fontsize':'12',
225  'fillcolor':'#FE464f',#'#EDC2CC',
226  'color':'#780006',#'#EBAEBB',
227  'fontcolor':'#780006',#'#EBAEBB',
228  'label':'\\n'.join(label_wrapper.wrap(outcome_label)),
229  'URL':':'.join([self._path,outcome_label])
230  }
231  dotstr += '"%s" %s;\n' % (outcome_path,attr_string(outcome_attrs))
232  dotstr += "}\n"
233 
234  # Iterate over children
235  for child_label in self._children:
236  child_attrs = {
237  'style':'filled,setlinewidth(2)',
238  'color':'#000000FF',
239  'fillcolor':'#FFFFFF00'
240  }
241 
242  child_path = '/'.join([self._path,child_label])
243  # Generate dotcode for children
244  if child_path in containers:
245  child_attrs['style'] += ',rounded'
246 
247  dotstr += containers[child_path].get_dotcode(
248  selected_paths,
249  closed_paths,
250  depth+1, max_depth,
251  containers,
252  show_all,
253  label_wrapper,
254  child_attrs)
255  else:
256  child_attrs['label'] = '\\n'.join(label_wrapper.wrap(child_label))
257  child_attrs['URL'] = child_path
258  dotstr += '"%s" %s;\n' % (child_path, attr_string(child_attrs))
259 
260  # Iterate over edges
261  internal_edges = zip(
262  self._internal_outcomes,
263  self._outcomes_from,
264  self._outcomes_to)
265 
266  # Add edge from container label to initial state
267  internal_edges += [('','__proxy__',initial_child) for initial_child in self._initial_states]
268 
269  has_explicit_transitions = []
270  for (outcome_label,from_label,to_label) in internal_edges:
271  if to_label != 'None' or outcome_label == to_label:
272  has_explicit_transitions.append(from_label)
273 
274  # Draw internal edges
275  for (outcome_label,from_label,to_label) in internal_edges:
276 
277  from_path = '/'.join([self._path, from_label])
278 
279  if show_all \
280  or to_label != 'None'\
281  or from_label not in has_explicit_transitions \
282  or (outcome_label == from_label) \
283  or from_path in containers:
284  # Set the implicit target of this outcome
285  if to_label == 'None':
286  to_label = outcome_label
287 
288  to_path = '/'.join([self._path, to_label])
289 
290  edge_attrs = {
291  'URL':':'.join([from_path,outcome_label,to_path]),
292  'fontsize':'12',
293  'label':'\\n'.join(label_wrapper.wrap(outcome_label))}
294  edge_attrs['style'] = 'setlinewidth(2)'
295 
296  # Hide implicit
297  #if not show_all and to_label == outcome_label:
298  # edge_attrs['style'] += ',invis'
299 
300  from_key = '"%s"' % from_path
301  if from_path in containers:
302  if max_depth == -1 or depth+1 <= max_depth:
303  from_key = '"%s:%s"' % ( from_path, outcome_label)
304  else:
305  edge_attrs['ltail'] = 'cluster_'+from_path
306  from_path = '/'.join([from_path,'__proxy__'])
307  from_key = '"%s"' % ( from_path )
308 
309  to_key = ''
310  if to_label in self._container_outcomes:
311  to_key = '"%s:%s"' % (self._path,to_label)
312  edge_attrs['color'] = '#00000055'# '#780006'
313  else:
314  if to_path in containers:
315  edge_attrs['lhead'] = 'cluster_'+to_path
316  to_path = '/'.join([to_path,'__proxy__'])
317  to_key = '"%s"' % to_path
318 
319  dotstr += '%s -> %s %s;\n' % (
320  from_key, to_key, attr_string(edge_attrs))
321 
322  dotstr += '}\n'
323  return dotstr
324 
325  def set_styles(self, selected_paths, depth, max_depth, items, subgraph_shapes, containers):
326  """Update the styles for a list of containers without regenerating the dotcode.
327 
328  This function is called recursively to update an entire tree.
329 
330  @param selected_paths: A list of paths to nodes that are currently selected.
331  @param depth: The depth to start traversing the tree
332  @param max_depth: The depth to traverse into the tree
333  @param items: A dict of all the graph items, keyed by url
334  @param subgraph_shapes: A dictionary of shapes from the rendering engine
335  @param containers: A dict of all the containers
336  """
337 
338  # Color root container
339  """
340  if depth == 0:
341  container_shapes = subgraph_shapes['cluster_'+self._path]
342  container_color = (0,0,0,0)
343  container_fillcolor = (0,0,0,0)
344 
345  for shape in container_shapes:
346  shape.pen.color = container_color
347  shape.pen.fillcolor = container_fillcolor
348  """
349 
350  # Color shapes for outcomes
351 
352  # Color children
353  if max_depth == -1 or depth <= max_depth:
354  # Iterate over children
355  for child_label in self._children:
356  child_path = '/'.join([self._path,child_label])
357 
358  child_color = [0.5,0.5,0.5,1]
359  child_fillcolor = [1,1,1,1]
360  child_linewidth = 2
361 
362  active_color = hex2t('#5C7600FF')
363  active_fillcolor = hex2t('#C0F700FF')
364 
365  initial_color = hex2t('#000000FF')
366  initial_fillcolor = hex2t('#FFFFFFFF')
367 
368  if child_label in self._active_states:
369  # Check if the child is active
370  child_color = active_color
371  child_fillcolor = active_fillcolor
372  child_linewidth = 5
373  elif child_label in self._initial_states:
374  # Initial style
375  #child_fillcolor = initial_fillcolor
376  child_color = initial_color
377  child_linewidth = 2
378 
379  # Check if the child is selected
380  if child_path in selected_paths:
381  child_color = hex2t('#FB000DFF')
382 
383  # Generate dotcode for child containers
384  if child_path in containers:
385  subgraph_id = 'cluster_'+child_path
386  if subgraph_id in subgraph_shapes:
387  if child_label in self._active_states:
388  child_fillcolor[3] = 0.25
389  elif 0 and child_label in self._initial_states:
390  child_fillcolor[3] = 0.25
391  else:
392  if max_depth > 0:
393  v = 1.0-0.25*((depth+1)/float(max_depth))
394  else:
395  v = 0.85
396  child_fillcolor = [v,v,v,1.0]
397 
398 
399  for shape in subgraph_shapes['cluster_'+child_path]:
400  pen = shape.pen
401  if len(pen.color) > 3:
402  pen_color_opacity = pen.color[3]
403  if pen_color_opacity < 0.01:
404  pen_color_opacity = 0
405  else:
406  pen_color_opacity = 0.5
407  shape.pen.color = child_color[0:3]+[pen_color_opacity]
408  shape.pen.fillcolor = [child_fillcolor[i] for i in range(min(3,len(pen.fillcolor)))]
409  shape.pen.linewidth = child_linewidth
410 
411  # Recurse on this child
412  containers[child_path].set_styles(
413  selected_paths,
414  depth+1, max_depth,
415  items,
416  subgraph_shapes,
417  containers)
418  else:
419  if child_path in items:
420  for shape in items[child_path].shapes:
421  if not isinstance(shape,xdot.xdot.TextShape):
422  shape.pen.color = child_color
423  shape.pen.fillcolor = child_fillcolor
424  shape.pen.linewidth = child_linewidth
425  else:
426  #print(child_path+" NOT IN "+str(items.keys()))
427  pass
428 
429 class SmachViewerFrame(wx.Frame):
430  """
431  This class provides a GUI application for viewing SMACH plans.
432  """
433  def __init__(self):
434  wx.Frame.__init__(self, None, -1, "Smach Viewer", size=(720,480))
435 
436  # Create graph
437  self._containers = {}
438  self._top_containers = {}
439  self._update_cond = threading.Condition()
440  self._needs_refresh = True
441 
442  vbox = wx.BoxSizer(wx.VERTICAL)
443 
444 
445  # Create Splitter
446  self.content_splitter = wx.SplitterWindow(self, -1,style = wx.SP_LIVE_UPDATE)
447  self.content_splitter.SetMinimumPaneSize(24)
448  self.content_splitter.SetSashGravity(0.85)
449 
450 
451  # Create viewer pane
452  viewer = wx.Panel(self.content_splitter,-1)
453 
454  # Create smach viewer
455  nb = wx.Notebook(viewer,-1,style=wx.NB_TOP | wx.WANTS_CHARS)
456  viewer_box = wx.BoxSizer()
457  viewer_box.Add(nb,1,wx.EXPAND | wx.ALL, 4)
458  viewer.SetSizer(viewer_box)
459 
460  # Create graph view
461  graph_view = wx.Panel(nb,-1)
462  gv_vbox = wx.BoxSizer(wx.VERTICAL)
463  graph_view.SetSizer(gv_vbox)
464 
465  # Construct toolbar
466  toolbar = wx.ToolBar(graph_view, -1)
467 
468  toolbar.AddControl(wx.StaticText(toolbar,-1,"Path: "))
469 
470  # Path list
471  self.path_combo = wx.ComboBox(toolbar, -1, style=wx.CB_DROPDOWN)
472  self.path_combo .Bind(wx.EVT_COMBOBOX, self.set_path)
473  self.path_combo.Append('/')
474  self.path_combo.SetValue('/')
475  toolbar.AddControl(self.path_combo)
476 
477  # Depth spinner
478  self.depth_spinner = wx.SpinCtrl(toolbar, -1,
479  size=wx.Size(50,-1),
480  min=-1,
481  max=1337,
482  initial=-1)
483  self.depth_spinner.Bind(wx.EVT_SPINCTRL,self.set_depth)
484  self._max_depth = -1
485  toolbar.AddControl(wx.StaticText(toolbar,-1," Depth: "))
486  toolbar.AddControl(self.depth_spinner)
487 
488  # Label width spinner
489  self.width_spinner = wx.SpinCtrl(toolbar, -1,
490  size=wx.Size(50,-1),
491  min=1,
492  max=1337,
493  initial=40)
494  self.width_spinner.Bind(wx.EVT_SPINCTRL,self.set_label_width)
495  self._label_wrapper = textwrap.TextWrapper(40,break_long_words=True)
496  toolbar.AddControl(wx.StaticText(toolbar,-1," Label Width: "))
497  toolbar.AddControl(self.width_spinner)
498 
499  # Implicit transition display
500  toggle_all = wx.ToggleButton(toolbar,-1,'Show Implicit')
501  toggle_all.Bind(wx.EVT_TOGGLEBUTTON, self.toggle_all_transitions)
503 
504  toolbar.AddControl(wx.StaticText(toolbar,-1," "))
505  toolbar.AddControl(toggle_all)
506 
507  toolbar.AddControl(wx.StaticText(toolbar,-1," "))
508  toolbar.AddLabelTool(wx.ID_HELP, 'Help',
509  wx.ArtProvider.GetBitmap(wx.ART_HELP,wx.ART_OTHER,(16,16)) )
510  toolbar.Realize()
511 
512 
513  self.Bind(wx.EVT_TOOL, self.ShowControlsDialog, id=wx.ID_HELP)
514 
515  # Create dot graph widget
516  self.widget = xdot.wxxdot.WxDotWindow(graph_view, -1)
517 
518  gv_vbox.Add(toolbar, 0, wx.EXPAND)
519  gv_vbox.Add(self.widget, 1, wx.EXPAND)
520 
521  # Create tree view widget
522  self.tree = wx.TreeCtrl(nb,-1,style=wx.TR_HAS_BUTTONS)
523  nb.AddPage(graph_view,"Graph View")
524  nb.AddPage(self.tree,"Tree View")
525 
526 
527  # Create userdata widget
528  borders = wx.LEFT | wx.RIGHT | wx.TOP
529  border = 4
530  self.ud_win = wx.ScrolledWindow(self.content_splitter, -1)
531  self.ud_gs = wx.BoxSizer(wx.VERTICAL)
532 
533  self.ud_gs.Add(wx.StaticText(self.ud_win,-1,"Path:"),0, borders, border)
534 
535  self.path_input = wx.ComboBox(self.ud_win,-1,style=wx.CB_DROPDOWN)
536  self.path_input.Bind(wx.EVT_COMBOBOX,self.selection_changed)
537  self.ud_gs.Add(self.path_input,0,wx.EXPAND | borders, border)
538 
539 
540  self.ud_gs.Add(wx.StaticText(self.ud_win,-1,"Userdata:"),0, borders, border)
541 
542  self.ud_txt = wx.TextCtrl(self.ud_win,-1,style=wx.TE_MULTILINE | wx.TE_READONLY)
543  self.ud_gs.Add(self.ud_txt,1,wx.EXPAND | borders, border)
544 
545  # Add initial state button
546  self.is_button = wx.Button(self.ud_win,-1,"Set as Initial State")
547  self.is_button.Bind(wx.EVT_BUTTON, self.on_set_initial_state)
548  self.is_button.Disable()
549  self.ud_gs.Add(self.is_button,0,wx.EXPAND | wx.BOTTOM | borders, border)
550 
551  self.ud_win.SetSizer(self.ud_gs)
552 
553 
554  # Set content splitter
555  self.content_splitter.SplitVertically(viewer, self.ud_win, 512)
556 
557  # Add statusbar
558  self.statusbar = wx.StatusBar(self,-1)
559 
560  # Add elements to sizer
561  vbox.Add(self.content_splitter, 1, wx.EXPAND | wx.ALL)
562  vbox.Add(self.statusbar, 0, wx.EXPAND)
563 
564  self.SetSizer(vbox)
565  self.Center()
566 
567  # smach introspection client
568  self._client = smach_ros.IntrospectionClient()
569  self._containers= {}
570  self._selected_paths = []
571 
572  # Message subscribers
573  self._structure_subs = {}
574  self._status_subs = {}
575 
576  self.Bind(wx.EVT_IDLE,self.OnIdle)
577  self.Bind(wx.EVT_CLOSE,self.OnQuit)
578 
579  # Register mouse event callback
580  self.widget.register_select_callback(self.select_cb)
581  self._path = '/'
582  self._needs_zoom = True
583  self._structure_changed = True
584 
585  # Start a thread in the background to update the server list
586  self._keep_running = True
587  self._server_list_thread = threading.Thread(target=self._update_server_list)
589 
590  self._update_graph_thread = threading.Thread(target=self._update_graph)
592  self._update_tree_thread = threading.Thread(target=self._update_tree)
594 
595  def OnQuit(self,event):
596  """Quit Event: kill threads and wait for join."""
597  with self._update_cond:
598  self._keep_running = False
599  self._update_cond.notify_all()
600 
601  self._server_list_thread.join()
602  self._update_graph_thread.join()
603  self._update_tree_thread.join()
604 
605  event.Skip()
606 
607  def update_graph(self):
608  """Notify all that the graph needs to be updated."""
609  with self._update_cond:
610  self._update_cond.notify_all()
611 
612  def on_set_initial_state(self, event):
613  """Event: Change the initial state of the server."""
614  state_path = self._selected_paths[0]
615  parent_path = get_parent_path(state_path)
616  state = get_label(state_path)
617 
618  server_name = self._containers[parent_path]._server_name
619  self._client.set_initial_state(server_name,parent_path,[state],timeout = rospy.Duration(60.0))
620 
621  def set_path(self, event):
622  """Event: Change the viewable path and update the graph."""
623  self._path = self.path_combo.GetValue()
624  self._needs_zoom = True
625  self.update_graph()
626 
627  def set_depth(self, event):
628  """Event: Change the maximum target_depth and update the graph."""
629  self._max_depth = self.depth_spinner.GetValue()
630  self._needs_zoom = True
631  self.update_graph()
632 
633  def set_label_width(self, event):
634  """Event: Change the label wrapper width and update the graph."""
635  self._label_wrapper.width = self.width_spinner.GetValue()
636  self._needs_zoom = True
637  self.update_graph()
638 
639  def toggle_all_transitions(self, event):
640  """Event: Change whether automatic transitions are hidden and update the graph."""
642  self._structure_changed = True
643  self.update_graph()
644 
645  def select_cb(self, item, event):
646  """Event: Click to select a graph node to display user data and update the graph."""
647  self.statusbar.SetStatusText(item.url)
648  # Left button-up
649  if event.ButtonUp(wx.MOUSE_BTN_LEFT):
650  # Store this item's url as the selected path
651  self._selected_paths = [item.url]
652  # Update the selection dropdown
653  self.path_input.SetValue(item.url)
654  wx.PostEvent(
655  self.path_input.GetEventHandler(),
656  wx.CommandEvent(wx.wxEVT_COMMAND_COMBOBOX_SELECTED,self.path_input.GetId()))
657  self.update_graph()
658 
659  def selection_changed(self, event):
660  """Event: Selection dropdown changed."""
661  path_input_str = self.path_input.GetValue()
662 
663  # Check the path is non-zero length
664  if len(path_input_str) > 0:
665  # Split the path (state:outcome), and get the state path
666  path = path_input_str.split(':')[0]
667 
668  # Get the container corresponding to this path, since userdata is
669  # stored in the containers
670  if path not in self._containers:
671  parent_path = get_parent_path(path)
672  else:
673  parent_path = path
674 
675  if parent_path in self._containers:
676  # Enable the initial state button for the selection
677  self.is_button.Enable()
678 
679  # Get the container
680  container = self._containers[parent_path]
681 
682  # Store the scroll position and selection
683  pos = self.ud_txt.HitTestPos(wx.Point(0,0))
684  sel = self.ud_txt.GetSelection()
685 
686  # Generate the userdata string
687  ud_str = ''
688  for k, v in container._local_data._data.items():
689  ud_str += str(k)+": "
690  vstr = str(v)
691  # Add a line break if this is a multiline value
692  if vstr.find('\n') != -1:
693  ud_str += '\n'
694  ud_str+=vstr+'\n\n'
695 
696  # Set the userdata string
697  self.ud_txt.SetValue(ud_str)
698 
699  # Restore the scroll position and selection
700  self.ud_txt.ShowPosition(pos[1])
701  if sel != (0,0):
702  self.ud_txt.SetSelection(sel[0],sel[1])
703  else:
704  # Disable the initial state button for this selection
705  self.is_button.Disable()
706 
707  def _structure_msg_update(self, msg, server_name):
708  """Update the structure of the SMACH plan (re-generate the dotcode)."""
709 
710  # Just return if we're shutting down
711  if not self._keep_running:
712  return
713 
714  # Get the node path
715  path = msg.path
716  pathsplit = path.split('/')
717  parent_path = '/'.join(pathsplit[0:-1])
718 
719  rospy.logdebug("RECEIVED: "+path)
720  rospy.logdebug("CONTAINERS: "+str(self._containers.keys()))
721 
722  # Initialize redraw flag
723  needs_redraw = False
724 
725  if path in self._containers:
726  rospy.logdebug("UPDATING: "+path)
727 
728  # Update the structure of this known container
729  needs_redraw = self._containers[path].update_structure(msg)
730  else:
731  rospy.logdebug("CONSTRUCTING: "+path)
732 
733  # Create a new container
734  container = ContainerNode(server_name, msg)
735  self._containers[path] = container
736 
737  # Store this as a top container if it has no parent
738  if parent_path == '':
739  self._top_containers[path] = container
740 
741  # Append paths to selector
742  self.path_combo.Append(path)
743  self.path_input.Append(path)
744 
745  # We need to redraw thhe graph if this container's parent is already known
746  if parent_path in self._containers:
747  needs_redraw= True
748 
749  # Update the graph if necessary
750  if needs_redraw:
751  with self._update_cond:
752  self._structure_changed = True
753  self._needs_zoom = True # TODO: Make it so you can disable this
754  self._update_cond.notify_all()
755 
756  def _status_msg_update(self, msg):
757  """Process status messages."""
758 
759  # Check if we're in the process of shutting down
760  if not self._keep_running:
761  return
762 
763  # Get the path to the updating conainer
764  path = msg.path
765  rospy.logdebug("STATUS MSG: "+path)
766 
767  # Check if this is a known container
768  if path in self._containers:
769  # Get the container and check if the status update requires regeneration
770  container = self._containers[path]
771  if container.update_status(msg):
772  with self._update_cond:
773  self._update_cond.notify_all()
774 
775  # TODO: Is this necessary?
776  path_input_str = self.path_input.GetValue()
777  if path_input_str == path or get_parent_path(path_input_str) == path:
778  wx.PostEvent(
779  self.path_input.GetEventHandler(),
780  wx.CommandEvent(wx.wxEVT_COMMAND_COMBOBOX_SELECTED,self.path_input.GetId()))
781 
782  def _update_graph(self):
783  """This thread continuously updates the graph when it changes.
784 
785  The graph gets updated in one of two ways:
786 
787  1: The structure of the SMACH plans has changed, or the display
788  settings have been changed. In this case, the dotcode needs to be
789  regenerated.
790 
791  2: The status of the SMACH plans has changed. In this case, we only
792  need to change the styles of the graph.
793  """
794  while self._keep_running and not rospy.is_shutdown():
795  with self._update_cond:
796  # Wait for the update condition to be triggered
797  self._update_cond.wait()
798 
799  # Get the containers to update
800  containers_to_update = {}
801  if self._path in self._containers:
802  # Some non-root path
803  containers_to_update = {self._path:self._containers[self._path]}
804  elif self._path == '/':
805  # Root path
806  containers_to_update = self._top_containers
807 
808  # Check if we need to re-generate the dotcode (if the structure changed)
809  # TODO: needs_zoom is a misnomer
810  if self._structure_changed or self._needs_zoom:
811  dotstr = "digraph {\n\t"
812  dotstr += ';'.join([
813  "compound=true",
814  "outputmode=nodesfirst",
815  "labeljust=l",
816  "nodesep=0.5",
817  "minlen=2",
818  "mclimit=5",
819  "clusterrank=local",
820  "ranksep=0.75",
821  # "remincross=true",
822  # "rank=sink",
823  "ordering=\"\"",
824  ])
825  dotstr += ";\n"
826 
827  # Generate the rest of the graph
828  # TODO: Only re-generate dotcode for containers that have changed
829  for path,tc in containers_to_update.items():
830  dotstr += tc.get_dotcode(
831  self._selected_paths,[],
832  0,self._max_depth,
833  self._containers,
835  self._label_wrapper)
836  else:
837  dotstr += '"__empty__" [label="Path not available.", shape="plaintext"]'
838 
839  dotstr += '\n}\n'
840 
841  # Set the dotcode to the new dotcode, reset the flags
842  self.set_dotcode(dotstr,zoom=False)
843  self._structure_changed = False
844 
845  # Update the styles for the graph if there are any updates
846  for path,tc in containers_to_update.items():
847  tc.set_styles(
848  self._selected_paths,
849  0,self._max_depth,
850  self.widget.items_by_url,
851  self.widget.subgraph_shapes,
852  self._containers)
853 
854  # Redraw
855  self.widget.Refresh()
856 
857  def set_dotcode(self, dotcode, zoom=True):
858  """Set the xdot view's dotcode and refresh the display."""
859  # Set the new dotcode
860  if self.widget.set_dotcode(dotcode, None):
861  self.SetTitle('Smach Viewer')
862  # Re-zoom if necessary
863  if zoom or self._needs_zoom:
864  self.widget.zoom_to_fit()
865  self._needs_zoom = False
866  # Set the refresh flag
867  self._needs_refresh = True
868  wx.PostEvent(self.GetEventHandler(), wx.IdleEvent())
869 
870  with open("dump.dot", "w+") as dotfile:
871  dotfile.write(dotcode)
872 
873  def _update_tree(self):
874  """Update the tree view."""
875  while self._keep_running and not rospy.is_shutdown():
876  with self._update_cond:
877  self._update_cond.wait()
878  self.tree.DeleteAllItems()
879  self._tree_nodes = {}
880  for path,tc in self._top_containers.items():
881  self.add_to_tree(path, None)
882 
883  def add_to_tree(self, path, parent):
884  """Add a path to the tree view."""
885  if parent is None:
886  container = self.tree.AddRoot(get_label(path))
887  else:
888  container = self.tree.AppendItem(parent,get_label(path))
889 
890  # Add children to tree
891  for label in self._containers[path]._children:
892  child_path = '/'.join([path,label])
893  if child_path in self._containers.keys():
894  self.add_to_tree(child_path, container)
895  else:
896  self.tree.AppendItem(container,label)
897 
898  def append_tree(self, container, parent = None):
899  """Append an item to the tree view."""
900  if not parent:
901  node = self.tree.AddRoot(container._label)
902  for child_label in container._children:
903  self.tree.AppendItem(node,child_label)
904 
905  def OnIdle(self, event):
906  """Event: On Idle, refresh the display if necessary, then un-set the flag."""
907  if self._needs_refresh:
908  self.Refresh()
909  # Re-populate path combo
910  self._needs_refresh = False
911 
913  """Update the list of known SMACH introspection servers."""
914  while self._keep_running:
915  # Update the server list
916  server_names = self._client.get_servers()
917  new_server_names = [sn for sn in server_names if sn not in self._status_subs]
918 
919  # Create subscribers for new servers
920  for server_name in new_server_names:
921  self._structure_subs[server_name] = rospy.Subscriber(
922  server_name+smach_ros.introspection.STRUCTURE_TOPIC,
923  SmachContainerStructure,
924  callback = self._structure_msg_update,
925  callback_args = server_name,
926  queue_size=50)
927 
928  self._status_subs[server_name] = rospy.Subscriber(
929  server_name+smach_ros.introspection.STATUS_TOPIC,
930  SmachContainerStatus,
931  callback = self._status_msg_update,
932  queue_size=50)
933 
934  # This doesn't need to happen very often
935  rospy.sleep(1.0)
936 
937 
938  #self.server_combo.AppendItems([s for s in self._servers if s not in current_servers])
939 
940  # Grab the first server
941  #current_value = self.server_combo.GetValue()
942  #if current_value == '' and len(self._servers) > 0:
943  # self.server_combo.SetStringSelection(self._servers[0])
944  # self.set_server(self._servers[0])
945 
946  def ShowControlsDialog(self,event):
947  dial = wx.MessageDialog(None,
948  "Pan: Arrow Keys\nZoom: PageUp / PageDown\nZoom To Fit: F\nRefresh: R",
949  'Keyboard Controls', wx.OK)
950  dial.ShowModal()
951 
952  def OnExit(self, event):
953  pass
954 
955  def set_filter(self, filter):
956  self.widget.set_filter(filter)
957 
958 def main():
959  app = wx.App()
960 
961  frame = SmachViewerFrame()
962  frame.set_filter('dot')
963 
964  frame.Show()
965 
966  app.MainLoop()
967 
968 if __name__ == '__main__':
969  rospy.init_node('smach_viewer',anonymous=False, disable_signals=True,log_level=rospy.INFO)
970 
971  main()
test_tools.headless_smach_viewer.SmachViewerFrame._update_tree_thread
_update_tree_thread
Definition: headless_smach_viewer.py:592
test_tools.headless_smach_viewer.ContainerNode.update_status
def update_status(self, msg)
Definition: headless_smach_viewer.py:134
test_tools.headless_smach_viewer.SmachViewerFrame.ud_txt
ud_txt
Definition: headless_smach_viewer.py:542
test_tools.headless_smach_viewer.SmachViewerFrame.width_spinner
width_spinner
Definition: headless_smach_viewer.py:489
test_tools.headless_smach_viewer.ContainerNode._children
_children
Definition: headless_smach_viewer.py:99
test_tools.headless_smach_viewer.SmachViewerFrame.content_splitter
content_splitter
Definition: headless_smach_viewer.py:446
test_tools.headless_smach_viewer.ContainerNode._dir
_dir
Definition: headless_smach_viewer.py:97
test_tools.headless_smach_viewer.SmachViewerFrame.path_input
path_input
Definition: headless_smach_viewer.py:535
test_tools.headless_smach_viewer.SmachViewerFrame._status_msg_update
def _status_msg_update(self, msg)
Definition: headless_smach_viewer.py:756
test_tools.headless_smach_viewer.SmachViewerFrame._structure_msg_update
def _structure_msg_update(self, msg, server_name)
Definition: headless_smach_viewer.py:707
test_tools.headless_smach_viewer.SmachViewerFrame.select_cb
def select_cb(self, item, event)
Definition: headless_smach_viewer.py:645
test_tools.headless_smach_viewer.SmachViewerFrame.set_label_width
def set_label_width(self, event)
Definition: headless_smach_viewer.py:633
test_tools.headless_smach_viewer.ContainerNode._internal_outcomes
_internal_outcomes
Definition: headless_smach_viewer.py:100
test_tools.headless_smach_viewer.ContainerNode.set_styles
def set_styles(self, selected_paths, depth, max_depth, items, subgraph_shapes, containers)
Definition: headless_smach_viewer.py:325
test_tools.headless_smach_viewer.SmachViewerFrame._status_subs
_status_subs
Definition: headless_smach_viewer.py:574
test_tools.headless_smach_viewer.ContainerNode._structure_changed
_structure_changed
Definition: headless_smach_viewer.py:142
test_tools.headless_smach_viewer.get_label
def get_label(path)
Definition: headless_smach_viewer.py:72
test_tools.headless_smach_viewer.main
def main()
Definition: headless_smach_viewer.py:958
test_tools.headless_smach_viewer.SmachViewerFrame._show_all_transitions
_show_all_transitions
Definition: headless_smach_viewer.py:502
test_tools.headless_smach_viewer.ContainerNode._outcomes_from
_outcomes_from
Definition: headless_smach_viewer.py:101
test_tools.headless_smach_viewer.ContainerNode._last_active_states
_last_active_states
Definition: headless_smach_viewer.py:109
test_tools.headless_smach_viewer.SmachViewerFrame._update_tree
def _update_tree(self)
Definition: headless_smach_viewer.py:873
test_tools.headless_smach_viewer.SmachViewerFrame.ShowControlsDialog
def ShowControlsDialog(self, event)
Definition: headless_smach_viewer.py:946
test_tools.headless_smach_viewer.SmachViewerFrame._top_containers
_top_containers
Definition: headless_smach_viewer.py:438
test_tools.headless_smach_viewer.SmachViewerFrame.set_filter
def set_filter(self, filter)
Definition: headless_smach_viewer.py:955
test_tools.headless_smach_viewer.ContainerNode._outcomes_to
_outcomes_to
Definition: headless_smach_viewer.py:102
test_tools.headless_smach_viewer.ContainerNode._local_data
_local_data
Definition: headless_smach_viewer.py:110
test_tools.headless_smach_viewer.SmachViewerFrame._label_wrapper
_label_wrapper
Definition: headless_smach_viewer.py:495
test_tools.headless_smach_viewer.SmachViewerFrame._needs_zoom
_needs_zoom
Definition: headless_smach_viewer.py:582
test_tools.headless_smach_viewer.SmachViewerFrame.OnQuit
def OnQuit(self, event)
Definition: headless_smach_viewer.py:595
test_tools.headless_smach_viewer.ContainerNode.get_dotcode
def get_dotcode(self, selected_paths, closed_paths, depth, max_depth, containers, show_all, label_wrapper, attrs={})
Definition: headless_smach_viewer.py:169
test_tools.headless_smach_viewer.ContainerNode._label
_label
Definition: headless_smach_viewer.py:96
test_tools.headless_smach_viewer.SmachViewerFrame._selected_paths
_selected_paths
Definition: headless_smach_viewer.py:570
test_tools.headless_smach_viewer.SmachViewerFrame.statusbar
statusbar
Definition: headless_smach_viewer.py:558
test_tools.headless_smach_viewer.SmachViewerFrame.add_to_tree
def add_to_tree(self, path, parent)
Definition: headless_smach_viewer.py:883
test_tools.headless_smach_viewer.SmachViewerFrame._path
_path
Definition: headless_smach_viewer.py:581
test_tools.headless_smach_viewer.SmachViewerFrame._structure_changed
_structure_changed
Definition: headless_smach_viewer.py:583
test_tools.headless_smach_viewer.SmachViewerFrame.ud_gs
ud_gs
Definition: headless_smach_viewer.py:531
test_tools.headless_smach_viewer.get_parent_path
def get_parent_path(path)
Definition: headless_smach_viewer.py:63
test_tools.headless_smach_viewer.graph_attr_string
def graph_attr_string(attrs)
Helper Functions.
Definition: headless_smach_viewer.py:53
test_tools.headless_smach_viewer.SmachViewerFrame
Definition: headless_smach_viewer.py:429
test_tools.headless_smach_viewer.ContainerNode._active_states
_active_states
Definition: headless_smach_viewer.py:108
test_tools.headless_smach_viewer.SmachViewerFrame._update_cond
_update_cond
Definition: headless_smach_viewer.py:439
test_tools.headless_smach_viewer.SmachViewerFrame._server_list_thread
_server_list_thread
Definition: headless_smach_viewer.py:587
test_tools.headless_smach_viewer.ContainerNode._info
_info
Definition: headless_smach_viewer.py:111
test_tools.headless_smach_viewer.SmachViewerFrame.ud_win
ud_win
Definition: headless_smach_viewer.py:530
test_tools.headless_smach_viewer.SmachViewerFrame.set_dotcode
def set_dotcode(self, dotcode, zoom=True)
Definition: headless_smach_viewer.py:857
test_tools.headless_smach_viewer.SmachViewerFrame.on_set_initial_state
def on_set_initial_state(self, event)
Definition: headless_smach_viewer.py:612
test_tools.headless_smach_viewer.ContainerNode._initial_states
_initial_states
Definition: headless_smach_viewer.py:107
test_tools.headless_smach_viewer.SmachViewerFrame.is_button
is_button
Definition: headless_smach_viewer.py:546
test_tools.headless_smach_viewer.SmachViewerFrame.update_graph
def update_graph(self)
Definition: headless_smach_viewer.py:607
test_tools.headless_smach_viewer.SmachViewerFrame.__init__
def __init__(self)
Definition: headless_smach_viewer.py:433
test_tools.headless_smach_viewer.SmachViewerFrame.path_combo
path_combo
Definition: headless_smach_viewer.py:471
test_tools.headless_smach_viewer.SmachViewerFrame._update_graph_thread
_update_graph_thread
Definition: headless_smach_viewer.py:590
test_tools.headless_smach_viewer.SmachViewerFrame.selection_changed
def selection_changed(self, event)
Definition: headless_smach_viewer.py:659
test_tools.headless_smach_viewer.SmachViewerFrame._update_graph
def _update_graph(self)
Definition: headless_smach_viewer.py:782
test_tools.headless_smach_viewer.SmachViewerFrame.OnExit
def OnExit(self, event)
Definition: headless_smach_viewer.py:952
test_tools.headless_smach_viewer.ContainerNode.update_structure
def update_structure(self, msg)
Definition: headless_smach_viewer.py:113
test_tools.headless_smach_viewer.SmachViewerFrame.toggle_all_transitions
def toggle_all_transitions(self, event)
Definition: headless_smach_viewer.py:639
test_tools.headless_smach_viewer.SmachViewerFrame.widget
widget
Definition: headless_smach_viewer.py:516
test_tools.headless_smach_viewer.ContainerNode._server_name
_server_name
Definition: headless_smach_viewer.py:93
test_tools.headless_smach_viewer.SmachViewerFrame._containers
_containers
Definition: headless_smach_viewer.py:437
test_tools.headless_smach_viewer.SmachViewerFrame._client
_client
Definition: headless_smach_viewer.py:568
test_tools.headless_smach_viewer.SmachViewerFrame._max_depth
_max_depth
Definition: headless_smach_viewer.py:484
test_tools.headless_smach_viewer.hex2t
def hex2t(color_str)
Definition: headless_smach_viewer.py:77
test_tools.headless_smach_viewer.SmachViewerFrame.OnIdle
def OnIdle(self, event)
Definition: headless_smach_viewer.py:905
test_tools.headless_smach_viewer.SmachViewerFrame.set_depth
def set_depth(self, event)
Definition: headless_smach_viewer.py:627
test_tools.headless_smach_viewer.ContainerNode._path
_path
Definition: headless_smach_viewer.py:94
test_tools.headless_smach_viewer.attr_string
def attr_string(attrs)
Definition: headless_smach_viewer.py:58
test_tools.headless_smach_viewer.SmachViewerFrame._structure_subs
_structure_subs
Definition: headless_smach_viewer.py:573
test_tools.headless_smach_viewer.SmachViewerFrame.tree
tree
Definition: headless_smach_viewer.py:522
test_tools.headless_smach_viewer.SmachViewerFrame.set_path
def set_path(self, event)
Definition: headless_smach_viewer.py:621
test_tools.headless_smach_viewer.SmachViewerFrame._update_server_list
def _update_server_list(self)
Definition: headless_smach_viewer.py:912
test_tools.headless_smach_viewer.SmachViewerFrame._keep_running
_keep_running
Definition: headless_smach_viewer.py:586
test_tools.headless_smach_viewer.ContainerNode
Definition: headless_smach_viewer.py:82
test_tools.headless_smach_viewer.SmachViewerFrame._tree_nodes
_tree_nodes
Definition: headless_smach_viewer.py:879
test_tools.headless_smach_viewer.ContainerNode._container_outcomes
_container_outcomes
Definition: headless_smach_viewer.py:104
test_tools.headless_smach_viewer.SmachViewerFrame.append_tree
def append_tree(self, container, parent=None)
Definition: headless_smach_viewer.py:898
test_tools.headless_smach_viewer.SmachViewerFrame._needs_refresh
_needs_refresh
Definition: headless_smach_viewer.py:440
test_tools.headless_smach_viewer.ContainerNode.__init__
def __init__(self, server_name, msg)
Definition: headless_smach_viewer.py:91
test_tools.headless_smach_viewer.SmachViewerFrame.depth_spinner
depth_spinner
Definition: headless_smach_viewer.py:478
data_collector.start
start
Definition: data_collector.py:59