tool_utils.py
Go to the documentation of this file.
00001 import PyQt4.QtGui as qtg
00002 import PyQt4.QtCore as qtc
00003 #from PyQt4.QtGui import *
00004 #from PyQt4.QtCore import *
00005 import smach_ros
00006 import functools as ft
00007 import actionlib_msgs.msg as am
00008 import os.path as pt
00009 from tf_broadcast_server.srv import GetTransforms
00010 import rospy
00011 import actionlib
00012 import smach
00013 
00014 
00015 status_dict = {am.GoalStatus.PENDING   : 'PENDING',
00016                am.GoalStatus.ACTIVE    : 'ACTIVE',   
00017                am.GoalStatus.PREEMPTED : 'PREEMPTED',
00018                am.GoalStatus.SUCCEEDED : 'SUCCEEDED',
00019                am.GoalStatus.ABORTED   : 'ABORTED',  
00020                am.GoalStatus.REJECTED  : 'REJECTED', 
00021                am.GoalStatus.PREEMPTING: 'PREEMPTING',
00022                am.GoalStatus.RECALLING : 'RECALLING',
00023                am.GoalStatus.RECALLED  : 'RECALLED', 
00024                am.GoalStatus.LOST      : 'LOST'}    
00025 
00026 class ComboBox:
00027 
00028     def __init__(self):
00029         pass
00030 
00031     def create_box(self, pbox):
00032         self.combobox = qtg.QComboBox(pbox)
00033 
00034     ##
00035     # Set selected item
00036     #
00037     # @param item item to set (string)
00038     # @param create whether to create the item in this ComboBox if it isn't already there
00039     def set_text(self, item, create=True):
00040         idx = combobox_idx(self.combobox, item, create=create)
00041         if idx != -1:
00042             self.combobox.setCurrentIndex(idx)
00043 
00044     def text(self):
00045         return str(self.combobox.currentText())
00046 
00047 class FrameBox(ComboBox):
00048 
00049     def __init__(self, frames_service=None):
00050         ComboBox.__init__(self)
00051         if frames_service == None:
00052             frames_service = rospy.ServiceProxy('get_transforms', GetTransforms)
00053         self.frames_service = frames_service
00054 
00055     def create_box(self, pbox):
00056         ComboBox.create_box(self, pbox)
00057         for f in self.frames_service().frames:
00058             self.combobox.addItem(f)
00059         self.setEnabled = self.combobox.setEnabled
00060         return self.combobox
00061 
00062 def goal_status_to_string(status):
00063     return status_dict[status]
00064 
00065 def create_tool_button(name, container):
00066     button = qtg.QToolButton(container)
00067     sizePolicy = qtg.QSizePolicy(qtg.QSizePolicy.Expanding, qtg.QSizePolicy.Fixed)
00068     sizePolicy.setHorizontalStretch(0)
00069     sizePolicy.setVerticalStretch(0)
00070     sizePolicy.setHeightForWidth(button.sizePolicy().hasHeightForWidth())
00071     button.setSizePolicy(sizePolicy)
00072     button.setAutoRaise(True)
00073     button.setObjectName("button")
00074     button.setCheckable(True)
00075     container.layout().addWidget(button)
00076     button.setText(qtg.QApplication.translate("RCommanderWindow", name, None, qtg.QApplication.UnicodeUTF8))
00077     return button
00078 
00079 def make_radio_box(parent, options, name_preffix):
00080     container_name = name_preffix + '_radio_box'
00081 
00082     container = qtg.QWidget(parent)
00083     container.setObjectName(container_name)
00084     hlayout = qtg.QHBoxLayout(container)
00085     radio_buttons = []
00086 
00087     for option in options:
00088         r = qtg.QRadioButton(container)
00089         r.setObjectName(name_preffix + '_' + option)
00090         r.setText(option)
00091         hlayout.addWidget(r)
00092         radio_buttons.append(r)
00093     radio_buttons[0].setChecked(True)
00094 
00095     return container, radio_buttons
00096 
00097 ##
00098 # Finds index of item in QComboBox
00099 #
00100 # @param combobox QComboBox object
00101 # @param name name of selection
00102 # @param create whether to create the item if it's not in the list
00103 def combobox_idx(combobox, name, create=True):
00104     if name == None:
00105         name = ' '
00106     idx = combobox.findText(name)
00107     if idx == -1:
00108         if create == True:
00109             combobox.addItem(name)
00110             idx = combobox.findText(name)
00111             return idx
00112         else:
00113             return -1
00114     return idx
00115 
00116 
00117 def double_spin_box(parent, minv, maxv, step):
00118     box = qtg.QDoubleSpinBox(parent)
00119     box.setMinimum(minv)
00120     box.setMaximum(maxv)
00121     box.setSingleStep(step)
00122     return box
00123 
00124 def selected_radio_button(buttons_list):
00125     selected = None
00126     for r in buttons_list:
00127         if r.isChecked():
00128             selected = str(r.text())
00129     return selected
00130 
00131 class SliderBox:
00132 
00133     def __init__(self, parent, initial_value, max_value, min_value, ticks, name_preffix, units=''):
00134         self.min_value = min_value
00135         self.max_value = max_value
00136 
00137         container_name = name_preffix + '_box'
00138         disp_name = name_preffix + '_disp'
00139         slider_name = name_preffix + '_slider'
00140 
00141         nticks = float(max_value - min_value) / float(ticks)
00142         tick_size = int(round(100. / nticks))
00143         tick_pos = self._units_to_slider(initial_value)
00144 
00145         container = qtg.QWidget(parent)
00146         container.setObjectName(container_name)
00147         
00148         disp = qtg.QLabel(container)
00149         disp.setObjectName(disp_name)
00150         disp.setText('%.2f %s' % (initial_value, units))
00151 
00152         slider = qtg.QSlider(container)
00153         slider.setSingleStep(1)
00154         slider.setOrientation(qtc.Qt.Horizontal)
00155         slider.setTickPosition(qtg.QSlider.TicksBelow)
00156         slider.setTickInterval(tick_size)
00157         slider.setObjectName(slider_name)
00158         slider.setSliderPosition(tick_size)
00159         slider.setValue(tick_pos)
00160 
00161         hlayout = qtg.QVBoxLayout(container)
00162         hlayout.addWidget(slider)
00163         hlayout.addWidget(disp)
00164 
00165         def slider_moved_cb(disp, value):
00166             cv = self._slider_to_units(value)
00167             disp.setText('%3.2f %s' % (cv, units))
00168         parent.connect(slider, qtc.SIGNAL('sliderMoved(int)'), ft.partial(slider_moved_cb, disp))
00169 
00170         self.container = container
00171         self.slider = slider
00172         self.disp = disp
00173         self.units = units
00174 
00175     def _slider_to_units(self, value):
00176         return ((value / 100.) * (self.max_value - self.min_value)) + self.min_value
00177         
00178     def _units_to_slider(self, value):
00179         return int(round(((value - self.min_value) / (self.max_value - self.min_value)) * 100.0))
00180 
00181     def value(self):
00182         return self._slider_to_units(self.slider.value())
00183 
00184     def set_value(self, value):
00185         self.slider.setValue(self._units_to_slider(value))
00186         self.disp.setText('%3.2f %s' % (value, self.units))
00187 
00188 
00189 class ToolBase:
00190 
00191     def __init__(self, rcommander, name, button_name, smach_class):
00192         self.rcommander = rcommander
00193 
00194         self.name = name
00195         self.name_input = None
00196         self.loaded_node_name = None
00197 
00198         self.button = None
00199         self.button_name = button_name
00200 
00201         self.smach_class = smach_class
00202         self.outcome_inputs = {}
00203         self.counter = 0
00204 
00205         self.node_exists = False
00206         self.saved_state = None
00207 
00208     def create_button(self, container):
00209         self.button = create_tool_button(self.button_name, container)
00210         self.rcommander.connect(self.button, qtc.SIGNAL('clicked()'), self.activate_cb)
00211         return self.button
00212 
00213     def activate_cb(self, loaded_node_name=None):
00214         self.rcommander.notify_activated()
00215 
00216         self.node_exists = False
00217         self.rcommander.enable_buttons()
00218         self.set_loaded_node_name(loaded_node_name)
00219         self.rcommander.add_mode()
00220         self.rcommander.empty_properties_box()
00221 
00222         if self.button.isChecked():
00223             #send fresh boxes out
00224             self.rcommander.set_selected_tool(self.get_smach_class())
00225             #This needs to appear *before* the various fills as they can fail
00226             self.fill_property_box(self.rcommander.ui.properties_tab)
00227             self.fill_connections_box(self.rcommander.ui.connections_tab)
00228             self.rcommander.ui.node_settings_tabs.setCurrentIndex(0)
00229         else:
00230             self.rcommander.set_selected_tool(None)
00231 
00232         if self.saved_state != None and loaded_node_name == None:
00233             self.set_node_properties(self.saved_state)
00234 
00235     def clear_saved_state(self):
00236         self.saved_state = None
00237 
00238     #Called by RCommander when user deselects this tool
00239     def deselect_tool(self):
00240         # Is the node we're displaying saved? has it been added??
00241         if self.node_exists:
00242             return
00243         else:
00244             try:
00245                 self.saved_state = self.new_node()
00246             except Exception, e:
00247                 rospy.loginfo(str(e))
00248 
00249     def _get_outcome_choices(self):
00250         outcome_choices = {}
00251         for outcome_name in self.outcome_inputs.keys():
00252             outcome_choices[outcome_name] = str(self.outcome_inputs[outcome_name].currentText())
00253         return outcome_choices
00254 
00255     def refresh_connections_box(self):
00256         if not self.button.isChecked():
00257             return
00258 
00259         #record what is selected
00260         outcome_choices = self._get_outcome_choices()
00261         node_name = str(self.name_input.text())
00262 
00263         #empty box
00264         self.rcommander.empty_container(self.rcommander.ui.connections_tab)
00265 
00266         #fill it back up
00267         self.fill_connections_box(self.rcommander.ui.connections_tab)
00268 
00269         #replace defaults with saved values
00270         self.name_input.setText(node_name)
00271         for k in outcome_choices:
00272             selected = outcome_choices[k]
00273             widget = self.outcome_inputs[k]
00274             widget.setCurrentIndex(widget.findText(selected))
00275 
00276     def fill_connections_box(self, pbox):
00277         formlayout = pbox.layout()
00278 
00279         if hasattr(self, 'set_child_node'):
00280             if self.rcommander.selected_node == None:
00281                 return
00282             else:
00283                 state = self.rcommander.graph_model.get_state(self.rcommander.selected_node)
00284                 self.set_child_node(state)
00285 
00286         self.name_input = qtg.QLineEdit() #Needs to occur before new_node as it can fail
00287         formlayout.addRow('Name', self.name_input)
00288 
00289         if self.get_current_node_name() == None:
00290             current_node = self.new_node()
00291         else:
00292             current_node = self.rcommander.graph_model.get_state(self.get_current_node_name())
00293 
00294         current_node_smach = current_node.get_smach_state()
00295         self.rcommander.connect_node(current_node_smach)
00296 
00297         current_node_name = current_node.get_name()
00298         self.name_input.setText(current_node_name)
00299 
00300         if issubclass(current_node.__class__, EmptyState):
00301             return 
00302 
00303         registered_outcomes = list(current_node_smach.get_registered_outcomes())
00304         registered_outcomes.sort()
00305         for outcome in registered_outcomes:
00306             #Make a new combobox and add available choices to it
00307             input_box = qtg.QComboBox(pbox)
00308             nodes = self.rcommander.connectable_nodes(self.get_current_node_name(), outcome)
00309 
00310             nodes.sort()
00311             for n in nodes:
00312                 input_box.addItem(n)
00313 
00314             #add to view 
00315             formlayout.addRow(outcome, input_box)
00316             #set outcome as default
00317             #TODO abstract this line out
00318             input_box.setCurrentIndex(input_box.findText(outcome))
00319             #store object
00320             self.outcome_inputs[outcome] = input_box
00321 
00322             #make callback
00323             def cb(outcome, new_index):
00324                 new_outcome = str(self.outcome_inputs[outcome].currentText())
00325                 self.rcommander.connection_changed(self.get_current_node_name(), outcome, new_outcome)
00326 
00327             outcome_cb = ft.partial(cb, outcome)
00328             self.rcommander.connect(input_box, qtc.SIGNAL('currentIndexChanged(int)'), outcome_cb)
00329 
00330 
00331     def set_loaded_node_name(self, name):
00332         self.loaded_node_name = name
00333 
00334     def get_current_node_name(self):
00335         return self.loaded_node_name 
00336 
00337     def create_node(self, unique=True):
00338         if unique:
00339             self.counter = self.counter + 1
00340         n = self.new_node(str(self.name_input.text()))
00341         if unique:
00342             self.node_exists = True
00343             self.saved_state = None
00344         return n
00345 
00346     def node_selected(self, node):
00347         self.node_exists = True
00348         outcome_list = self.rcommander.current_children_of(node.get_name())
00349         for outcome_name, connect_node in outcome_list:
00350             if not self.outcome_inputs.has_key(outcome_name):
00351                 continue
00352             widget = self.outcome_inputs[outcome_name]
00353             widget.setCurrentIndex(widget.findText(connect_node))
00354 
00355         self.name_input.setText(node.get_name())
00356         self.set_loaded_node_name(node.get_name())
00357         self.loaded_node_name = node.get_name()
00358         self.set_node_properties(node)
00359 
00360     def get_smach_class(self):
00361         return self.smach_class
00362 
00363     ##
00364     # Responsible for setting up graphical widgets allowing users to edit the
00365     # parameters of an action. Called when users click on a node of the class
00366     # smach_class (given in constructor) and whenever this tool is selected via
00367     # its menu bar button.
00368     #
00369     # @param pbox a QT widget with a FormLayout that can be filled with
00370     #             appropriate controls for this tool
00371     #
00372     def fill_property_box(self, pbox):
00373         pass
00374 
00375     ##
00376     # When called must return an instance of smach_class (an object which
00377     # inherits from tool_utils.StateBase given in constructor). 
00378     #
00379     # Called with a name parameter when users click the big Add button, called
00380     # with a name=None by the connections tab to find out how many outgoing
00381     # connections this node has.
00382     #
00383     # @return a valid smach state derived from StateBase
00384     def new_node(self, name=None):
00385         pass
00386 
00387     ##
00388     # Called by parent when user selects a node created by this tool (of class
00389     # smach_class).
00390     #
00391     # @param node a State object created by this tool of class smach_class
00392     def set_node_properties(self, node):
00393         pass
00394 
00395     ##
00396     # Resets the state of this GUI tool, sets all options to sensible defaults.
00397     # Called when users click the big Reset button.
00398     #
00399     def reset(self):
00400         pass
00401 
00402 
00403 class StateBase:
00404 
00405     ##
00406     # Base construction for a state.
00407     #
00408     # @param name name of this state
00409     # @param outputs a dictionary mapping names (strings) to Python classes.
00410     def __init__(self, name, outputs={}):
00411         self.name = name
00412         self.remapping = {}
00413         self.runnable = True
00414         self.outputs = outputs
00415 
00416     ##
00417     # Returns a list of names of all declared outputs variables (same as the
00418     # output variables in SMACH).
00419     #
00420     def output_names(self):
00421         return self.outputs.keys()
00422 
00423     ##
00424     # Gives the type (class) of output with the given name.  Used by RCommander
00425     # to match node's input with other node's outputs.
00426     #
00427     # @param name string, name of output whose class we are interested in.
00428     def output_type(self, name):
00429         return self.outputs[name]
00430     
00431     ##
00432     # Sets the name of this action, used when user sets name in the GUI's Connections
00433     # tab.
00434     #
00435     # @param name string
00436     def set_name(self, name):
00437         self.name = name
00438 
00439     ##
00440     # Gets the name of this object.
00441     #
00442     def get_name(self):
00443         return self.name
00444 
00445     ##
00446     # Gets the name of an output variable of another node in the state machine
00447     # that maps to the input var_name in this node.
00448     #
00449     # @param var_name an input variable for this state
00450     def remapping_for(self, var_name):
00451         return self.remapping[var_name]
00452 
00453     ##
00454     # Sets the name of an output variable of another node in the state machine
00455     # that maps to the input var_name in this node.
00456     #
00457     # @param var input variable for this node.
00458     # @param remapped output variable of another node.
00459     def set_remapping_for(self, var, remapped):
00460         self.remapping[var] = remapped
00461 
00462     ##
00463     # Checks to see whether this node is executable.
00464     #
00465     def is_runnable(self):
00466         return self.runnable
00467 
00468     ##
00469     # Identifies whether this node is executable or not (almost always this
00470     # should be True.
00471     #
00472     # @param v boolean value
00473     def set_runnable(self, v):
00474         self.runnable = v
00475 
00476     ##
00477     # Returns an object that implements smach.State, needed by RCommander to
00478     # create the final state machine.
00479     #
00480     def get_smach_state(self):
00481         raise RuntimeError('Unimplemented method: get_smach_state')
00482 
00483 
00484 class EmptyState(StateBase):
00485 
00486     TOOL_NAME = 'outcome'
00487 
00488     def __init__(self, name, temporary, tool_name=TOOL_NAME):
00489         StateBase.__init__(self, name)
00490         self.set_runnable(False)
00491         self.temporary = temporary
00492         self.tool_name = tool_name
00493 
00494     def get_smach_state(self):
00495         return self
00496 
00497 ##
00498 # State that can be embedded as a child in other states.
00499 #
00500 class EmbeddableState(StateBase):
00501 
00502     def __init__(self, name, child_gm):
00503         StateBase.__init__(self, name)
00504         self.child_gm = child_gm
00505         self.child_document = None 
00506 
00507         #Look inside state machine and look for things with remaps
00508         self._init_child(child_gm)
00509 
00510     def get_child_document(self):
00511         return self.child_document
00512 
00513     def abort_child(self):
00514         fetus = self.child_gm 
00515         self.child_gm = None
00516         return fetus
00517 
00518     def get_child(self):
00519         return self.child_gm
00520 
00521     def set_child(self, child):
00522         self.child_gm = child
00523 
00524     ##
00525     # Look for remappings in child state machine.
00526     #
00527     # @param child_gm GraphModel object
00528     def _init_child(self, child_gm):
00529         if child_gm != None:
00530             self.child_document = child_gm.get_document()
00531             #child_gm.set_document(None)
00532             for node_name in child_gm.states_dict.keys():
00533                 mapping = child_gm.get_state(node_name).remapping
00534                 for input_key in mapping.keys():
00535                     source = mapping[input_key]
00536                     self.set_remapping_for(source, source)
00537 
00538     ##
00539     # Saves child state machine to path given by base_path
00540     #
00541     def save_child(self, base_path=""):
00542         child_gm = self.get_child()
00543         if child_gm.document.has_real_filename():
00544             #print 'saving child to', child_gm.get_document().get_filename() 
00545             child_gm.save(child_gm.get_document().get_filename())
00546         else:
00547             fname = pt.join(base_path, child_gm.get_document().get_name())
00548             #If child does not exists
00549             if not pt.exists(fname):
00550                 child_gm.save(fname)
00551                 #print 'saving child to', fname
00552             else:
00553                 raise RuntimeError('FILE EXISTS. CAN\'T OVERWRITE')
00554 
00555         self.child_document = child_gm.document
00556 
00557     ##
00558     # Loads the child state machine and calls recreate on it.
00559     #
00560     def load_and_recreate(self, base_path):
00561         import graph_model as gm
00562 
00563         fname = pt.join(base_path, pt.split(self.child_document.get_filename())[1])
00564         #print 'load_and_recreate:', fname
00565         child_gm = gm.GraphModel.load(fname)
00566         return self.recreate(child_gm)
00567 
00568     ##
00569     # Returns the name of the child state machine.
00570     #
00571     def get_child_name(self):
00572         return self.child_gm.get_start_state()
00573 
00574     ##
00575     # After being loaded from disk, recreates/clones the object
00576     #
00577     def recreate(self, graph_model):
00578         raise RuntimeError('Unimplemented!!')
00579 
00580 
00581 class SimpleStateCB:
00582 
00583     def __init__(self, cb_func, input_keys, output_keys):
00584         self.cb_func = cb_func
00585         self.input_keys = input_keys
00586         self.output_keys = output_keys
00587 
00588     def __call__(self, userdata, default_goal): 
00589         return self.cb_func(userdata, default_goal)
00590 
00591     def get_registered_input_keys(self):
00592         return self.input_keys
00593 
00594     def get_registered_output_keys(self):
00595         return self.output_keys
00596 
00597     def get_registered_outcomes(self):
00598         return []
00599 
00600 ##
00601 # Implements a simple StateBase that calls an actionlib action
00602 #
00603 class SimpleStateBase(StateBase):
00604 
00605     ##
00606     #
00607     # @param name
00608     # @param action_name
00609     # @param action_type
00610     # @param goal_cb_str
00611     # @param input_keys
00612     # @param output_keys
00613     def __init__(self, name, action_name, action_type, goal_cb_str, input_keys=[], output_keys=[]):
00614         StateBase.__init__(self, name)
00615         self.action_name = action_name
00616         self.action_type = action_type
00617         self.goal_cb_str = goal_cb_str
00618         self.input_keys = input_keys
00619         self.output_keys = output_keys
00620 
00621     def get_smach_state(self):
00622         return SimpleStateBaseSmach(self.action_name, self.action_type, self, self.goal_cb_str,
00623                 self.input_keys, self.output_keys)
00624 
00625 
00626 class SimpleStateBaseSmach(smach_ros.SimpleActionState):
00627 
00628     def __init__(self, action_name, action_type, goal_obj, goal_cb_str, input_keys, output_keys):
00629         smach_ros.SimpleActionState.__init__(self, action_name, action_type, 
00630                 goal_cb = SimpleStateCB(eval('goal_obj.%s' % goal_cb_str), input_keys, output_keys))
00631         self.goal_obj = goal_obj
00632 
00633     def __call__(self, userdata, default_goal): 
00634         f = eval('self.goal_obj.%s' % self.goal_cb_str)
00635         return f(userdata, default_goal)
00636 
00637 
00638 class ActionWithTimeoutSmach(smach.State):
00639 
00640     def __init__(self, time_out, action_name, action_class):
00641         smach.State.__init__(self, 
00642                 outcomes = ['succeeded', 'preempted', 'timed_out'], 
00643                 input_keys = [], output_keys = [])
00644         self.action_client = actionlib.SimpleActionClient(action_name, action_class)
00645         self.action_name = action_name
00646         self.time_out = time_out
00647 
00648     def get_goal(self):
00649         pass
00650 
00651     def process_result(self):
00652         pass
00653 
00654     def execute(self, userdata):
00655         self.action_client.send_goal(self.get_goal())
00656         state = self.action_client.get_state()
00657 
00658         succeeded = False
00659         preempted = False
00660         r = rospy.Rate(30)
00661         start_time = rospy.get_time()
00662         while True:
00663             #we have been preempted
00664             if self.preempt_requested():
00665                 rospy.loginfo('%s: preempt requested' % self.action_name)
00666                 self.action_client.cancel_goal()
00667                 self.service_preempt()
00668                 preempted = True
00669                 break
00670 
00671             if (rospy.get_time() - start_time) > self.time_out:
00672                 self.action_client.cancel_goal()
00673                 rospy.loginfo('%s: timed out!' % self.action_name)
00674                 succeeded = False
00675                 break
00676 
00677             #print tu.goal_status_to_string(state)
00678             if (state not in [am.GoalStatus.ACTIVE, am.GoalStatus.PENDING]):
00679                 if state == am.GoalStatus.SUCCEEDED:
00680                     rospy.loginfo('%s: Succeeded!' % self.action_name)
00681                     succeeded = True
00682                 break
00683 
00684             state = self.action_client.get_state()
00685             r.sleep()
00686 
00687         if preempted:
00688             return 'preempted'
00689 
00690         if succeeded:
00691             self.process_result(self.action_client.get_result())
00692             return 'succeeded'
00693 
00694         return 'timed_out'
00695 
00696 
00697 


rcommander
Author(s): Hai Nguyen (haidai@gmail.com)
autogenerated on Thu Nov 28 2013 11:46:34