[Documentation] [TitleIndex] [WordIndex

(!) Please ask about problems and questions regarding this tutorial on answers.ros.org. Don't forget to include in your question the link to this page, the versions of your OS & ROS, and also add appropriate tags.

Creating New States Editor in RCommander

Description: Guides you through creating new editors for your SMACH states in RCommander.

Tutorial Level: INTERMEDIATE

Next Tutorial: Sharing Resources Between Different State Editors

Classes Involved in a State Editor

my_sleep.png

A states editor is responsible for creating the GUI panel for editing the properties of a SMACH state. In the above screenshot, the panel on the right side of the screen is the states editor for the state my_sleep0.

Each states editor requires the creation of three different classes: a classic SMACH state class, an RCommander state class, and the GUI panel class. If you have a system that already runs SMACH, you will likely have a couple of SMACH classes already (class that inherits smach.State). The RCommander state class is new and will require you to inherit from rcommander.tool_utils.StateBase. This class is allows your SMACH action to be saved to and loaded from disk. Finally, the GUI panel class (which inherits from rcommander.tool_utils.ToolBase) creates the GUI panel itself so that you can edit the parameters of your SMACH state.

Overview of Steps to Create a State Editor

To create a states editor you'll need to do the following steps:

Let's get started in creating our states editor!

Creating the Necessary Classes

In this example we will create a simple tool that allows us to create actions that instructs the Smach state machine to sleep for a small amount of time. Assuming you were following along in the last tutorial create a new file called my_rcommander/src/my_rcommander/my_sleep_tool.py.

Creating a class that inherits from smach.State

First, we will need a basic smach.State:

   1 import smach
   2 import rospy
   3 
   4 class MySleepSmachState(smach.State):
   5 
   6     def __init__(self, sleep_time):
   7         smach.State.__init__(self, outcomes=['done'], input_keys=[], output_keys=[])
   8         self.sleep_time = sleep_time
   9 
  10     def execute(self, userdata):
  11         rospy.sleep(self.sleep_time)
  12         return 'done'

This Smach state has one parameter sleep_time and sleeps for that amount of time when executed. However, let's improve this implementation by allowing the state to be preempted or interrupted when it executes:

   1 import smach
   2 import rospy
   3 
   4 class MySleepSmachState(smach.State):
   5 
   6     def __init__(self, sleep_time):
   7         smach.State.__init__(self, outcomes=['preempted', 'done'], input_keys=[], output_keys=[])
   8         self.sleep_time = sleep_time
   9 
  10     def execute(self, userdata):
  11         r = rospy.Rate(30)
  12         start_time = rospy.get_time()
  13         while (rospy.get_time() - start_time) < self.sleep_time:
  14             r.sleep()
  15             if self.preempt_requested():
  16                 self.services_preempt()
  17                 return 'preempted'
  18         return 'done'

The Smach now sleeps in small increments until it has slept for sleep_time seconds. After each time step it checks to see whether it has been interrupted or not. Notice that we've also expanded the outcomes in our call to smach.State.__init__ to preempted and done. In general, this is a good pattern for Smach states to follow.

Creating a class that inherits from rcommander.tool_utils.StateBase

In this next step, we make a new class that will enable RCommander to load and save our SMACH state to disk. As is, the state we've created above is not compatible with the Python pickling functions, making it difficult to save, so we'll need to separate its parameters out from the the execution logic:

   1 import rcommander.tool_utils as tu
   2 
   3 class MySleepState(tu.StateBase):
   4 
   5     def __init__(self, name, sleep_time):
   6         tu.StateBase.__init__(self, name)
   7         self.sleep_time = sleep_time
   8 
   9     def get_smach_state(self):
  10         return MySleepSmachState(self.sleep_time)

When inheriting from StateBase we're responsible for implementing the function get_smach_state which returns an instance of the object we constructed earlier. It is this class that will be pickled and saved to disk and not the Smach state so it is important that this class has no references to anything that cannot be pickled.

Creating a class that inherits from rcommander.tool_utils.ToolBase

In this next step we create another class that will actually implement the interface that we want to use to interact with our SMACH objects. This class will need to inherit from ToolBase and implement four methods: __init__, fill_property_box, new_node, set_node_properties, and reset. This is illustrated in the code below:

   1 from PyQt4.QtGui import *
   2 from PyQt4.QtCore import *
   3 
   4 class MySleepTool(tu.ToolBase):
   5 
   6     def __init__(self, rcommander):
   7         tu.ToolBase.__init__(self, rcommander, 'my_sleep', 'My Sleep', MySleepState)
   8 
   9     def fill_property_box(self, pbox):
  10         formlayout = pbox.layout()
  11         self.time_box = QDoubleSpinBox(pbox)
  12         self.time_box.setMinimum(0)
  13         self.time_box.setMaximum(1000.)
  14         self.time_box.setSingleStep(.2)
  15         self.time_box.setValue(3.)
  16         formlayout.addRow("&Seconds", self.time_box)
  17 
  18     def new_node(self, name=None):
  19         if name == None:
  20             nname = self.name + str(self.counter)
  21         else:
  22             nname = name
  23         return MySleepState(nname, self.time_box.value())
  24 
  25     def set_node_properties(self, my_node):
  26         self.time_box.setValue(my_node.sleep_time)
  27 
  28     def reset(self):
  29         self.time_box.setValue(3.)

Let's go through this function by function.

   1     def __init__(self, rcommander):
   2         tu.ToolBase.__init__(self, rcommander, 'my_sleep', 'My Sleep', MySleepState)

First we have the __init__ method which simply calls the base construction with several arguments. The first argument here is a reference to the QT widget that contains the RCommander main window. Next, my_sleep is a prefix for the default names of nodes created. Then My Sleep gives a name for the menu bar button. Lastly, MySleepState is the class of the states that our tool will create (our class from the last section). This information is crucial so that RCommander can later match the objects created by our tool back to the tool itself.

   1     def fill_property_box(self, pbox):
   2         formlayout = pbox.layout()
   3         self.time_box = QDoubleSpinBox(pbox)
   4         self.time_box.setMinimum(0)
   5         self.time_box.setMaximum(1000.)
   6         self.time_box.setSingleStep(.2)
   7         self.time_box.setValue(3.)
   8         formlayout.addRow("&Seconds", self.time_box)

In the method fill_property_box, we will need a little of bit of knowledge about QT widgets. Here, we populate the property widget with a QDoubleSpinBox, along with reasonable settings for the sleep_time property.

   1     def new_node(self, name=None):
   2         if name == None:
   3             nname = self.name + str(self.counter)
   4         else:
   5             nname = name
   6         return MySleepState(nname, self.time_box.value())

The new_node method give this tool the ability to create new nodes (i.e. objects that inherits !StateBase). This method gets called whenever someone clicks the Add button and when RCommander needs to find out what our node's outcomes are. Because of this the parameter name can either be empty (when RCommander calls) or a string (called from the Add button with a real name). RCommander also provides a counter that acts as a guide for creating default state names. Most nodes will not need anything more sophisticated than the above however. Finally, we return an instance of our MySleepState based on the interface's current state.

   1     def set_node_properties(self, my_node):
   2         self.time_box.setValue(my_node.sleep_time)

The set_node_properties is called whenever the user selects an instance of our node in the State Machine. When it's called, we get back the selected node and will be responsible for setting the state of our QT widgets. Here, we are setting the value of the time_box (QDoubleSpinBox) widget.

   1     def reset(self):
   2         self.time_box.setValue(3.)

Finally, reset is called whenever someone clicks the big Reset button. Let's set the default setting to a reasonable 3 seconds. That's it! We're now done with the Python section of our plugin. Let's declare this in the manifest.xml file and relaunch RCommander!

Plug-in Declaration in manifest.xml

As the title says we will declare this plugin in our manifest.xml. Open the manifest file at my_rcommander/manifest.xml and insert the following lines:

  <export>
      <rcommander plugin="my_rcommander.my_sleep_tool" robot="myrobot" tab="My Robot Actions"/>
  </export>

In this export, my_rcommander.my_sleep_tool indicates the Python file import where RCommander will be able to find our to tool. Then, myrobot indicates which robot this tool applies to (we'll need to add this to the parameter in run_rcommander). It doesn't matter what this says as much, as long as you're consistent. Finally, the last argument My Robot Actions tells RCommander which tab our tool should be placed into. Finally, we need to update the call to run_rcommander. Open my_rcommander/src/my_rcommander/my_rcommander.py and change the call to:

   1 rc.run_rcommander(['default', 'myrobot'])

Finally, we're ready to relaunch! Run:

./my_rcommander/src/my_rcommander/my_rcommander.py

You should see this new RCommander window:

my_sleep.png

Try adding and playing with instances of your newly added state. Next we'll learn to Share Resources Between Different State Editors.


2019-12-14 13:00