Skip to content

Inter Module Communication Tutorial

Paul Heinen edited this page Jul 25, 2017 · 5 revisions

Inter-Module communication Tutorial

Often, modules need to communicate between each other. In some cases, this communication is as simple as passing information from one module to the other. In other cases however, we need to request that a module perform an action, or make some information available to us. In order to deal with this issue of inter-module communication, we have created two utilities - the Bazaar and Intents.

Overviews

Here, we quickly overview the classes that we are going to use before getting to the tutorial.

The Bazaar

The Bazaar is a place where modules can make memory available to other modules and processes. The Bazaar wraps the "Memory" class found in "memory.h" and "memory.cpp" and is a namespace for the memory static instance variable. This means that memory vended in any module, is available in any other module, it is, in essence, a safer way to manage global variables. Note - like any global store, there are no rules for automatically cleaning up memory in the Bazaar. Usually, this is left up to the creating class, though it is the programmer's decision in the end to decide what to allow.

The following methods are available in the Bazaar

  • Vend(std::string key, std::shared_ptr<boost_any> data): This method allows the shared pointer to be posted to the bazaar with the given key.
  • Get(std::string key): This method returns the shared pointer at the given key
  • UpdateListing(std::string key, std::shared_ptr<boost_any> data): This method updates the pointer with a given key. When this is done, all listeners are notified.
  • Touch(std::string key): This method notifies all listeners. Note that because the data is shared as a pointer, listeners are not notified when the data changes, only when the pointer to the data has changed and needs to be updated in the Bazaar (which is why we provide the touch method)
  • AskRemove(std::string key): This method asks that a key be removed from the store. This will only do so if nobody is listening to the key.
  • ForceRemove(std::string key): This method forcibly removes a key from the store.
  • Subscribe(std::string key, Module* module, Intent int): This method subscribes to a key in such a way that the Bazaar generates an intent on the given module whenever listeners are notified (when touch is called, or when updatelisting is called). This method returns a subscription ID, which can be used to unsubscribe from a key.
  • Unsubscribe(std::string key, uint32_t id): Using the ID key, a module can unsubscribe from future notifications.

Intents

Intents are glorified strings, which are used for passing requests between modules. Because of the hard scheduling of each of the modules, a module may not want to do processing for a request in its own time slot - for example, the vision module may not want to use its time slice to read images from the camera. In this case, an intent could be sent to the camera module, requesting that it generate a new image for the vision module, and to alert the vision module when it has done so.

An intent usually takes the following form:

"Module-From/Module-To/Intent-Function/Arg1/Arg2/..../ArgN"

We have provided an intent parser which can parse intents of this form, however it is not restricted for modules to ask for and receive intents in this form. In fact, it is entirely up to the module developer to choose which intents are received and which are ignored, as well as define their own structure for intents. Because they are strings, this allows us pass arbitrary amounts of information in a simple, easy to parse, way. Strings are also convenient when sending requests remotely, which we explore farther in the remote intent tutorial.

Adding Module Communication

We will assume that we have two basic modules set up, like those in the module development tutorial. In this tutorial, we will use the Bazaar, as well as intent communication to control what "Hello World" string our module is printing. Let's take a look:

// Module 1 RunFrame
bool RunFrame() {
    std::cout << "Hello World!" << std::endl;
}

In the code above, we have our module's RunFrame method. As we can see, our module prints "Hello World" over and over. We're going to set up this module to use intents to switch the contents of the module. We're going to send the intent:

"Global/Module-1/ChangeString/Goodbye World"

In this situation, we're sending an intent from Global to Module-1, we're calling the function ChangeString with argument "Goodbye World". Thus, we need to adapt our module from the hello world module tutorial a little bit.

//In the Module 1 Header
std::string myString = "Hello World";

// Module 1 RunFrame
bool RunFrame() {
    std::cout << myString << std::endl;
}

We first parameterize the module, adding a string variable.

Next, we have to teach the module to use intents. Let's create a function in Module-1 called change string (All it does is update the string).

// In module 1 (both header and file)
void ChangeString(std::string newString) {
    myString = newString;
}

We then create what is the core of the module's intent processing system - the intent queue. By using our PendingIntents data type, we can figure out what we're dealing with. Thus, first, in the header file, create:

PendingIntents pending_intents;

We now need to edit the process-intent method. Right now, the process-intent method is empty, our module ignores all intents. We don't want our module to ignore intents, so we are going to save them in the queue. Thus, we change the ProcessIntent(Intent i) method to be:

bool ProcessIntent(Intent &intent) {
    pending_intents.push_back(intent);
    return true;
}

Remember, we never do any processing of intents in the ProcessIntent method. Why is this? The process intent method is actually called by the calling function which means that the module creating the intent has to do all of the work on it's time. That's not how we designed them. Thus, all we do in our processing function is read the intents into a queue. Next, we actually add the code to deal with the intent (which will go in the RunFrame method). Our new RunFrame method looks like this:

// Module 1 RunFrame
bool RunFrame() {
    if (!pendingIntents.empty()) {
        //Use try catch blocks so your module doesn't crash if an invalid intent is passed to it.
        try { 
            ParsedIntent PI = pending_intents.pop_front().Parse();
            /* Method calling is done here. In the future this will be handled automatically.
               For now you can use string comparison checks using if statements or try catch blocks.
               You can also use function pointers or a macro. This might look something like:
               std::unordered_map<std::string, std::function<void(std::string)>> function_map = {
                   {"ChangeString", std::bind(&Module-1::ChangeString, this, std::placeholders::_1)},
                   ...
               };
            */
            if(PI[2] == "ChangeString")
            {
                /* You should probably ensure that the given value types match the functions input.
                   By default every argument in the ParsedIntent is a string, so convert these values
                   Appropriately. See http://en.cppreference.com/w/cpp/header/string under 
                   the section titled: Numeric conversions */
                ChangeString(PI[3]);
            }
        } catch(const std::exception &e) {
            LOG_WARNING << "Intent Parsing Exception: " << e.what();
        }
    }
    std::cout << myString << std::endl;
}

We use the Parse() method (which is declared in the Intent struct) to split the string into a vector of strings, and then take the third element of the resulting vector (that is, "Goodbye World" in our example), and set the value of the string to this new value. After that we call the appropriate method in the module (ChangeString), as specified in the second element of the ParsedIntent vector, to update the myString variable.

NOTE: as mentioned above in the code example, the way module methods get called from the given Intent will be changed to occur automatically. For now you have to handle this yourself.

All we've done is check if we can update the value in each frame, and if we can, we do so. Notice that this update happens in the order that the intents were received. A module may choose to implement a more complicated intent system, such as adding priorities to intents, or some other complicated scheduling method. This is one of the reasons that we chose to leave the intent system open ended.

Adding Bazaar Support

Word of Caution: The Bazaar is NOT thread safe by default. Please consider this when using it.

As you may have noticed, the Intent message passing system is simple and works well for allowing modules to communicate with each other for simple messages. The Intent system, however, was not designed for efficiently passing messages which require large amounts of data, or for modules which may be required to receive and process data from multiple modules each time the module is run (note: The default Parse() method for Intent's runs in O(n) time). For more complicated inter-module communication scenarios we need to use the Bazaar.

The Bazaar, as previously stated, is a global memory pool for storing any type of data and allowing any number of modules to access it. It essentially designed around the Publisher-Subscriber model, where notifications of data change are sent as intents defined on Subscription. To allow any type of data to be stored in the Bazaar we take advantage of the type boost::any which gets dynamically interpreted on variable assignment, however all type information about the object originally assigned is lost. Objects of type boost::any must be cast using boost::any_cast<Type To Cast To>(Object). Since everything in the Bazaar is stored as a std::shared_ptr<boost::any> this can lead to some interesting issues. In this tutorial we will show a few variations of using the Bazaar.

// Module 1: Create and Subscribe
...
// First we insert some data into the Bazaar
void initialize_bazaar_data()
{
    //first we create our data
    float c_array_data[] = {1.2f, 2.4f, 4.6f};
    std::vector<float> cpp_dynamic_data = {3.45f, 5.323f, 12.4f};
    std::array<int, 3> cpp_array_data = {1,2,3};
    //Now lets put the data in the Bazaar
    Bazaar::Vend("c_array_data", std::make_shared<boost::any>(c_array_data));
    //obviously the key need not be the same as the variable name.
    Bazaar::Vend("dynamic sized array", std::make_shared<boost::any>(cpp_dynamic_data));
    Bazaar::Vend("cpp array", std::make_shared<boost::any>(cpp_array_data));
}

The Bazaar now contains three Key Value pairs with pointers to our data. We will now show how to efficiently read and manipulate that data in a way where we won't make any copies.

// Module 2: Subscribe and modify the data
//These variables should go in the private section of your header file

//Variables to point to data in the Bazaar
float *array_of_floats;
std::vector<float> *dynamic_data;
std::array<int, 3> *cpp_array;

//Shared pointers to the boost::any data
std::shared_ptr<boost::any> any_0, any_1, any_2;
//Subscription id's
const uint32_t sub_id_0, sub_id_1, sub_id_2;

void access_bazaar_data()
{
    //First we subscribe to the data
    sub_id_0 = Bazaar::Subscribe("c_array_data", this->instance, "Module2/c_array_data/notify modification");
    sub_id_1 = Bazaar::Subscribe("dynamic sized array", this->instance, "Module2/dynamic sized array/notify modification");
    sub_id_2 = Bazaar::Subscribe("cpp array", this->instance, "Module2/cpp array/notify modification");
   
   //Now we can get pointers to the data
   any_0 = Bazaar::Get("c_array_data");
   any_1 = Bazaar::Get("dynamic sized array");
   any_2 = Bazaar::Get("cpp array");
}

Now that we have subscribed to the data any have pointers to the KV pairs, we can transform the data back to it's original state. This part gets a little messy.

//Module 2 Continued

...
bool RunFrame()
{
    ...
    read_bazaar_data();
    insert_pi();
    ...
}
....

void read_bazaar_data()
{
    try
    {
	    //Notice the difference between casting a c-style array and STL objects.
	    array_of_floats = boost::any_cast<float *>(*any_0);
	    dynamic_array = boost::any_cast<std::vector<float>>(&*any_1);
	    cpp_array = boost::any_cast<std::array<int, 3>(&*any_2);
	    
    } catch(const boost::bad_any_cast &e) {
	    LOG_WARNING << "Error with boost anycast: " << e.what();
    }
}

//Ok lets modify the data and notify the subscribers.
void insert_pi()
{
    array_of_float[0] = 3.14f;
    Bazaar::Touch("c_array_data");
    (*dynamic_array)[0] = 3.14f;
    Bazaar::Touch("dynamic sized array");
    (*cpp_array)[0] = 314;
    Bazaar::Touch("cpp array");
}

We have now updated the data in the Bazaar and notified all subscribers of the change. The heavy use of pointers here makes things appear a bit messy, however we have not created any duplicates of the data modified, thus it is more efficient. For more examples see:

  • The Global Clock used by the watchdog in main.cpp
  • The Inter-module communication between Kinematics and NAOInterface.
  • The Tutorial on communicating with NAOInterface (tdb).

Congratulations! You can now parse intents in your module! For more information, check out the other tutorials (in the side bar):