33 from __future__
import print_function
40 from smach_msgs.msg
import SmachContainerStatus, SmachContainerStructure
42 wxversion.select(
"2.8")
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'
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))+
']'
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])
69 parent_path =
'/'.join(path_tokens[0:1])
73 """Get the label of an xdot node."""
74 path_tokens = path.split(
'/')
75 return path_tokens[-1]
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)]
84 This class represents a given container in a running SMACH system.
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.
95 splitpath = msg.path.split(
'/')
97 self.
_dir =
'/'.join(splitpath[0:-1])
114 """Update the structure of this container from a given message. Return True if anything changes."""
135 """Update the known userdata and active state set and return True if the graph needs to be redrawn."""
153 while not rospy.is_shutdown():
155 self.
_local_data._data = pickle.loads(msg.local_data)
157 except ImportError
as ie:
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)
165 self.
_info = msg.info
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.
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
182 dotstr =
'subgraph "cluster_%s" {\n' % (self.
_path)
185 attrs[
'color'] =
'#00000000'
186 attrs[
'fillcolor'] =
'#0000000F'
201 proxy_attrs[
'label'] =
'\\n'.join(label_wrapper.wrap(self.
_label))
202 dotstr +=
'"%s" %s;\n' % (
203 '/'.join([self.
_path,
'__proxy__']),
207 if max_depth == -1
or depth <= max_depth:
209 dotstr +=
'subgraph "cluster_%s" {\n' %
'/'.join([self.
_path,
'__outcomes__'])
211 'style':
'rounded,filled',
214 'fillcolor':
'#FFFFFF00'
219 outcome_path =
':'.join([self.
_path,outcome_label])
223 'style':
'filled,rounded',
225 'fillcolor':
'#FE464f',
227 'fontcolor':
'#780006',
228 'label':
'\\n'.join(label_wrapper.wrap(outcome_label)),
229 'URL':
':'.join([self.
_path,outcome_label])
231 dotstr +=
'"%s" %s;\n' % (outcome_path,
attr_string(outcome_attrs))
237 'style':
'filled,setlinewidth(2)',
239 'fillcolor':
'#FFFFFF00'
242 child_path =
'/'.join([self.
_path,child_label])
244 if child_path
in containers:
245 child_attrs[
'style'] +=
',rounded'
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))
261 internal_edges = zip(
267 internal_edges += [(
'',
'__proxy__',initial_child)
for initial_child
in self.
_initial_states]
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)
275 for (outcome_label,from_label,to_label)
in internal_edges:
277 from_path =
'/'.join([self.
_path, from_label])
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:
285 if to_label ==
'None':
286 to_label = outcome_label
288 to_path =
'/'.join([self.
_path, to_label])
291 'URL':
':'.join([from_path,outcome_label,to_path]),
293 'label':
'\\n'.join(label_wrapper.wrap(outcome_label))}
294 edge_attrs[
'style'] =
'setlinewidth(2)'
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)
305 edge_attrs[
'ltail'] =
'cluster_'+from_path
306 from_path =
'/'.join([from_path,
'__proxy__'])
307 from_key =
'"%s"' % ( from_path )
311 to_key =
'"%s:%s"' % (self.
_path,to_label)
312 edge_attrs[
'color'] =
'#00000055'
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
319 dotstr +=
'%s -> %s %s;\n' % (
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.
328 This function is called recursively to update an entire tree.
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
341 container_shapes = subgraph_shapes['cluster_'+self._path]
342 container_color = (0,0,0,0)
343 container_fillcolor = (0,0,0,0)
345 for shape in container_shapes:
346 shape.pen.color = container_color
347 shape.pen.fillcolor = container_fillcolor
353 if max_depth == -1
or depth <= max_depth:
356 child_path =
'/'.join([self.
_path,child_label])
358 child_color = [0.5,0.5,0.5,1]
359 child_fillcolor = [1,1,1,1]
362 active_color =
hex2t(
'#5C7600FF')
363 active_fillcolor =
hex2t(
'#C0F700FF')
365 initial_color =
hex2t(
'#000000FF')
366 initial_fillcolor =
hex2t(
'#FFFFFFFF')
370 child_color = active_color
371 child_fillcolor = active_fillcolor
376 child_color = initial_color
380 if child_path
in selected_paths:
381 child_color =
hex2t(
'#FB000DFF')
384 if child_path
in containers:
385 subgraph_id =
'cluster_'+child_path
386 if subgraph_id
in subgraph_shapes:
388 child_fillcolor[3] = 0.25
390 child_fillcolor[3] = 0.25
393 v = 1.0-0.25*((depth+1)/float(max_depth))
396 child_fillcolor = [v,v,v,1.0]
399 for shape
in subgraph_shapes[
'cluster_'+child_path]:
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
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
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
431 This class provides a GUI application for viewing SMACH plans.
434 wx.Frame.__init__(self,
None, -1,
"Smach Viewer", size=(720,480))
442 vbox = wx.BoxSizer(wx.VERTICAL)
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)
461 graph_view = wx.Panel(nb,-1)
462 gv_vbox = wx.BoxSizer(wx.VERTICAL)
463 graph_view.SetSizer(gv_vbox)
466 toolbar = wx.ToolBar(graph_view, -1)
468 toolbar.AddControl(wx.StaticText(toolbar,-1,
"Path: "))
471 self.
path_combo = wx.ComboBox(toolbar, -1, style=wx.CB_DROPDOWN)
485 toolbar.AddControl(wx.StaticText(toolbar,-1,
" Depth: "))
496 toolbar.AddControl(wx.StaticText(toolbar,-1,
" Label Width: "))
500 toggle_all = wx.ToggleButton(toolbar,-1,
'Show Implicit')
504 toolbar.AddControl(wx.StaticText(toolbar,-1,
" "))
505 toolbar.AddControl(toggle_all)
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)) )
516 self.
widget = xdot.wxxdot.WxDotWindow(graph_view, -1)
518 gv_vbox.Add(toolbar, 0, wx.EXPAND)
519 gv_vbox.Add(self.
widget, 1, wx.EXPAND)
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")
528 borders = wx.LEFT | wx.RIGHT | wx.TOP
531 self.
ud_gs = wx.BoxSizer(wx.VERTICAL)
533 self.
ud_gs.Add(wx.StaticText(self.
ud_win,-1,
"Path:"),0, borders, border)
540 self.
ud_gs.Add(wx.StaticText(self.
ud_win,-1,
"Userdata:"),0, borders, border)
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)
549 self.
ud_gs.Add(self.
is_button,0,wx.EXPAND | wx.BOTTOM | borders, border)
568 self.
_client = smach_ros.IntrospectionClient()
576 self.Bind(wx.EVT_IDLE,self.
OnIdle)
577 self.Bind(wx.EVT_CLOSE,self.
OnQuit)
596 """Quit Event: kill threads and wait for join."""
608 """Notify all that the graph needs to be updated."""
613 """Event: Change the initial state of the server."""
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))
622 """Event: Change the viewable path and update the graph."""
628 """Event: Change the maximum target_depth and update the graph."""
634 """Event: Change the label wrapper width and update the graph."""
640 """Event: Change whether automatic transitions are hidden and update the graph."""
646 """Event: Click to select a graph node to display user data and update the graph."""
649 if event.ButtonUp(wx.MOUSE_BTN_LEFT):
656 wx.CommandEvent(wx.wxEVT_COMMAND_COMBOBOX_SELECTED,self.
path_input.GetId()))
660 """Event: Selection dropdown changed."""
664 if len(path_input_str) > 0:
666 path = path_input_str.split(
':')[0]
683 pos = self.
ud_txt.HitTestPos(wx.Point(0,0))
684 sel = self.
ud_txt.GetSelection()
688 for k, v
in container._local_data._data.items():
689 ud_str += str(k)+
": "
692 if vstr.find(
'\n') != -1:
697 self.
ud_txt.SetValue(ud_str)
700 self.
ud_txt.ShowPosition(pos[1])
702 self.
ud_txt.SetSelection(sel[0],sel[1])
708 """Update the structure of the SMACH plan (re-generate the dotcode)."""
716 pathsplit = path.split(
'/')
717 parent_path =
'/'.join(pathsplit[0:-1])
719 rospy.logdebug(
"RECEIVED: "+path)
720 rospy.logdebug(
"CONTAINERS: "+str(self.
_containers.keys()))
726 rospy.logdebug(
"UPDATING: "+path)
729 needs_redraw = self.
_containers[path].update_structure(msg)
731 rospy.logdebug(
"CONSTRUCTING: "+path)
738 if parent_path ==
'':
757 """Process status messages."""
765 rospy.logdebug(
"STATUS MSG: "+path)
771 if container.update_status(msg):
780 wx.CommandEvent(wx.wxEVT_COMMAND_COMBOBOX_SELECTED,self.
path_input.GetId()))
783 """This thread continuously updates the graph when it changes.
785 The graph gets updated in one of two ways:
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
791 2: The status of the SMACH plans has changed. In this case, we only
792 need to change the styles of the graph.
800 containers_to_update = {}
804 elif self.
_path ==
'/':
811 dotstr =
"digraph {\n\t"
814 "outputmode=nodesfirst",
829 for path,tc
in containers_to_update.items():
830 dotstr += tc.get_dotcode(
837 dotstr +=
'"__empty__" [label="Path not available.", shape="plaintext"]'
846 for path,tc
in containers_to_update.items():
851 self.
widget.subgraph_shapes,
858 """Set the xdot view's dotcode and refresh the display."""
861 self.SetTitle(
'Smach Viewer')
868 wx.PostEvent(self.GetEventHandler(), wx.IdleEvent())
870 with open(
"dump.dot",
"w+")
as dotfile:
871 dotfile.write(dotcode)
874 """Update the tree view."""
878 self.
tree.DeleteAllItems()
884 """Add a path to the tree view."""
892 child_path =
'/'.join([path,label])
896 self.
tree.AppendItem(container,label)
899 """Append an item to the tree view."""
901 node = self.
tree.AddRoot(container._label)
902 for child_label
in container._children:
903 self.
tree.AppendItem(node,child_label)
906 """Event: On Idle, refresh the display if necessary, then un-set the flag."""
913 """Update the list of known SMACH introspection servers."""
916 server_names = self.
_client.get_servers()
917 new_server_names = [sn
for sn
in server_names
if sn
not in self.
_status_subs]
920 for server_name
in new_server_names:
922 server_name+smach_ros.introspection.STRUCTURE_TOPIC,
923 SmachContainerStructure,
925 callback_args = server_name,
929 server_name+smach_ros.introspection.STATUS_TOPIC,
930 SmachContainerStatus,
947 dial = wx.MessageDialog(
None,
948 "Pan: Arrow Keys\nZoom: PageUp / PageDown\nZoom To Fit: F\nRefresh: R",
949 'Keyboard Controls', wx.OK)
962 frame.set_filter(
'dot')
968 if __name__ ==
'__main__':
969 rospy.init_node(
'smach_viewer',anonymous=
False, disable_signals=
True,log_level=rospy.INFO)