Mutex control in Choregraphe

Choregraphe is a software package provided by Aldebaran Robotics together with the NAO robot. It  allows to program so called behaviors in a graphical way.  Inconspicuous at the first sight, the application is indeed a very advanced programming and animation tool! It allows to focus on the fun part – making NAO behave – instead of handling low-level tasks that would be an issue in C++ or low-level Python programming. Still, going into more complex applications, you need to dive into Python programming.

Choregraphe (1.12) provides already a lot of flow control boxes out of the box, so to speak, but some functionalities required for advanced multitasking behaviors are not there by default. One of them is a high-level locking mechanism, a so called Mutex.

Imagine the following multi-tasking behavior:

  • The face tracker (not part of the default boxes) tries to find human faces, follows them and let NAO present himself when he sees a face. (Obviously, we should limit the presentation to once per minute or so).
  • Touching the back head-sensor lets NAO say “I’ll move a bit”, standing-up followed by saying “I still am quite small”.
  • The front head sensor instructs NAO to start a short lecture waving around with his hands.

All three tasks are composed by multiple boxes, they take some time to be executed and they should not run at the same time. In fact, when one task is running, the others should be disabled entirely. In order to achieve this, we need a multiple box-spanning locking mechanism and we will discuss in this article how to build one.

Note that you can reserve NAOs “resources” for each specific box, but this applies to the robot resources itself (voice, cameras, joints).

In order to have multiple non-linked boxes communicate together keeping a common “state” information, the technique of choice is a “shared memory”.

Let’s start with the simplest box, out of three:

1) SetLock(Name,True/False)

This box will allow to set a given lock to a specified value. It’s useful to initialise a lock at behavior start-up and to release it after finishing a operation arc. It should not be use to reserve a resource since the box ignores the current state. (There is room for improvement here).

We create a new box of type “Script” by right clicking into the Choregraphe window. We keep the default input and outputs, but we need to add two parameters:

  • LockValue: a multiple choice (!) string of ‘True’ and ‘False’ with the default value set to ‘False’.
  • LockName: a free text string with a default value of ‘defaultlock’. This parameters has to be defined for all boxes hereafter.

The python code is straight forward. It constructs the internal lock name, sets the passed string value – which can only be ‘True’ and ‘False’ due to the multiple choice type, and activates the ‘onStopped’ output to relay to the next operation in the event wire.

class MyClass(GeneratedClass):
  def __init__(self):
    GeneratedClass.__init__(self)
  def onLoad(self):
    pass
  def onUnload(self):
    pass

  def onInput_onStart(self):
    self.lockName="lock-"+self.getParameter("LockName")
    self.logger.debug("SetLock "+self.lockName+"="+
                                      self.getParameter("LockValue"))
    ALMemory.insertData(self.lockName,self.getParameter("LockValue"))
    self.onStopped()

  def onInput_onStop(self):
    self.onUnload()
    pass

The next box is in just a helper box allowing for example to activate some small tasks (like eye blinking) while a lock is reserved. It’s also handy for debugging/monitoring when connected to a pulse timer.

2) IsLocked(Name)

This is again a Python script box, having one single parameter, the LockName as above and providing two output parameters: ‘Yes’ and ‘No’ of (default) type Bang/ponctual (single impulse).

The Python script looks like this:

class MyClass(GeneratedClass):
   def __init__(self):
      GeneratedClass.__init__(self)
   def onLoad(self):
      pass
   def onUnload(self):
      pass

   def onInput_onStart(self):
      self.lockName="lock-"+self.getParameter("LockName")
      try:
         self.lock=ALMemory.getData(self.lockName)
         if (self.lock =="True"):
            self.Yes()
            self.logger.debug("IsLocked "+self.lockName+"=True");
         else:
            self.No()
            self.logger.debug("IsLocked "+self.lockName+"=False")
      except Exception as inst:
         ALMemory.insertData(self.lockName,"False")
         self.No()
         self.logger.debug("IsLocked "+self.lockName+"=False (Init)")

   def onInput_onStop(self):
      self.onUnload()
      pass

After building the internal lock name, it sets the corresponding output depending on the current shared memory value.

Additionally, we need to take care of the fact that the memory value might not yet have been created, in which case we instantiate it to ‘False’. This case is handled by catching the NAOqi framework’s exception.

In any case, there is no ‘onStopped’ output in this box. The subsequent event flow is decided by the Yes or No outcome.

The last box will allow is to reserve the lock in case it is available:

3) GetLock(Name)

Again we need a fresh Python script box, keeping the default inputs, defining the LockName input parameter as about and having three output values of (default) type Bang/ponctual:

  • OK – Lock acquired.
  • NOK – Lock not acquired i.e. currently reserved by another task.
  • onStopped – Always activated, independently of OK or NOK (might be handy).

The Python script is self-documenting. As before, we consider the non-existence of the shared memory value as not reserved lock and proceed in this sense. In case the lock is reserved, we simply pulse the NOK output without touching the lock value. If the lock is free, we reserve it and pulse the OK output. The box is non-blocking, which means we need a manual (touch the head sensor again) or automatic trigger to repeat the attempt in case the lock cannot be obtained.

class MyClass(GeneratedClass):
   def __init__(self):
      GeneratedClass.__init__(self)
   def onLoad(self):
      pass
   def onUnload(self):
      pass

   def onInput_onStart(self):
      self.lockName="lock-"+self.getParameter("LockName")
      try:
         self.lock=ALMemory.getData(self.lockName)
         if (self.lock =="True"):
            self.NOK()
            self.onStopped()
            self.logger.debug("GetLock "+self.lockName+"=NOK");
         else:
            ALMemory.insertData(self.lockName,"True")
            self.OK()
            self.onStopped()
            self.logger.debug("GetLock "+self.lockName+"=OK")

      except Exception as inst:
         ALMemory.insertData(self.lockName,"True")
         self.OK()
         self.onStopped()
         self.logger.debug("GetLock "+self.lockName+"=OK (Init)")

   def onInput_onStop(self):
      self.onUnload()
      pass

Well, that’s it.Of course there is room for improvement. We could for example introduce the notion of ‘lock owner’. Cross-checking the owner against the lock requester would improve the “bug resistance” of the mutex.

If you like to see a more complex behavior that uses a mutex to synchonise multiple tasks, have a look at the “NAO speaks about Mars” project (Video, French). The source code of these boxes is available on GitHub (or here).

Et zou!

Leave a Reply

Your email address will not be published. Required fields are marked *