Extending NAO API - Creating a new module

This is a step by step tutorial.

You should have follow all the steps of the C++ SDK Installation fist.

Create the skeleton using qibuild create

qisrc create will simply create a new project.

Navigate to the work tree where you want to create your project then enter:

$ qisrc create mymodule

This will create a new project in work_tree/mymodule. We now take a look at what has been generated:

myproject
|__ CMakeLists.txt
|__ main.cpp
|__ qibuild.cmake
|__ qibuild.manifest
  • CMakeLists.txt: this is a script file that will be read by CMake to generate makefiles, or Visual Studio solutions.
  • main.cpp: this is just a standard “Hello World”.
  • qibuild.cmake: this file MUST be included by the CMakeLists.txt to find the qiBuild CMake framework.
  • qibuild.manifest: this file MUST be present for qiBuild to know how to build the foo project.

Declare dependency in CMakeLists.txt

This file is a standard CMakelists.

cmake_minimum_required(VERSION 2.8)

# Give a name to the project.
project(mymodule)

# You need this to find the qiBuild CMake framework
include("qibuild.cmake")

# Create a executable named mymodule
# with the source file: main.cpp
qi_create_bin(mymodule "main.cpp")

To communicate with naoqi you need to use a proxy inside your code. Proxies are inside ALCommon library. You need to import ALCommon inside your CMakeLists.

Add inside the CMakeLists.txt:

qi_use_lib(<your_project_name> <library_you_want_use_1> <library_you_want_use_2>)

Your CMakeLists will look like:

cmake_minimum_required(VERSION 2.8)

# Give a name to the project.
project(mymodule)

# You need this to find the qiBuild CMake framework
find_package(qibuild)

# Create a executable named myproject
# with the source file: main.cpp
qi_create_bin(myproject "main.cpp")

# Tell CMake that myproject depends on ALCOMMON and ALPROXIES
# This will set the libraries to link myproject with,
# the include paths, and so on
qi_use_lib(myproject ALCOMMON ALPROXIES)

main.cpp

This generated file is a standard main.cpp, this one only print an “Hello, world” on standard output.

We now extend a little this file to communicate with NAOqi and call bind function from NAOqi’s module.

First of all you need to do a small command line option parser with at least --pip and --pport option.

You need create a proxy to the module you want use. To do that you must include <alcommon/alproxy.h>, then create a proxy to the module and finally you can call a method from this module.

#include <iostream>
#include <stdlib.h>

#include <alcommon/alproxy.h>
#include <alcommon/albroker.h>

int main(int argc, char* argv[])
{
  // We will try to connect our broker to a running NAOqi
  int pport = 9559;
  std::string pip = "127.0.0.1";

  // command line parse option
  // check the number of arguments
  if (argc != 1 && argc != 3 && argc != 5)
  {
    std::cerr << "Wrong number of arguments!" << std::endl;
    std::cerr << "Usage: mymodule [--pip robot_ip] [--pport port]" << std::endl;
    exit(2);
  }

  // if there is only one argument it should be IP or PORT
  if (argc == 3)
  {
    if (std::string(argv[1]) == "--pip")
      pip = argv[2];
    else if (std::string(argv[1]) == "--pport")
      pport = atoi(argv[2]);
    else
    {
      std::cerr << "Wrong number of arguments!" << std::endl;
      std::cerr << "Usage: mymodule [--pip robot_ip] [--pport port]" << std::endl;
      exit(2);
    }
  }

  // Sepcified IP or PORT for the connection
  if (argc == 5)
  {
    if (std::string(argv[1]) == "--pport"
        && std::string(argv[3]) == "--pip")
    {
      pport = atoi(argv[2]);
      pip = argv[4];
    }
    else if (std::string(argv[3]) == "--pport"
             && std::string(argv[1]) == "--pip")
    {
      pport = atoi(argv[4]);
      pip = argv[2];
    }
    else
    {
      std::cerr << "Wrong number of arguments!" << std::endl;
      std::cerr << "Usage: mymodule [--pip robot_ip] [--pport port]" << std::endl;
      exit(2);
    }
  }

  // A broker needs a name, an IP and a port to listen:
  const std::string brokerName = "mybroker";

  // Create your own broker
  boost::shared_ptr<AL::ALBroker> broker =
    AL::ALBroker::createBroker(brokerName, "0.0.0.0", 54000, pip, pport);

  /**
   * Create a proxy to a module in NAOqi process so that we can call
   * the bind method of this module
   * AL::ALProxy proxy(<broker>, <module_name>);
   */
  // Create a proxy to ALTextToSpeechProxy
  AL::ALProxy proxy(broker, "ALTextToSpeech");

  /**
   * If the bind methode is a void return
   * you can call bind methode using callVoid
   * proxy.callVoid(<bind_method>, <parameter>, ...)
   */
  // Call say methode
  proxy.callVoid("say", std::string("Sentence to say!"));

  /**
   * Otherwise you can use template call methode
   * type res = proxy.call<type>(<bind_methode>, <parameter>, ...);
   */
  // Call ping function that return a boolean
  bool res = proxy.call<bool>("ping");

  return 0;
}

Now you know how to communicate with NAOqi and a method from a module, we want to add some functionality to NAOqi. We will now create a module which it extends, adds some new features.

How to create a remote module

A remote module is a program which will connect to NAOqi over the network. It will allow you to extend/add basic NAOqi bind functions.

Right now you have few stuff to do before running a good module.

Create a new class derived from ALModule

You need to create your own inherited class form ALModule. You’ll need ALModule and ALBroker.

Here is an example of a basic mymodule.h

#ifndef MY_MODULE_H
# define MY_MODULE_H

# include <iostream>
# include <alcommon/almodule.h>

namespace AL
{
  // This is a forward declaration of AL:ALBroker which
  // avoids including <alcommon/albroker.h> in this header
  class ALBroker;
}

/**
 * This class inherits AL::ALModule. This allows it to bind methods
 * and be run as a remote executable within NAOqi
 */
class MyModule : public AL::ALModule
{
public:
  MyModule(boost::shared_ptr<AL::ALBroker> broker,
           const std::string &name);

  virtual ~MyModule();

  /**
   * Overloading ALModule::init().
   * This is called right after the module has been loaded
   */
  virtual void init();

  // After that you may add all your bind method.

  // Function which prints "Hello!" on standard output
  void printHello();
  // Function which prints the word given on parameters
  void printWord(const std::string &word);
  // Function which returns true
  bool returnTrue();
};
#endif // MY_MODULE_H

Now you want to implement all your methods. Here an example of a basic mymodule.cpp

#include "mymodule.h"

#include <iostream>
#include <alcommon/albroker.h>

MyModule::MyModule(boost::shared_ptr<AL::ALBroker> broker,
                   const std::string& name)
  : AL::ALModule(broker, name)
{
  // Describe the module here. This will appear on the webpage
  setModuleDescription("My own custom module.");

  /**
   * Define callable methods with their descriptions:
   * This makes the method available to other cpp modules
   * and to python.
   * The name given will be the one visible from outside the module.
   * This method has no parameters or return value to describe
   * functionName(<method_name>, <class_name>, <method_description>);
   * BIND_METHOD(<method_reference>);
   */
  functionName("printHello", getName(), "Print hello to the world");
  BIND_METHOD(MyModule::printHello);

  /**
   * addParam(<attribut_name>, <attribut_descrption>);
   * This enables to document the parameters of the method.
   * It is not compulsory to write this line.
   */
  functionName("printWord", getName(), "Print a given word.");
  addParam("word", "The word to be print.");
  BIND_METHOD(MyModule::printWord);

  /**
   * setReturn(<return_name>, <return_description>);
   * This enables to document the return of the method.
   * It is not compulsory to write this line.
   */
  functionName("returnTrue", getName(), "Just return true");
  setReturn("boolean", "return true");
  BIND_METHOD(MyModule::returnTrue);

  // If you had other methods, you could bind them here...
  /**
   * Bound methods can only take const ref arguments of basic types,
   * or AL::ALValue or return basic types or an AL::ALValue.
   */
}

MyModule::~MyModule()
{
}

void MyModule::init()
{
  /**
   * Init is called just after construction.
   * Do something or not
   */
  std::cout << returnTrue() << std::endl;
}


void MyModule::printHello()
{
  std::cout << "Hello!" << std::endl;
}

void MyModule::printWord(const std::string &word)
{
  std::cout << word << std::endl;
}

bool MyModule::returnTrue()
{
  return true;
}

Now you already create your inherited class, you can update your main.cpp to create broker and to allow every module to communicate with your bind methods.

Update CMakelists.txt

Here you just need to add the new source file you have created (mymodule.cpp, mymodule.h) using set CMake function:

set(<variable_name> <source_file>)

Your CMakeLists.txt should look like:

cmake_minimum_required(VERSION 2.8)

# Give a name to the project.
project(mymodule)

# You need this to find the qiBuild CMake framework
find_package(qibuild)

# Create a list of source files
set(_srcs
    mymodule.cpp
    mymodule.h
    main.cpp)

# Create a executable named mybroker
# with the source file: main.cpp
qi_create_bin(mybroker ${_srcs})

# Tell CMake that mybroker depends on ALCOMMON and ALPROXIES
# This will set the libraries to link mybroker with,
# the include paths, and so on
qi_use_lib(mybroker ALCOMMON ALPROXIES)

Update main.cpp

First of all you need to create a broker, then you must add your new broker into the NAOqi’s broker manager. Then you can create your custom module and link it with the new broker you have just created.

Example of main.cpp

#include <iostream>
#include <stdlib.h>
#include <qi/os.hpp>

#include "mymodule.h"

#include <alcommon/almodule.h>
#include <alcommon/albroker.h>
#include <alcommon/albrokermanager.h>

int main(int argc, char* argv[])
{
  // We will try to connect our broker to a running NAOqi
  int pport = 9559;
  std::string pip = "127.0.0.1";

  // command line parse option
  // check the number of arguments
  if (argc != 1 && argc != 3 && argc != 5)
  {
    std::cerr << "Wrong number of arguments!" << std::endl;
    std::cerr << "Usage: mymodule [--pip robot_ip] [--pport port]" << std::endl;
    exit(2);
  }

  // if there is only one argument it should be IP or PORT
  if (argc == 3)
  {
    if (std::string(argv[1]) == "--pip")
      pip = argv[2];
    else if (std::string(argv[1]) == "--pport")
      pport = atoi(argv[2]);
    else
    {
      std::cerr << "Wrong number of arguments!" << std::endl;
      std::cerr << "Usage: mymodule [--pip robot_ip] [--pport port]" << std::endl;
      exit(2);
    }
  }

  // Sepcified IP or PORT for the connection
  if (argc == 5)
  {
    if (std::string(argv[1]) == "--pport"
        && std::string(argv[3]) == "--pip")
    {
      pport = atoi(argv[2]);
      pip = argv[4];
    }
    else if (std::string(argv[3]) == "--pport"
             && std::string(argv[1]) == "--pip")
    {
      pport = atoi(argv[4]);
      pip = argv[2];
    }
    else
    {
      std::cerr << "Wrong number of arguments!" << std::endl;
      std::cerr << "Usage: mymodule [--pip robot_ip] [--pport port]" << std::endl;
      exit(2);
    }
  }

  // Need this to for SOAP serialization of floats to work
  setlocale(LC_NUMERIC, "C");

  // A broker needs a name, an IP and a port:
  const std::string brokerName = "mybroker";
  // FIXME: would be a good idea to look for a free port first
  int brokerPort = 54000;
  // listen port of the broker (here an anything)
  const std::string brokerIp = "0.0.0.0";


  // Create your own broker
  boost::shared_ptr<AL::ALBroker> broker;
  try
  {
    broker = AL::ALBroker::createBroker(
        brokerName,
        brokerIp,
        brokerPort,
        pip,
        pport,
        0    // you can pass various options for the broker creation,
             // but default is fine
      );
  }
  catch(...)
  {
    std::cerr << "Fail to connect broker to: "
              << pip
              << ":"
              << pport
              << std::endl;

    AL::ALBrokerManager::getInstance()->killAllBroker();
    AL::ALBrokerManager::kill();

    return 1;
  }

  // Deal with ALBrokerManager singleton (add your borker into NAOqi)
  AL::ALBrokerManager::setInstance(broker->fBrokerManager.lock());
  AL::ALBrokerManager::getInstance()->addBroker(broker);

  // Now it's time to load your module with
  // AL::ALModule::createModule<your_module>(<broker_create>, <your_module>);
  AL::ALModule::createModule<MyModule>(broker, "MyModule");

  while (true)
    qi::os::sleep(1);

  return 0;
}

Congratulations, you now have created your first remote module.

You can find it in a folder named build-<name>/sdk/bin/ where name if the name of the toolchain you have created containing the NAOqi C++ SDK.

To use it you just need to launch the program created with your robot IP address on parameters.

How to start remote module

Using binary program

If you want to start your module on remote, you need to launch the program you just compiled:

./mymodule --pip <robot_ip> --pport <robot_port>

Example:

./mymodule --pip 192.168.0.12 --pport 9559

Using autoload.ini

This way will launch your module at NAOqi startup. Be careful you MUST implement an option command line parser for --pip and --pport.

First of all you need to send your program on your robot (using scp or rsync), and then add the path to your program into /home/nao/naoqi/preferences/autoload.ini under [program] tag.

Example of autoload.ini:

[program]
/home/nao/mymodule
# or
/home/nao/myfolder/whatever/mymodule

How to create a local module

There is a second way to create a module. This is a local module. This one is launched in NAOqi’s process. Since this is in NAOqi process, this type of module by far the fastest one.

This type of module is not a program (binary) anymore. You will create a library. You need to modified your Cmakelists.txt to create a library and your main.cpp to specified the library entry points.

Modified Cmakelists.txt

Here you need to replace your qi_create_bin by a qi_create_lib AND add a dependency to BOOST into your qi_use_lib (for boost::shared_ptr).

cmake_minimum_required(VERSION 2.8)

# Give a name to the project.
project(mymodule)

# You need this to find the qiBuild CMake framework
find_package(qibuild)

# Create a list of source files
set(_srcs
    mymodule.cpp
    mymodule.h
    main.cpp)

# Create a plugin, that is a shared library, and make
# sure it is built in lib/naoqi
qi_create_lib(mymodule SHARED ${_srcs} SUBFOLDER naoqi)

# Tell CMake that mymodule depends on ALCOMMON and ALPROXIES.
# This will set the libraries to link mymodule with,
# the include paths, and so on
qi_use_lib(mymodule ALCOMMON ALPROXIES BOOST)

Once you have update the CMakeLists the only thing you need to do is change the main.cpp

Change main.cpp

When you create a NAOqi plugin (local module) you do not need main function, you need the entry and exit points.

They are defined by two functions:

  • int _createModule(boost::shared_ptr<AL::ALBroker> broker)
  • int _closeModule()

If you want your library working one Windows you must export those entry point using __declspec(dllexport).

Now your main.cpp should looks like:

#include "mymodule.h"

#include <boost/shared_ptr.hpp>

#include <alcommon/albroker.h>
#include <alcommon/albrokermanager.h>

// we're in a dll, so export the entry point
#ifdef _WIN32
# define ALCALL __declspec(dllexport)
#else
# define ALCALL
#endif

extern "C"
{
  ALCALL int _createModule(boost::shared_ptr<AL::ALBroker> broker)
  {
    // init broker with the main broker instance
    // from the parent executable
    AL::ALBrokerManager::setInstance(broker->fBrokerManager.lock());
    AL::ALBrokerManager::getInstance()->addBroker(broker);
    // create module instances
    AL::ALModule::createModule<MyModule>(broker, "MyModule");
    return 0;
  }

  ALCALL int _closeModule(  )
  {
    return 0;
  }
} // extern "C"

You’ve just created your first local module for NAOqi.

You can find it in a folder named build-<name>/sdk/lib/naoqi where name if the name of the toolchain you have created containing the NAOqi C++ SDK.

How to start a local module

Using autoload.ini

Add the path to your library into the [user] tag in autoload.ini file is the first way to start your module. This file is located into /home/nao/naoqi/preferences/autoload.ini on your robot.

Example:

[user]
 /path/to/libmymodule.so

When you add the path to you library, at NAOqi startup your module will be automatically loaded in NAOqi process. After that you can use your module as classic ones.

Note: for this to work on your robot, you must cross-compile your module.

Note: If you are using Visual Studio, make sure to use naoqi_d.bat if you have compiled your module in debug, and naoqi.bat if you have compiled it on release.

Using dynamic linking loader

If you want to use your module in another one, the good way to do that is to use programming interface to dynamic linking loader (dlclose, dlerror, dlopen, dlsym).

The first thing is to find the library on your robot. If you use the standard SDK layout described here (FIXME), you can use qi::path::findLib to obtain the path to your library. Otherwise, you need to hard code the path to it.

Example:

// Find your library
std::string filename = qi::path::findLib("mymodule.so");
// Open your library
void* handle = qi::os::dlopen(filename.c_str());
if (!handle)
{
  // Log the last message coming form dynamic linking function
  qiLogWarning("mymodule") << "Could not load library:"
                           << qi::os::dlerror() << std::endl;
  return -1;
}

myFunc fun;
// Find the symbol you want to use
fun = (myFunc)qi::os::dlsym(handle, "my_function");
if (!fun)
{
  qiLogWarning("mymodule") << "Could not find my_function method in plugin"
                           << std::endl;
  return -1;
}

// Use your function
bool res = fun(args);
if (!res)
{
  qi::os::dlclose(handle);
  return -1;
}

Switching from local to remote using a CMake option

This is totally optional of course, but that’s how all our examples are written.

For every example, you can choose whether you want a local or a remote module by setting a CMake option looking like MYMODULE_IS_REMOTE.

Adding a CMake option

You should patch your CMake code to look like:

option(MYMODULE_IS_REMOTE
  "module is compiled as a remote module (ON or OFF)"
  ON)


if(MYMODULE_IS_REMOTE)
  add_definitions( " -DMYMODULE_IS_REMOTE ")
  qi_create_bin(....)
else()
  qi_create_lib(...)
endif()

Note the call to add_definitions. This will make sure you can use the same main.cpp, but in one case, create a lib an export the _createModule symbol, and in an other case, create a binary with a main symbol, by adding a definition to the compiler.

Patching main.cpp

Finally, you should patch the main.cpp file to have something like:

#ifdef MYMODULE_IS_REMOTE
# define ALCALL
#else
// when not remote, we're in a dll, so export the entry point
# ifdef _WIN32
#  define ALCALL __declspec(dllexport)
# else
#  define ALCALL
# endif
#endif


extern "C"
{
  ALCALL int _createModule(...)
  // ...

};

#ifdef HELLOWORLD_IS_REMOTE
int main(int argc, char* argv[])
{
  // pointer to createModule
  TMainType sig;
  sig = &_createModule;
  // call main
  return ALTools::mainFunction("MyModule", argc, argv, sig);
}
#endif

The full resulting example can be found in here: helloworld.zip