Custom isaac-ros-nitros msgs

Hi,

I’m trying to create a custom Nitro message for my CUDA ROS 2 node, but I can’t find any documentation on how to approach this.

Could someone provide the steps or point me to a guide on creating and integrating custom Nitro messages in ROS 2? Any examples would be greatly appreciated.

Thanks in advance!

Hello @dresi,

Thanks for posting in the Isaac ROS forum!

If you’d like to build your own NITROS type and node, you can refer to the folders below:

isaac_ros_managed_nitros_examples/custom_nitros_image

isaac_ros_managed_nitros_examples/custom_nitros_message_filter

Also, you can check “Managed NITROS” docs for the NITROS publisher and subscriber concepts.

Hi

I was using these as a tutorial for how to use Nitro pub/sub in my ROS 2 node. However, the existing Nitro message types do not include the fields I need.

I need a custom message—for example, something similar to nvidia::isaac_ros::nitros::NitrosCustomListMap (namespace TBD custom::custom::NitroCustomListMap) where I can define the message fields myself.

Is there a way to create a custom Nitro message?

Hi,
I’m still trying to create a custom message for Nitros. I need a modified version of the occupancyGrid and then a message with a List of occupancyGrid.

I used the occupancy grid nitro msgs as a base to learn how to read the arguments: isaac_ros_nitros/isaac_ros_nitros_type/isaac_ros_nitros_occupancy_grid_type/src/nitros_occupancy_grid.cpp at main · NVIDIA-ISAAC-ROS/isaac_ros_nitros · GitHub

I created the ros msgs interfaces. But now I’m stuck on how to pass the list to the gfx entity. I know I can’t use the same approach as the TensorList since I do not have a gfx entity, but I saw I could use a createOccupancySetMessage and getOccupancySetMessage, but I’m not sure how or if it’s the correct approach.

auto griddata_msg = msg_entity->get<mycustom::OccupancySet>()

OR 
griddata_msg =  createOccupancySetMessage( msg_entity.value())

 

for (map : griddata_msg){

grid_data = mycustom::OccupancyGrid();

grid_data.name = map→get<int>(“name”);

}

From your pseudocode,

auto griddata_msg = msg_entity->get<mycustom::OccupancySet>();

// OR
griddata_msg = createOccupancySetMessage(msg_entity.value());

these are not interchangeable:

  • msg_entity->get<mycustom::OccupancySet>() Used when some upstream GXF codelet has already created the message and you’re just reading it (typical for convert_to_ros()).

  • createOccupancySetMessage(...) Used when you’re in ROS → GXF direction, and you need to allocate and populate the GXF message from a ROS msg (typical for convert_to_gxf()).

If you’ve already created your own ROS msg type something like below,

# CustomOccupancyGrid.msg
string name
float32 resolution

# … other fields …

# CustomOccupancyGridList.msg
CustomOccupancyGrid grids

then in your Nitros type’s ROS → GXF conversion you can do something like:

void convert_to_gxf(const my_pkg::msg::CustomOccupancyGridList & msg)
{
    // here `msg` is CustomOccupancyGridList
    // and `msg.grids` is the array field defined above
}

and inside that function follow this pattern to pass the list into the GXF entity.

// PSEUDOCODE – use the real OccupancySet API from its header

// Allocate an entity that owns an OccupancySet.
auto maybe_entity = createOccupancySetMessage(context /* or nitros ctx */);
if (!maybe_entity) {
  // handle error
}
auto entity = std::move(maybe_entity.value());

// Get your OccupancySet component off the entity
auto occ_set = entity.getmycustom::OccupancySet().value();

// Resize / add entries to match msg.grids.size()
occ_set->resize(msg.grids.size());   // or addGrid(i), depending on API

for (size_t i = 0; i < msg.grids.size(); ++i) {

  auto & map = occ_set->at(i);       // or operator[] / getGrid(i) / iterator

  // Fill fields from your ROS message (keys depend on your OccupancySet schema)
  map.set<std::string>("name", src.name);
  map.set<float>("res", src.resolution);
  // … etc: copy width, height, origin, data, etc., according to your design ...
}

// Return ‘entity’ as the GXF message for NITROS

Note: this is not a fully validated implementation. You’ll need to adapt it to your actual OccupancySet API and your specific use case.

Thanks this is very useful, I got confused by the two functions.

When you say your actual OccupancySet API, you mean the API I created when defining the gxf entity?

struct MapMetaData : public gxf::Component

{

uint32_t width;

uint32_t height;

float resolution;

  //Pose3d gfx entity   SO3<K> rotation;

  //Vector3<K> translation;

gxf::Handle<gxf::Pose3D> origin;

}




struct GridData : public gxf::Component
{

gxf::Handle<gxf::Timestamp> timestamp;

 //name of the map

std::string name;

//map medatata

gxf::Handle<MapMetaData> info;

  //data type used for the grid TBD
int32_t data_type;

//data buffer

nvidia::gxf::Handle<nvidia::gxf::Tensor> data;

};




struct GridDataList : public gxf::Component
{
gxf::Handle<gxf::Timestamp> timestamp;
std::vector<GridData> grids;
};

Instead of struct should I use a class and define the functions here?

ALso my createMessage is something in this sort if I understood correctly, not sure if it’s correct

gxf_result_t CreateGridDataListMessage(
  gxf_context_t context,
  const isaac_grid_set_list::msg::GridDataList & ros_msg,
  gxf::Entity & entity)
{
  entity = gxf::Entity::New(context);

  auto list = entity.add<GridDataList>();

  list->timestamp = entity.add<gxf::Timestamp>();

  for (const auto & grid_msg : ros_msg.grids) {

    GridData grid;

    grid.timestamp = entity.add<gxf::Timestamp>();
    grid.name = grid_msg.name;

    auto info = entity.add<MapMetaData>();

    info->width = grid_msg.info.width;
    info->height = grid_msg.info.height;
    info->resolution = grid_msg.info.resolution;

    auto pose = entity.add<gxf::Pose3D>();

    pose->translation.x() = grid_msg.info.origin.position.x;
    pose->translation.y() = grid_msg.info.origin.position.y;
    pose->translation.z() = grid_msg.info.origin.position.z;

    pose->rotation = gxf::SO3d::FromQuaternion(
      grid_msg.info.origin.orientation.w,
      grid_msg.info.origin.orientation.x,
      grid_msg.info.origin.orientation.y,
      grid_msg.info.origin.orientation.z);

    info->origin = pose;

    grid.info = info;

    grid.data_type = grid_msg.data_type;

    auto tensor = entity.add<gxf::Tensor>();

    gxf::Shape shape{grid_msg.info.height, grid_msg.info.width};

    tensor->reshape<uint8_t>(
      shape,
      gxf::MemoryStorageType::kDevice);

    std::memcpy(
      tensor->pointer(),
      grid_msg.data.data(),
      grid_msg.data.size());

    grid.tensor = tensor;

    list->grids.push_back(grid);
  }

For now I have:

  • msg definition in msg folder
  • ros/nitros interface msg (not sure about this one how to use it) in the msg folder
  • builder / viewer for pub/sub
  • msg definiton for gxf entity with createMessage / getMessage and the msg gxf entity (as in the code above)
  • type adaptation for ros-nitro/nitro-ros

am I forgetting some steps?

In your design, GridData is declared as a gxf::Component but you never create it with entity.add<GridData>(), you just do GridData grid; and push it into a std::vector<GridData>.
That’s inconsistent with how GXF Components are supposed to be used.

You can fix this in the simpler way by dropping : public gxf::Component from GridData, so it’s just a plain struct that holds handles to real Components. With GridData no longer a Component, storing it by value in std::vector<GridData> is fine.
Also, you need to fix the member name mismatch in CreateGridDataListMessag.
It should be grid.data= tensor;not grid.tensor.

Another issue is kDevice means GPU memory; std::memcpy will not copy host→device correctly. If you only need CPU storage: use MemoryStorageType::kHost (or whichever host storage the allocator provides), then std::memcpy is OK.
If you really want to use kDevice, then replace std::memcpy with a CUDA copy or NITROS utility that copies from host to device.

The existing NITROS occupancy grid type is a good reference for how to wire the allocator + copy properly: