smach_viewer_base.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 
3 import threading
4 
5 import pickle
6 import base64
7 import re
8 import roslib
9 import rospy
10 import smach
11 import smach_ros
12 import sys
13 
14 from smach_viewer.text_wrapper import TextWrapper
15 from smach_viewer.utils import attr_string
16 from smach_viewer.utils import graph_attr_string
17 
18 from smach_msgs.msg import SmachContainerStatus
19 from smach_msgs.msg import SmachContainerStructure
20 
21 
22 class ContainerNode(object):
23  """
24  This class represents a given container in a running SMACH system.
25 
26  Its primary use is to generate dotcode for a SMACH container. It has
27  methods for responding to structure and status messages from a SMACH
28  introspection server, as well as methods for updating the styles of a
29  graph once it's been drawn.
30  """
31  def __init__(self, server_name, msg):
32  # Store path info
33  self._server_name = server_name
34  self._path = msg.path
35  splitpath = msg.path.split('/')
36  self._label = splitpath[-1]
37  self._dir = '/'.join(splitpath[0:-1])
38 
39  self._children = msg.children
40  self._internal_outcomes = msg.internal_outcomes
41  self._outcomes_from = msg.outcomes_from
42  self._outcomes_to = msg.outcomes_to
43 
44  self._container_outcomes = msg.container_outcomes
45 
46  # Status
47  self._initial_states = []
48  self._active_states = []
50  self._local_data = smach.UserData()
51  self._info = ''
52 
53  def update_structure(self, msg):
54  """Update the structure of this container from a given message. Return True if anything changes."""
55  needs_update = False
56 
57  if self._children != msg.children\
58  or self._internal_outcomes != msg.internal_outcomes\
59  or self._outcomes_from != msg.outcomes_from\
60  or self._outcomes_to != msg.outcomes_to\
61  or self._container_outcomes != msg.container_outcomes:
62  needs_update = True
63 
64  if needs_update:
65  self._children = msg.children
66  self._internal_outcomes = msg.internal_outcomes
67  self._outcomes_from = msg.outcomes_from
68  self._outcomes_to = msg.outcomes_to
69 
70  self._container_outcomes = msg.container_outcomes
71 
72  return needs_update
73 
74  def _load_local_data(self, msg):
75  """Unpack the user data"""
76  if re.match(r'^[A-Za-z0-9+/]*={0,2}$', msg.local_data):
77  local_data_raw_bytes = base64.b64decode(msg.local_data)
78  local_data = pickle.loads(local_data_raw_bytes)
79  elif isinstance(msg.local_data, str) and sys.version_info.major >= 3:
80  local_data = pickle.loads(msg.local_data.encode('utf-8'))
81  else:
82  local_data = pickle.loads(msg.local_data)
83  return local_data
84 
85  def update_status(self, msg):
86  """Update the known userdata and active state set and return True if the graph needs to be redrawn."""
87 
88  # Initialize the return value
89  needs_update = False
90 
91  # Check if the initial states or active states have changed
92  if set(msg.initial_states) != set(self._initial_states):
93  self._structure_changed = True
94  needs_update = True
95  if set(msg.active_states) != set(self._active_states):
96  needs_update = True
97 
98  # Store the initial and active states
99  self._initial_states = msg.initial_states
101  self._active_states = msg.active_states
102 
103  # Unpack the user data
104  while not rospy.is_shutdown():
105  try:
106  self._local_data._data = self._load_local_data(msg)
107  break
108  except ImportError as ie:
109  # This will only happen once for each package
110  modulename = ie.args[0][16:]
111  packagename = modulename[0:modulename.find('.')]
112  roslib.load_manifest(packagename)
113  self._local_data._data = self._load_local_data(msg)
114 
115  # Store the info string
116  self._info = msg.info
117 
118  return needs_update
119 
120  def get_dotcode(self, selected_paths, closed_paths, depth, max_depth, containers, show_all, label_wrapper, attrs={}):
121  """Generate the dotcode representing this container.
122 
123  @param selected_paths: The paths to nodes that are selected
124  @closed paths: The paths that shouldn't be expanded
125  @param depth: The depth to start traversing the tree
126  @param max_depth: The depth to which we should traverse the tree
127  @param containers: A dict of containers keyed by their paths
128  @param show_all: True if implicit transitions should be shown
129  @param label_wrapper: A text wrapper for wrapping element names
130  @param attrs: A dict of dotcode attributes for this cluster
131  """
132 
133  dotstr = 'subgraph "cluster_%s" {\n' % (self._path)
134  if depth == 0:
135  #attrs['style'] = 'filled,rounded'
136  attrs['color'] = '#00000000'
137  attrs['fillcolor'] = '#0000000F'
138  #attrs['rank'] = 'max'
139 
140  #,'succeeded','aborted','preempted'attrs['label'] = self._label
141  dotstr += graph_attr_string(attrs)
142 
143  # Add start/terimate target
144  proxy_attrs = {
145  'URL':self._path,
146  'shape':'plaintext',
147  'color':'gray',
148  'fontsize':'18',
149  'fontweight':'18',
150  'rank':'min',
151  'height':'0.01'}
152  proxy_attrs['label'] = '\\n'.join(label_wrapper.wrap(self._label))
153  dotstr += '"%s" %s;\n' % (
154  '/'.join([self._path,'__proxy__']),
155  attr_string(proxy_attrs))
156 
157  # Check if we should expand this container
158  if max_depth == -1 or depth <= max_depth:
159  # Add container outcomes
160  dotstr += 'subgraph "cluster_%s" {\n' % '/'.join([self._path,'__outcomes__'])
161  outcomes_attrs = {
162  'style':'rounded,filled',
163  'rank':'sink',
164  'color':'#FFFFFFFF',#'#871C34',
165  'fillcolor':'#FFFFFF00'#'#FE464f3F'#'#DB889A'
166  }
167  dotstr += graph_attr_string(outcomes_attrs)
168 
169  for outcome_label in self._container_outcomes:
170  outcome_path = ':'.join([self._path,outcome_label])
171  outcome_attrs = {
172  'shape':'box',
173  'height':'0.3',
174  'style':'filled,rounded',
175  'fontsize':'12',
176  'fillcolor':'#FE464f',#'#EDC2CC',
177  'color':'#780006',#'#EBAEBB',
178  'fontcolor':'#780006',#'#EBAEBB',
179  'label':'',
180  'xlabel':'\\n'.join(label_wrapper.wrap(outcome_label)),
181  'URL':':'.join([self._path,outcome_label])
182  }
183  dotstr += '"%s" %s;\n' % (outcome_path,attr_string(outcome_attrs))
184  dotstr += "}\n"
185 
186  # Iterate over children
187  for child_label in self._children:
188  child_path = '/'.join([self._path,child_label])
189  # Generate dotcode for children
190  if child_path in containers:
191  if child_label in self._active_states:
192  child_color = '#5C7600FF'
193  child_fillcolor = '#C0F700FF'
194  child_linewidth = 5
195  else:
196  child_color = '#000000FF'
197  child_fillcolor = 'gray'
198  child_linewidth = 2
199 
200  child_attrs = {
201  'style': 'filled,setlinewidth({}),rounded'.format(child_linewidth),
202  'color': child_color,
203  'fillcolor': child_fillcolor,
204  }
205 
206  dotstr += containers[child_path].get_dotcode(
207  selected_paths,
208  closed_paths,
209  depth+1, max_depth,
210  containers,
211  show_all,
212  label_wrapper,
213  child_attrs
214  )
215  else:
216  if child_label in self._active_states:
217  child_color = '#5C7600FF'
218  child_fillcolor = '#C0F700FF'
219  child_linewidth = 5
220  else:
221  child_color = '#000000FF'
222  child_fillcolor = '#FFFFFFFF'
223  child_linewidth = 2
224 
225  child_attrs = {
226  'style': 'filled,setlinewidth({})'.format(child_linewidth),
227  'color': child_color,
228  'fillcolor': child_fillcolor,
229  'label': '\\n'.join(label_wrapper.wrap(child_label)),
230  'URL': child_path,
231  }
232  dotstr += '"%s" %s;\n' % (child_path, attr_string(child_attrs))
233 
234  # Iterate over edges
235  internal_edges = list(zip(
236  self._internal_outcomes,
237  self._outcomes_from,
238  self._outcomes_to))
239 
240  # Add edge from container label to initial state
241  internal_edges += [('','__proxy__',initial_child) for initial_child in self._initial_states]
242 
243  has_explicit_transitions = []
244  for (outcome_label,from_label,to_label) in internal_edges:
245  if to_label != 'None' or outcome_label == to_label:
246  has_explicit_transitions.append(from_label)
247 
248  # Draw internal edges
249  for (outcome_label,from_label,to_label) in internal_edges:
250 
251  from_path = '/'.join([self._path, from_label])
252 
253  if show_all \
254  or to_label != 'None'\
255  or from_label not in has_explicit_transitions \
256  or (outcome_label == from_label) \
257  or from_path in containers:
258  # Set the implicit target of this outcome
259  if to_label == 'None':
260  to_label = outcome_label
261 
262  to_path = '/'.join([self._path, to_label])
263 
264  edge_attrs = {
265  'URL':':'.join([from_path,outcome_label,to_path]),
266  'fontsize':'12',
267  'label':'',
268  'xlabel':'\\n'.join(label_wrapper.wrap(outcome_label))}
269  edge_attrs['style'] = 'setlinewidth(2)'
270 
271  # Hide implicit
272  #if not show_all and to_label == outcome_label:
273  # edge_attrs['style'] += ',invis'
274 
275  from_key = '"%s"' % from_path
276  if from_path in containers:
277  if max_depth == -1 or depth+1 <= max_depth:
278  from_key = '"%s:%s"' % ( from_path, outcome_label)
279  else:
280  edge_attrs['ltail'] = 'cluster_'+from_path
281  from_path = '/'.join([from_path,'__proxy__'])
282  from_key = '"%s"' % ( from_path )
283 
284  to_key = ''
285  if to_label in self._container_outcomes:
286  to_key = '"%s:%s"' % (self._path,to_label)
287  edge_attrs['color'] = '#00000055'# '#780006'
288  else:
289  if to_path in containers:
290  edge_attrs['lhead'] = 'cluster_'+to_path
291  to_path = '/'.join([to_path,'__proxy__'])
292  to_key = '"%s"' % to_path
293 
294  dotstr += '%s -> %s %s;\n' % (
295  from_key, to_key, attr_string(edge_attrs))
296 
297  dotstr += '}\n'
298  return dotstr
299 
300 
301 class SmachViewerBase(object):
302 
303  _container_class = ContainerNode
304 
305  def __init__(self):
306  # Create graph
307  self._containers = {}
308  self._top_containers = {}
309  self._update_cond = threading.Condition()
310  self._needs_refresh = True
311  self.dotstr = ''
312 
313  # smach introspection client
314  self._client = smach_ros.IntrospectionClient()
315  self._containers = {}
316  self._selected_paths = []
317 
318  # Message subscribers
319  self._structure_subs = {}
320  self._status_subs = {}
321 
322  self._path = '/'
323  self._needs_zoom = True
324  self._structure_changed = True
325  self._max_depth = -1
327  self._label_wrapper = TextWrapper(40, break_long_words=True)
328 
329  # Start a thread in the background to update the server list
330  self._keep_running = True
331  self._server_list_thread = threading.Thread(
332  target=self._update_server_list)
333  self._server_list_thread.start()
334 
335  self._update_graph_thread = threading.Thread(target=self._update_graph)
336  self._update_graph_thread.start()
337 
338  def _update_graph(self):
339  """This thread continuously updates the graph when it changes.
340 
341  The graph gets updated in one of two ways:
342 
343  1: The structure of the SMACH plans has changed, or the display
344  settings have been changed. In this case, the dotcode needs to be
345  regenerated.
346 
347  2: The status of the SMACH plans has changed. In this case, we only
348  need to change the styles of the graph.
349  """
350  while self._keep_running and not rospy.is_shutdown():
351  with self._update_cond:
352  self._update_graph_step()
353 
355  # Wait for the update condition to be triggered
356  self._update_cond.wait()
357 
358  containers_to_update = {}
359  if self._path in self._containers:
360  # Some non-root path
361  containers_to_update = {
362  self._path: self._containers[self._path]}
363  elif self._path == '/':
364  # Root path
365  containers_to_update = self._top_containers
366 
367  # Check if we need to re-generate the dotcode
368  # (if the structure changed)
369  # TODO(???): needs_zoom is a misnomer
370  if self._structure_changed or self._needs_zoom:
371  dotstr = "digraph {\n\t"
372  dotstr += ';'.join([
373  "compound=true",
374  "outputmode=nodesfirst",
375  "labeljust=l",
376  "nodesep=0.5",
377  "minlen=2",
378  "mclimit=5",
379  "clusterrank=local",
380  "ranksep=0.75",
381  # "remincross=true",
382  # "rank=sink",
383  "ordering=\"\"",
384  ])
385  dotstr += ";\n"
386 
387  # Generate the rest of the graph
388  # TODO(???): Only re-generate dotcode
389  # for containers that have changed
390  for path, tc in containers_to_update.items():
391  dotstr += tc.get_dotcode(
392  self._selected_paths, [],
393  0, self._max_depth,
394  self._containers,
396  self._label_wrapper)
397  if len(containers_to_update) == 0:
398  dotstr += '"__empty__" ' \
399  + '[label="Path not available.", shape="plaintext"]'
400 
401  dotstr += '\n}\n'
402  self.dotstr = dotstr
403  return containers_to_update
404 
406  """Update the list of known SMACH introspection servers."""
407  while self._keep_running:
408  # Update the server list
409  server_names = self._client.get_servers()
410  new_server_names = [
411  sn for sn in server_names if sn not in self._status_subs]
412 
413  # Create subscribers for new servers
414  for server_name in new_server_names:
415  self._structure_subs[server_name] = rospy.Subscriber(
416  server_name+smach_ros.introspection.STRUCTURE_TOPIC,
417  SmachContainerStructure,
418  callback=self._structure_msg_update,
419  callback_args=server_name,
420  queue_size=50)
421 
422  self._status_subs[server_name] = rospy.Subscriber(
423  server_name+smach_ros.introspection.STATUS_TOPIC,
424  SmachContainerStatus,
425  callback=self._status_msg_update,
426  queue_size=50)
427 
428  # This doesn't need to happen very often
429  rospy.sleep(1.0)
430 
431  def _structure_msg_update(self, msg, server_name):
432  """Update the structure of the SMACH plan (re-generate the dotcode)."""
433 
434  # Just return if we're shutting down
435  if not self._keep_running:
436  return
437 
438  # Get the node path
439  path = msg.path
440  pathsplit = path.split('/')
441  parent_path = '/'.join(pathsplit[0:-1])
442 
443  rospy.logdebug("RECEIVED: "+path)
444  rospy.logdebug("CONTAINERS: "+str(list(self._containers.keys())))
445 
446  # Initialize redraw flag
447  needs_redraw = False
448 
449  if path in self._containers:
450  rospy.logdebug("UPDATING: "+path)
451 
452  # Update the structure of this known container
453  needs_redraw = self._containers[path].update_structure(msg)
454  else:
455  rospy.logdebug("CONSTRUCTING: "+path)
456 
457  # Create a new container
458  container = self._container_class(server_name, msg)
459  self._containers[path] = container
460 
461  # Store this as a top container if it has no parent
462  if parent_path == '':
463  self._top_containers[path] = container
464 
465  # We need to redraw thhe graph
466  # if this container's parent is already known
467  if parent_path in self._containers:
468  needs_redraw = True
469 
470  # Update the graph if necessary
471  if needs_redraw:
472  with self._update_cond:
473  self._structure_changed = True
474  # TODO(???): Make it so you can disable this
475  self._needs_zoom = True
476  self._update_cond.notify_all()
477 
478  def _status_msg_update(self, msg):
479  """Process status messages."""
480 
481  # Check if we're in the process of shutting down
482  if not self._keep_running:
483  return
484 
485  # Get the path to the updating conainer
486  path = msg.path
487  rospy.logdebug("STATUS MSG: "+path)
488 
489  # Check if this is a known container
490  if path in self._containers:
491  # Get the container and check
492  # if the status update requires regeneration
493  container = self._containers[path]
494  if container.update_status(msg):
495  with self._update_cond:
496  self._update_cond.notify_all()
497 
498  def kill(self):
499  with self._update_cond:
500  self._keep_running = False
501  self._update_cond.notify_all()
502 
503  self._server_list_thread.join()
504  self._update_graph_thread.join()
smach_viewer.utils.attr_string
def attr_string(attrs)
Definition: utils.py:8
smach_viewer.smach_viewer_base.SmachViewerBase._needs_zoom
_needs_zoom
Definition: smach_viewer_base.py:323
smach_viewer.smach_viewer_base.ContainerNode
Definition: smach_viewer_base.py:22
smach_viewer.smach_viewer_base.ContainerNode.__init__
def __init__(self, server_name, msg)
Definition: smach_viewer_base.py:31
smach_viewer.smach_viewer_base.SmachViewerBase.dotstr
dotstr
Definition: smach_viewer_base.py:311
smach_viewer.smach_viewer_base.SmachViewerBase._needs_refresh
_needs_refresh
Definition: smach_viewer_base.py:310
smach_viewer.smach_viewer_base.ContainerNode.update_structure
def update_structure(self, msg)
Definition: smach_viewer_base.py:53
smach_viewer.smach_viewer_base.ContainerNode._load_local_data
def _load_local_data(self, msg)
Definition: smach_viewer_base.py:74
smach_viewer.smach_viewer_base.SmachViewerBase.kill
def kill(self)
Definition: smach_viewer_base.py:498
smach_viewer.smach_viewer_base.ContainerNode._initial_states
_initial_states
Definition: smach_viewer_base.py:47
smach_viewer.smach_viewer_base.ContainerNode.update_status
def update_status(self, msg)
Definition: smach_viewer_base.py:85
smach_viewer.smach_viewer_base.ContainerNode._label
_label
Definition: smach_viewer_base.py:36
smach_viewer.smach_viewer_base.SmachViewerBase._update_cond
_update_cond
Definition: smach_viewer_base.py:309
smach_viewer.smach_viewer_base.SmachViewerBase._server_list_thread
_server_list_thread
Definition: smach_viewer_base.py:331
smach_viewer.smach_viewer_base.SmachViewerBase._show_all_transitions
_show_all_transitions
Definition: smach_viewer_base.py:326
smach_viewer.smach_viewer_base.ContainerNode._info
_info
Definition: smach_viewer_base.py:51
smach_viewer.smach_viewer_base.SmachViewerBase._update_graph_thread
_update_graph_thread
Definition: smach_viewer_base.py:335
smach_viewer.smach_viewer_base.SmachViewerBase._structure_changed
_structure_changed
Definition: smach_viewer_base.py:324
smach_viewer.smach_viewer_base.ContainerNode._local_data
_local_data
Definition: smach_viewer_base.py:50
smach_viewer.smach_viewer_base.SmachViewerBase._update_graph_step
def _update_graph_step(self)
Definition: smach_viewer_base.py:354
smach_viewer.smach_viewer_base.ContainerNode._server_name
_server_name
Definition: smach_viewer_base.py:33
smach_viewer.smach_viewer_base.SmachViewerBase.__init__
def __init__(self)
Definition: smach_viewer_base.py:305
smach_viewer.smach_viewer_base.SmachViewerBase._status_msg_update
def _status_msg_update(self, msg)
Definition: smach_viewer_base.py:478
smach_viewer.smach_viewer_base.SmachViewerBase._structure_subs
_structure_subs
Definition: smach_viewer_base.py:319
smach_viewer.smach_viewer_base.SmachViewerBase._label_wrapper
_label_wrapper
Definition: smach_viewer_base.py:327
smach_viewer.smach_viewer_base.SmachViewerBase._path
_path
Definition: smach_viewer_base.py:322
smach_viewer.smach_viewer_base.ContainerNode._active_states
_active_states
Definition: smach_viewer_base.py:48
smach_viewer.smach_viewer_base.SmachViewerBase._selected_paths
_selected_paths
Definition: smach_viewer_base.py:316
smach_viewer.smach_viewer_base.ContainerNode._dir
_dir
Definition: smach_viewer_base.py:37
smach_viewer.smach_viewer_base.SmachViewerBase._keep_running
_keep_running
Definition: smach_viewer_base.py:330
smach_viewer.smach_viewer_base.SmachViewerBase._structure_msg_update
def _structure_msg_update(self, msg, server_name)
Definition: smach_viewer_base.py:431
smach_viewer.smach_viewer_base.ContainerNode._container_outcomes
_container_outcomes
Definition: smach_viewer_base.py:44
smach_viewer.smach_viewer_base.ContainerNode._last_active_states
_last_active_states
Definition: smach_viewer_base.py:49
smach_viewer.smach_viewer_base.SmachViewerBase._update_graph
def _update_graph(self)
Definition: smach_viewer_base.py:338
smach_viewer.smach_viewer_base.ContainerNode._path
_path
Definition: smach_viewer_base.py:34
smach_viewer.smach_viewer_base.ContainerNode._outcomes_from
_outcomes_from
Definition: smach_viewer_base.py:41
smach_viewer.smach_viewer_base.ContainerNode._internal_outcomes
_internal_outcomes
Definition: smach_viewer_base.py:40
smach_viewer.smach_viewer_base.SmachViewerBase._client
_client
Definition: smach_viewer_base.py:314
smach_viewer.smach_viewer_base.SmachViewerBase._containers
_containers
Definition: smach_viewer_base.py:307
smach_viewer.text_wrapper.TextWrapper
Definition: text_wrapper.py:31
smach_viewer.smach_viewer_base.ContainerNode._children
_children
Definition: smach_viewer_base.py:39
smach_viewer.smach_viewer_base.ContainerNode._outcomes_to
_outcomes_to
Definition: smach_viewer_base.py:42
smach_viewer.smach_viewer_base.SmachViewerBase._status_subs
_status_subs
Definition: smach_viewer_base.py:320
smach_viewer.smach_viewer_base.ContainerNode._structure_changed
_structure_changed
Definition: smach_viewer_base.py:93
smach_viewer.smach_viewer_base.SmachViewerBase._update_server_list
def _update_server_list(self)
Definition: smach_viewer_base.py:405
smach_viewer.smach_viewer_base.SmachViewerBase._top_containers
_top_containers
Definition: smach_viewer_base.py:308
smach_viewer.utils
Definition: utils.py:1
smach_viewer.smach_viewer_base.ContainerNode.get_dotcode
def get_dotcode(self, selected_paths, closed_paths, depth, max_depth, containers, show_all, label_wrapper, attrs={})
Definition: smach_viewer_base.py:120
smach_viewer.smach_viewer_base.SmachViewerBase
Definition: smach_viewer_base.py:301
smach_viewer.utils.graph_attr_string
def graph_attr_string(attrs)
Definition: utils.py:2
smach_viewer.smach_viewer_base.SmachViewerBase._container_class
_container_class
Definition: smach_viewer_base.py:303
smach_viewer.text_wrapper
Definition: text_wrapper.py:1
smach_viewer.smach_viewer_base.SmachViewerBase._max_depth
_max_depth
Definition: smach_viewer_base.py:325


smach_viewer
Author(s): Jonathan Bohren
autogenerated on Thu Feb 20 2025 03:09:09