Developing Codelets in C++

The goal of this tutorial is to develop two codelets in C++: The first is effectively a machine that goes “ping”, while the second listens for and ingests the “ping” message. For this tutorial, no external dependencies or special hardware is required.

Creating a New Application

Each Issac Robotics application requires an application JSON file and a Bazel build file. We will create these files first before writing the codelets.

Create a new directory for the application

Create a folder in the isaac/sdk/packages directory with a command similar to the following:

bob@desktop:~/isaac/sdk/packages/$ mkdir ping

For the rest of the tutorial, whenever you are asked to create a new file, place it directly into this folder. More complicated packages will have subfolders, but for this tutorial things are kept simple.

Create an application JSON file

Every Isaac application is based on a JSON file, which describes the dependencies of the application, the node graph, and the message flow; it also contains custom configuration data. Create a new JSON file called ping.app.json and specify its name as seen in the following snippet:

{
  "name": "ping"
}

Create a Bazel build file

Next, create a Bazel build file for compiling and running the application. Bazel provides very good dependency management and excellent build speed for large projects, and Bazel build files are very easy to write. Create a file named BUILD with a new app target named ping, as shown below:

load("@com_nvidia_isaac_sdk//bzl:module.bzl", "isaac_app", "isaac_cc_module")

isaac_app(
     name = "ping"
)

Build the application with Bazel

Now you can build the application by running the following command in the sdk directory:

bob@desktop:~/isaac/sdk/$ bazel build packages/ping:ping

The command may take some time as all external dependencies for the Isaac Robot Engine are downloaded and compiled. After a while, the first build should succeed with output similar to the following:

bob@desktop:~/isaac/sdk/$ bazel build packages/ping:ping
INFO: Analyzed target //packages/ping:ping (69 packages loaded, 4167 targets configured).
INFO: Found 1 target...
Target //packages/ping:ping up-to-date:
  bazel-bin/packages/ping/run_ping
  bazel-bin/packages/ping/ping
INFO: Elapsed time: 10.602s, Critical Path: 8.60s
INFO: 3 processes: 3 linux-sandbox.
INFO: Build completed successfully, 8 total actions

Next you can run your new application by executing the following command:

bob@desktop:~/isaac/sdk/$ bazel run packages/ping:ping

This will start the ping application and keep it running. You can stop a running application by pressing Ctrl+C in the console. This will gracefully shut down the application.

You will notice that not much is happening because we don’t have an application graph yet. Next we will create some nodes for the application.

Creating a Node

An Isaac application consists of many nodes running in parallel. They can send each other messages or interact with each other using various other mechanisms provided by the Isaac Robot Engine. Nodes are lightweight and do not require their own processes, or even their own threads.

To customize the behavior of the ping node, we have to equip it with components. We will create our own component called “Ping”.

Create a codelet .hpp file

Create a new file called Ping.hpp in the ping directory, with the following contents:

#pragma once
#include "engine/alice/alice_codelet.hpp"
class Ping : public isaac::alice::Codelet {
 public:
  void start() override;
  void tick() override;
  void stop() override;
};
ISAAC_ALICE_REGISTER_CODELET(Ping);

Codelets provide three main functions, which can be overloaded: start, tick and stop. When a node is started, the start functions of all attached codelets are called first. For example, start is a good place to allocate resources. You can configure a codelet to tick periodically or each time a new message is received. Most of the functionality is then performed by the tick function.

At the end, when a node stops, the stop function is called. You should free all previously allocated resources in the stop function. Do not use constructors or destructors: You do not have access to any Isaac Robot Engine functionality (such as configuration) in the constructor.

Each custom codelet you create needs to be registered with the Isaac Robot Engine. This is done at the end of the file using the ISAAC_ALICE_REGISTER_CODELET macro. If your codelet is inside a namespace, you have to provide the fully qualified type name, for example ISAAC_ALICE_REGISTER_CODELET(foo::bar::MyCodelet);.

Create a codelet .cpp File

To add some functionality to the codelet, create a source file called Ping.cpp, which contains this functionality:

#include "Ping.hpp"
void Ping::start() {}
void Ping::tick() {}
void Ping::stop() {}

Define the tick() behavior

Codelets can tick in different ways, but for now we will use periodic ticking, which can be achieved by calling the tickPeriodically function in the Ping::start function. Add the following code to the start function in Ping.cpp:

void Ping::start() {
  tickPeriodically();
}

Add a log message

To verify that something is in fact happening, we will print a message when the codelet ticks. The Isaac SDK includes utility functions for logging data; LOG_INFO can be used to print a message on the console. It follows the printf-style syntax. Add the tick function to Ping.cpp as shown below:

void Ping::tick() {
  LOG_INFO("ping");
}

Add the component to the BUILD file

Add the component to the BUILD file as a module, as shown below:

isaac_app(
  ...
)

isaac_cc_module(
  name = "ping_components",
  srcs = ["Ping.cpp"],
  hdrs = ["Ping.hpp"],
)

An Isaac module defines a shared library that encapsulates a set of codelets and can be used by different applications.

Add a new node to the JSON File

To use the Ping codelet in the application, we first need to create a new node in the application JSON file:

{
  "name": "ping",
  "graph": {
    "nodes": [
      {
        "name": "ping",
        "components": []
      }
    ],
    "edges": []
  }
}

Add the Ping codelet to the node

Each node can contain multiple components, which define its functionality. Add the Ping codelet to the node by adding a new section in the components array:

{
  "name": "ping",
  "graph": {
    "nodes": [
      {
        "name": "ping",
        "components": [
          {
            "name": "ping",
            "type": "Ping"
          }
         ]
      }
    ],
    "edges": []
  }
}

An application graph normally has edges connecting different nodes, which determine the message-passing sequence between nodes. Because this application does not have any other nodes, we will leave the edges blank.

Add the component to the modules lists

If you try to run this application, it will panic and show the error message Could not load component ‘Ping’. This happens because all components used in an application must be added to the modules list. You need to do this both in the BUILD file and in the application JSON file:

load("@com_nvidia_isaac_sdk//bzl:module.bzl", "isaac_app", "isaac_cc_module")

isaac_app(
    name = "ping",
    modules = ["//packages/ping:ping_components"]
)
{
  "name": "ping",
  "modules": [
    "//packages/ping:ping_components"
  ],
  "graph": {
    ...
  }
}

If you run the application now, you will get a different panic message: “Parameter ‘ping/ping/tick_period’ not found or wrong type”. This message appears because we need to set the tick period of the Ping codelet in the configuration section. We will do this in the next section.

Configuration

Most code requires various parameters for customizing behavior. For example, you might want to give the user of our ping machine the option to change the tick period. In the Isaac framework, this can be achieved with configuration.

Let’s specify the tick period in the “config” section of the application JSON file so that we can finally run the application.

{
  "name": "ping",
  "modules": [
    "//packages/ping:ping_components"
  ],
  "graph": {
    ...
  },
  "config": {
    "ping" : {
      "ping" : {
        "tick_period" : "1Hz"
      }
    }
  }
}

Every configuration parameter is referenced with three elements: node name, component name, and parameter name. In this case we are setting the parameter tick_period of the component ping in the node ping.

Note

Configuration values must match the data type specified in the component API. See the Component API Overview or the component .hpp file for the expected data type. Also note that an integer value is accepted as a type of double value.

Now the application will run successfully and print ping once a second. You should see output similar to the snippet below. You can gracefully stop the application by pressing Ctrl+C.

bob@desktop:~/isaac/sdk/packages/ping$ bazel run ping
2019-03-24 17:09:39.726 DEBUG   engine/alice/backend/codelet_backend.cpp@61: Starting codelet 'ping/ping' ...
2019-03-24 17:09:39.726 DEBUG   engine/alice/backend/codelet_backend.cpp@73: Starting codelet 'ping/ping' DONE
2019-03-24 17:09:39.726 DEBUG   engine/alice/backend/codelet_backend.cpp@291: Starting job for codelet 'ping/ping'
2019-03-24 17:09:39.726 INFO    packages/ping/Ping.cpp@8: ping
2019-03-24 17:09:40.727 INFO    packages/ping/Ping.cpp@8: ping
2019-03-24 17:09:41.726 INFO    packages/ping/Ping.cpp@8: ping

Add a new parameter to the codelet .hpp file

The tick_period parameter is automatically created for us, but we can also create our own parameters to customize the behavior of codelets. Add a parameter to your codelet as shown below:

class Ping : public isaac::alice::Codelet {
 public:
  void start() override;
  void tick() override;
  void stop() override;
  ISAAC_PARAM(std::string, message, "Hello World!");
};

ISAAC_PARAM takes three arguments:

  1. The type of the parameter, usually double, int, bool, or std::string.
  2. The name of the parameter, which is used to access or specify the parameter.
  3. The default value of the parameter. If no default value is given, and the parameter is not specified via a configuration file, the program asserts when the parameter is accessed.

The ISAAC_PARAM macro creates an accessor called get_message and a bit more code to properly connect the parameter with the rest of the system.

Use the new parameter from the codelet .hpp file

We can now use the parameter in the tick() function instead of the hard-coded value. Call get_message() to retrieve the value of the message parameter:

void tick() {
  LOG_INFO(get_message().c_str());
}

Configure the parameter in the JSON file

The next step is to add the configuration for the node. Use the node name (ping), component name (ping), and the parameter name (message) to specify the desired value.

{
  "name": "ping",
  "modules": [
    "//packages/ping:ping_components"
  ],
  "graph": {
    ...
  },
  "config": {
    "ping" : {
      "ping" : {
        "message": "My own hello world!",
        "tick_period" : "1Hz"
      }
    }
  }
}

That’s it! You now have an application that can periodically print a custom message. Run the application with the following command:

bob@desktop:~/isaac/sdk/packages/ping$ bazel run ping

As expected, the codelet prints the message periodically on the command line.

Sending Messages

The custom codelet Ping is now happily ticking. For other nodes to react to the ping, the Ping codelet must send a message that other codelets can receive.

Add the ISAAC_PROTO_TX macro to the codelet .hpp file

Publishing a message is easy. Use the ISAAC_PROTO_TX macro to specify that a codelet is publishing a message. Add it to Ping.hpp as shown below:

 #pragma once

 #include "engine/alice/alice.hpp"
 #include "messages/ping.capnp.h"

 class Ping : public isaac::alice::Codelet {
  public:
   ...

   ISAAC_PARAM(std::string, message, "Hello World!");
   ISAAC_PROTO_TX(PingProto, ping);
 };

ISAAC_ALICE_REGISTER_CODELET(Ping);

The ISAAC_PROTO_TX macro takes two arguments. The first argument specifies the message to publish. Here, use the PingProto message, which comes with the Isaac message API. Access PingProto by including the corresponding header file. The second argument specifies the name of the channel under which we want to publish the message.

Modify the tick() function in the codelet .cpp file

Next, change the tick() function to publish a message instead of printing to the console. The Isaac SDK currently supports cap’n’proto messages. Protos are a platform- and language-independent way of representing and serializing data. Creating a message is initiated by calling the initProto function on the accessor that the ISAAC_PROTO_TX macro created. This function returns a cap’n’proto builder object, which can be used to write data directly to the proto.

The ProtoPing message has a field called message of type string, so in this instance we can use the .setMessage() function to write some text to the proto. After the proto is populated, we can send the message by calling the publish function. This immediately sends the message to any connected receivers.

Change the .tick() function in Ping.cpp to the following:

...
void Ping::tick() {
  // create and publish a ping message
  auto proto = tx_ping().initProto();
  proto.setMessage(get_message());
  tx_ping().publish();
}
...

Add the MessageLedger component to the node

Lastly, upgrade the node (in the JSON file) to support message passing. Nodes in Isaac SDK are by default light-weight objects requiring minimal setup of mandatory components, and some nodes in your application may not need to send or receive messages.

To enable message passing on a node, we need to add a component called MessageLedger. This component handles incoming and outgoing messages and relays them to MessageLedger components in other nodes.

{
  "name": "ping",
  "graph": {
    "nodes": [
      {
        "name": "ping",
        "components": [
          {
            "name": "message_ledger",
            "type": "isaac::alice::MessageLedger"
          },
          {
            "name": "ping",
            "type": "Ping"
          }
         ]
      }
    ],
    "edges": []
},
"config": {
  ...
}

Build and run the application. It appears that nothing happens because right now nothing is connected to your channel. While you are publishing a message, no one is there to receive it and react to it. We will fix that in the next section.

Receiving Messages

You need a node that can receive the ping message and react to it in some way. For this purpose, let’s create a Pong codelet, which is triggered by the message sent by Ping.

Create a codelet .hpp file for Pong

Create a new file named Pong.hpp with the following contents:

#pragma once
#include "engine/alice/alice.hpp"
#include "messages/ping.capnp.h"

class Pong : public isaac::alice::Codelet {
 public:
  void start() override;
  void tick() override;

  // An incoming message channel on which we receive pings.
  ISAAC_PROTO_RX(PingProto, trigger);

  // Specifies how many times we print 'PONG' when we are triggered
  ISAAC_PARAM(int, count, 3);
};

ISAAC_ALICE_REGISTER_CODELET(Pong);

Add the Pong component to the BUILD file

The Pong codelets need to be added to the ping_components module in order to be compiled. Add them to the BUILD file as shown below (we will create the Pong.cpp file later in this section):

isaac_cc_module(
  name = "ping_components",
  srcs = [
    "Ping.cpp",
    "Pong.cpp"
  ],
  hdrs = [
    "Ping.hpp",
    "Pong.hpp"
  ],
)

Create a Pong node in the JSON file

In the application JSON file, create a second node and attach the new Pong codelet to it.

"nodes": [
  {
    "name": "ping",
    "components": [
      {
        "name": "message_ledger",
        "type": "isaac::alice::MessageLedger"
      },
      {
        "name": "ping",
        "type": "Ping"
      }
    ]
  },
  {
    "name": "pong",
    "components": [
      {
        "name": "message_ledger",
        "type": "isaac::alice::MessageLedger"
      },
      {
        "name": "pong",
        "type": "Pong"
      }
    ]
  }
],

Add an edge to the JSON file

Edges connect receiving RX channels to transmitting TX channels. A transmitting channel can transmit data to multiple receivers. A receiving channel can also receive data from multiple transmitters; however, this comes with caveats and is discouraged.

Similar to parameters, channels are referenced with three elements: node name, component name, and channel name. An edge can be created by adding it to the edges section in the application JSON file. Here source is the full name of the transmitting channel and target is the full name of the receiving channel.

Connect the Ping and the Pong nodes using an edge:

{
  "name": "ping",
  "modules": [
    "ping:ping_components"
  ],
  "graph": {
    "nodes": [
      {
        "name": "ping",
        "components": [
          {
            "name": "message_ledger",
            "type": "isaac::alice::MessageLedger"
          },
          {
            "name": "ping",
            "type": "Ping"
          }
        ]
      },
      {
        "name": "pong",
        "components": [
          {
            "name": "message_ledger",
            "type": "isaac::alice::MessageLedger"
          },
          {
            "name": "pong",
            "type": "Pong"
          }
        ]
      }
    ],
    "edges": [
      {
        "source": "ping/ping/ping",
        "target": "pong/pong/trigger"
      }
    ]
  },
  "config": {
    "ping" : {
      "ping" : {
        "message": "My own hello world!",
        "tick_period" : "1Hz"
      }
    }
  }
}

Create a codelet .cpp for Pong

The last remaining task is to set up the Pong codelet to do something when it receives the ping. Create a new file named Pong.cpp. Call the tickOnMessage() function in start() to instruct the codelet to tick each time it receives a new message on that channel. In tick(), we add the functionality to print out “PONG!” as many times as defined by the count parameter in the Pong header file:

#include "Pong.hpp"

#include <cstdio>

void Pong::start() {
  tickOnMessage(rx_trigger());
}

void Pong::tick() {
  // Parse the message we received
  auto proto = rx_trigger().getProto();
  const std::string message = proto.getMessage();

  // Print the desired number of 'PONG!' to the console
  const int num_beeps = get_count();
  std::printf("%s:", message.c_str());
  for (int i = 0; i < num_beeps; i++) {
    std::printf(" PONG!");
  }
  if (num_beeps > 0) {
    std::printf("\n");
  }
}

By using tickOnMessage() instead of tickPeriodically(), we instruct the codelet to only tick when a new message is received on the incoming data channel, in this case trigger. The tick function now only executes when you receive a new message. This is guaranteed by the Isaac Robot Engine.

Run the application. You should see that a “pong” is generated every time the Pong codelet receives a ping message from the Ping codelet. By changing the parameters in the configuration file, you can change the interval at which a ping is created, alter the message that is sent together with each ping, and print pong more or less often whenever a ping is received.

Sending Messages Over a Network

If the Ping and Pong components run on different devices, we need network connections. The TcpPublisher and TcpSubscriber nodes facilitate network connections as shown below:

{
  "name": "ping",
  "modules": ["engine_tcp_udp"],
  "graph": {
    "nodes": [
      ...
      {
        "name": "pub",
        "components": [
          {
            "name": "message_ledger",
            "type": "isaac::alice::MessageLedger"
          },
          {
            "name": "tcp_publisher",
            "type": "isaac::alice::TcpPublisher"
          }
        ]
      }
    ],
    "edges": [
      {
        "source": "ping/ping/ping",
        "target": "pub/tcp_publisher/tunnel"
      }
    ]
  },
  "config": {
    ...
    "pub": {
      "tcp_publisher": {
        "port": 5005
      }
    }
  }
}

The port parameter specifies the network port for accepting a connection. Make sure it is available on the device. On the other end, TcpSubscriber can deliver messages when set up in the JSON file as shown below:

{
  "name": "pong",
  ...
  "graph": {
    "nodes": [
      ...
      {
        "name": "sub",
        "components": [
          {
            "name": "message_ledger",
            "type": "isaac::alice::MessageLedger"
          },
          {
            "name": "tcp_receiver",
            "type": "isaac::alice::TcpSubscriber"
          }
        ]
      }
    ],
    "edges": [
      {
        "source": "sub/tcp_receiver/tunnel",
        "target": "pong/pong/trigger"
      }
    ]
  },
  "config": {
    ...
    "sub": {
      "tcp_receiver": {
        "port": 5005,
        "reconnect_interval": 0.5,
        "host": "127.0.0.1"
      }
    }
  }
}

The host parameter specifies the IP address to listen to. Make sure host and port specify the open port and IP address of the device where Ping is running. Run these applications on separate devices to see messages communicated through the network.

This is just quick start with a very simple application. A real-world application consists of dozens of nodes, each with multiple components with one or more codelets. Codelets receive multiple types of messages, call specialized libraries to solve hard computational problems, and publish their results again to be consumed by other nodes.