Implementing a Custom Module

This tutorial should guide anyone through the process of creating a new FruityMesh module. We’ll implement a PingModule that allows us to ping a node and get its response. You’d normally implement ping functionality into the StatusReporter oder DebugModule, which is where it belongs, but the use-case acts as a good and easy example for building your own module. You can find the finished files in the folder src/examples or you can just follow the tutorial.

What is a Module

Modules are used to structure functionality that does not directly interfer with the mesh-logic. Modules extend the Module class. Each module has the possibility to save a persistent configuration. It can be loaded or unloaded and it is possible to decide which modules are part of the firmware during compile time. A module can choose to register an UART listener and react on commands, it can also output to UART for logging or communication purposes. It can also send data packets through the mesh and receive data.

Here is an overview over some of the handlers that it can use:

  • ConfigurationLoadedHandler: Is called when a new Module configuration is loaded

  • TimerEventHandler: Is called at a fixed interval to do periodic tasks

  • BleEventHandler: Routes all Bluetooth Low Energy events to the module

  • MeshMessageReceivedEventHandler: Delivers data that has been sent over the mesh

  • TerminalCommandHandler: Gets called when data is received over UART

Creating a Ping module

I will now guide you through the steps to implement a simple module that can ping another node over the network and parse the response.

Step 1

First, clone the TemplateModule.cpp file from the src/examples folder into src/modules/PingModule.cpp, afterwards, do the same with TemplateModule.h but put it in the inc/ folder. You must of course refactor the Method names and some more variales, just use the search function and look out for the string "template" anywhere in those two files and rename them accordingly.

Step 2

Activate your module by instantiating it in the constructor of src/FruityMesh.cpp right where all the other modules are instantiated. You might have to increase the MAX_MODULE_COUNT in config/Config.h. Your PingModule Constructor needs a moduleID for instantiation. All module Ids are specified in the Config.h (enum moduleID) as well. You should add a new enum entry under the section "Custom modules", e.g. PING_MODULE_ID=200.

For example initialization can look like this:

activeModules[15] = new PingModule();

You should also include the new <PingModule.h>.

Once the modules .cpp file is in the modules folder, it will automatically be included in the compilation.

Step 3

Now go ahead and try to compile the binary and flash it to a device. Connect via terminal and input get_modules this. You should now see a list of modules and your new Ping modules id should be part of it.

It does not do much yet, but we’ll now start to add more functionality.

Step 4

Your PingModule does already overwrite a few of the methods in its base-class. One of these is the TerminalCommandHandler. We’ll keep it simple for this tutorial and we’ll only implement the ping command so that it can be triggered via a locally connected terminal. (It would also be possible to trigger a remote node to send a ping and communicate the result back to another node).

In the TerminalCommandHandler, add the following lines to the beginning of the function:

if(TERMARGS(0, "pingmod")){
    //Get the id of the target node
    nodeID targetNodeId = atoi(commandArgs[0]);
    logt("PINGMOD", "Trying to ping node %u", targetNodeId);

    //TODO: Send ping packet to that node

    return true;
}

The command name and command arguments should be in lowercase letters to be consistent with other commands. You must include <stdlib.h> so that the atoi method works.

Next, flash it to your device again and watch if it reacts on your pingmod command. If it does not, make sure you are using the logtag "PINGMOD" and you either enable it by writing debug pingmod in the terminal first, or you enable the logtag by default in FruityMesh.cpp.

pingmod_1

Step 5

Now that the module is reacting to our command, we want to send our ping packet. Because this is going to be a very simple message, we will use a predefined message format called connPacketModule. This packet is intended to be used for triggering actions and for responding to these triggers. It has a special message header that contains the moduleId and an actionType. This will ensure that they do not interfere with mesh messages or messages from other modules.

To keep our module messages organized, we’ll add an enum that contains all of our messages in the private: section of our PingModule.h file:

enum PingModuleTriggerActionMessages{
    TRIGGER_PING=0
};

Next, we add the code that is responsible for sending this packet to the other node. The previously written code now looks like this:

if(TERMARGS(0, "pingmod")){
    //Get the id of the target node
    nodeID targetNodeId = atoi(commandArgs[0]);
    logt("PINGMOD", "Trying to ping node %u", targetNodeId);

    //Some data
    u8 data[1];
    data[0] = 123;

    //Send ping packet to that node
    SendModuleActionMessage(
            MESSAGE_TYPE_MODULE_TRIGGER_ACTION,
            targetNodeId,
            PingModuleTriggerActionMessages::TRIGGER_PING,
            0,
            data,
            1,
            false
    );

    return true;
}

This code creates a buffer of 1 byte and fills in some data (123). This data is not necessary for a ping and was only added for illustration purpose. The message is sent as a ModuleMessage with the moduleId automatically added by the SendModuleActionMessage method. The actionType is TRIGGER_PING. The message type `MESSAGE_TYPE_MODULE_TRIGGER_ACTION``is used for sending messages that await a response.

The ConnectionManager (cm) will handle the transmission of this packet, it will copy the packet to its buffer and queue the packet transmission. It is important to pass the size of payload (1). The last parameter is used to specify that this packet should be transmitted by using BLE-unacknowledged packet transmission (WRITE_CMD).

Step 6

Next, we want to see if the packet arrived at its destination, we’ll need to implement the MeshMessageReceivedEventHandler in our PingModule which looks like this:

void PingModule::MeshMessageReceivedHandler(BaseConnection* connection, BaseConnectionSendData* sendData, connPacketHeader* packetHeader)
{
    //Must call superclass for handling
    Module::MeshMessageReceivedHandler(connection, sendData, packetHeader);

    //Filter trigger_action messages
    if(packetHeader->messageType == MESSAGE_TYPE_MODULE_TRIGGER_ACTION){
        connPacketModule* packet = (connPacketModule*)packetHeader;

        //Check if our module is meant and we should trigger an action
        if(packet->moduleId == moduleId){
            //It's a ping message
            if(packet->actionType == PingModuleTriggerActionMessages::TRIGGER_PING){

                //Inform the user
                logt("PINGMOD", "Ping request received with data: %d", packet->data[0]);

                //TODO: Send ping response
            }
        }
    }
}

In the PingModule.h, you must now also add the definition for this handler or uncomment it.

We can now perform a simple test by flashing this new firmware on our development board again. There is a simple trick that allows us to test the functionality with a single node by pinging the node itself:

pingmod_2

The ConnectionManager will parse the packet and will route it back to the MeshMessageReceived without broadcasting it because the nodeId is the same as its own. As you can see, the packet triggered the appropriate action in the node.

Step 7

With this working, you should now perform a test with two different nodes. Flash both of them, connect with two terminals and watch how the packet is delivered:

pingmod_3

Step 8

Now, a proper ping message should, well, …​. pong. That’s why we need a return packet. Go to PingModule.h and add another enum that contains action responses:

enum PingModuleActionResponseMessages{
    PING_RESPONSE=0
};

Then, go back to your .cpp file and insert this updated code:

void PingModule::MeshMessageReceivedHandler(BaseConnection* connection, BaseConnectionSendData* sendData, connPacketHeader* packetHeader)
{
    //Must call superclass for handling
    Module::MeshMessageReceivedHandler(connection, sendData, packetHeader);

    //Filter trigger_action messages
    if(packetHeader->messageType == MESSAGE_TYPE_MODULE_TRIGGER_ACTION){
        connPacketModule* packet = (connPacketModule*)packetHeader;

        //Check if our module is meant and we should trigger an action
        if(packet->moduleId == moduleId){
            //It's a ping message
            if(packet->actionType == PingModuleTriggerActionMessages::TRIGGER_PING){

                //Inform the user
                logt("PINGMOD", "Ping request received with data: %d", packet->data[0]);

                u8 data[2];
                data[0] = packet->data[0];
                data[1] = 111;

                //Send ping packet to that node
                SendModuleActionMessage(
                        MESSAGE_TYPE_MODULE_ACTION_RESPONSE,
                        packetHeader->sender,
                        PingModuleActionResponseMessages::PING_RESPONSE,
                        0,
                        data,
                        2,
                        false
                );
            }
        }
    }

    //Parse Module action_response messages
    if(packetHeader->messageType == MESSAGE_TYPE_MODULE_ACTION_RESPONSE){

        connPacketModule* packet = (connPacketModule*)packetHeader;

        //Check if our module is meant and we should trigger an action
        if(packet->moduleId == moduleId)
        {
            //Somebody reported its connections back
            if(packet->actionType == PingModuleActionResponseMessages::PING_RESPONSE){
                logt("PINGMOD", "Ping came back from %u with data %d, %d", packet->header.sender, packet->data[0], packet->data[1]);
            }
        }
    }
}

This code sends a response to the ping request, includes the data that came with the initial request and adds some more data. Also, it adds another condition that checks for the reply to the ping request and prints it out on the terminal.

Step 9

That’s it, you should now be able to ping any node in the mesh network and see its response. The intermediate nodes will automatically route all traffic without having to know what kind of message it is.

pingmod_4

You would probably want to use a counter with the ping message to generate a handle for a ping. Then, you’d be able to calculate the time that it took for the packet to come back through the mesh. And as I’ve said initially, you would not necessarily want to create new module for pinging other nodes but you’d have that functionality in a core module.

I hope you’ve been successful by following this tutorial and I’ll wait for the modules you’re going to implement on top of FruityMesh :-)