Skip to content
James Edmondson edited this page Dec 19, 2023 · 9 revisions

Overview

This page is for users who would like to port their existing threaded or non-threaded C++ apps to use the thread-safe knowledge base for data transfer between agents.

What You Likely Have

  • Object-oriented classes
  • Unsafe code, message passing, etc.
  • A main program

What You Will Have After You're Done

  • Object-oriented classes
  • Unsafe and safe code, thread-safe data sharing, networkable data shared over network
  • A main program

Your Existing Data Structure

Most C++ applications have either classes or structs that are nested in a hierarchical way to represent something about their environment, a robot, an agent, or some other programming concept. Let's call this data structure that you might want to pass around RobotData. It may look something like this:

// data/RobotData.h
#pragma once

namespace data {
  class PoseData {
    public:
      double latitude;
      double longitude;
      double altitude;
      double heading;
  };

  class RobotData {
    public:
      PoseData pose;
  };
}

And then you use that data structure everywhere. You pass this data structure around. You serialize and deserialize to/from it, possibly using ROS or some other message passing system to move this data around, create snapshots of the data, etc. You get a similar look and feel when you use MADARA containers (object-oriented classes that point into the knowledge base, which is networked to other agents).

Your New MADARA Data Structure

// data/RobotData.h
#pragma once

#include "madara/knowledge/containers/Double.h"

namespace data {
  class PoseData {
    public:
      madara::knowledge::containers::Double latitude;
      madara::knowledge::containers::Double longitude;
      madara::knowledge::containers::Double altitude;
      madara::knowledge::containers::Double heading;
  };

  class RobotData {
    public:
      PoseData pose;
  };
}

// declared elsewhere, often globally, but not necessarily. We need the kb to stay in scope as
// long as any threads referencing the RobotData structure stay in scope. In other words, when
// the threads are deleted, make sure the kb is not cleaned up until after the threads are joined
madara::knowledge::KnowledgeBase kb;

We've essentially replaced the C++ primitive double with madara::knowledge::containers::Double, and we've added a MADARA Knowledge Base somewhere (either in a main function or a global variable somewhere). We're not completely done with the PoseData or RobotData though. You probably want them to be initialized properly with the knowledge base so anything you change in pose is reflected in the Knowledge Base.

Setting Up Your MADARA containers


// data/RobotData.h
#pragma once

#include "madara/knowledge/containers/Double.h"

namespace data {
  class PoseData {
    public:
      /**
       * Default constructor
       * @param   prefix    a prefix to append to this robot data (e.g., "agent.0")
       * @param   kb        the knowledge base to update
       **/
      PoseData(madara::knowledge::KnowledgeBase & kb, const std::string & prefix = "agent.0")
        : latitude(prefix + ".latitude", kb, 0.0),
        : longitude(prefix + ".longitude", kb, 0.0),
        : altitude(prefix + ".altitude", kb, 0.0),
        : heading(prefix + ".heading", kb, 0.0)
      {
        
      }

      madara::knowledge::containers::Double latitude;
      madara::knowledge::containers::Double longitude;
      madara::knowledge::containers::Double altitude;
      madara::knowledge::containers::Double heading;
  };

  class RobotData {
    public:
      /**
       * Default constructor
       * @param   prefix    a prefix to append to this robot data (e.g., "agent.0")
       * @param   kb        the knowledge base to update
       **/
      RobotData(madara::knowledge::KnowledgeBase & kb, const std::string & prefix = "agent.0")
        : pose(kb, prefix)
      {}

      PoseData pose;
  };
}

What we've done is initialized the RobotData structure to point to locations inside the Knowledge Base with a prefix of "agent.0" by default but this is configurable. So, if we want to do a multi-agent system, we just pass a unique name in to each agent (e.g., at the command line or in a config file), and then each robot will update its pose data and send that to other agents.

Interacting with your MADARA Data Structure

my_app.cpp

#include <chrono>
#include <thread>

#include "data/RobotData.h"
#include "madara/knowledge/KnowledgeBase.h"

int main(int argc, char ** argv) {
  // MADARA has many constructors but we'll just manually set the transport later
  // as it is easier to show how to extend it for command line parsing
  madara::knowledge::KnowledgeBase kb;

  // let's broadcast to the world
  madara::transport::QoSTransportSettings transport;
  settings.hosts.push_back ("192.168.0.255:15000");
  settings.type = Madara::Transport::BROADCAST;

  // we'll call ourselves agent.0  
  std::string agent_id = "agent.0";

  // attach this transport to the kb
  kb.attach_transport(agent_id, transport);

  // create the robot data structure and set up what we'll update
  data::RobotData data(kb, agent_id);

  // let's just move in a diagonal line in 3D and update the world every second for 4 seconds
  for (int i = 0; i < 4; ++i) {
    data.pose.latitude = (double)i;
    data.pose.longitude = (double)i;
    data.pose.altitude = (double)i;
    data.pose.heading = (double)i * 90; // turn in a full circle over 4 seconds

    // send an update to the world
    kb.send_modifieds();

    // sleep for one second between updating and sending data
    std::this_thread::sleep_for(std::chrono::seconds(1));
  }

  return 0;
}

Pretty straightforward. We set up a transport, attach it to a knowledge base, update some data, send all the modifieds cumulatively to the world, and then wait for a second before updating.

Interacting with your MADARA Data Structure

Thread-Safe Data Updates

The Knowledge Base is thread-safe, but that doesn't mean your code is. If you have multiple threads that might update your position or latitude/longitude/altitude, for instance via sensor fusion techniques where multiple sensors are updating this, then you probably want to put a mutex guard around sections of the code that should be consistently updated together. We can do this with a very small addition to the code

#include <chrono>
#include <thread>

#include "data/RobotData.h"
#include "madara/knowledge/KnowledgeBase.h"

int main(int argc, char ** argv) {
  // MADARA has many constructors but we'll just manually set the transport later
  // as it is easier to show how to extend it for command line parsing
  madara::knowledge::KnowledgeBase kb;

  // let's broadcast to the world
  madara::transport::QoSTransportSettings transport;
  settings.hosts.push_back ("192.168.0.255:15000");
  settings.type = Madara::Transport::BROADCAST;

  // we'll call ourselves agent.0  
  std::string agent_id = "agent.0";

  // attach this transport to the kb
  kb.attach_transport(agent_id, transport);

  // create the robot data structure and set up what we'll update
  data::RobotData data(kb, agent_id);

  // let's just move in a diagonal line in 3D and update the world every second for 4 seconds
  for (int i = 0; i < 4; ++i) {
    // THREAD SAFE UPDATE
    {
      // Can also use madara::knowledge::ContextGuard in the same way
      std::lock_guard guard(kb);

      data.pose.latitude = (double)i;
      data.pose.longitude = (double)i;
      data.pose.altitude = (double)i;
      data.pose.heading = (double)i * 90; // turn in a full circle over 4 seconds
    }

    // send an update to the world
    kb.send_modifieds();

    // sleep for one second between updating and sending data
    std::this_thread::sleep_for(std::chrono::seconds(1));
  }

  return 0;
}

The only change here is that we have added a scope to the data.pose.* updates that will use STL lock_guard to lock the Knowledge Base. You'll be able to exclusively lock the kb against any updates from the network or from local threads trying to make their own updates and provide a safe update process to prevent race conditions (e.g., a thread wrote a different latitude, but you wrote the longitude, altitude, and heading changes, resulting in an inconsistent state for the robot). With this simple guard addition, you're update process is thread-safe.

Updating CMake Processes

To update your CMake to include MADARA, you just need to add find_package(madara REQUIRED), which should find the installation in /usr/local if you've installed MADARA. You then simply need to add target_link_libraries(app_name PRIVATE madara) in your cmake file for each executable or library that is linking to MADARA and it will setup your include directories and libraries correctly.

An example CMakeLists.txt file in the project root might look like this:

cmake_minimum_required(VERSION 3.5)
project(my_app)

# Set the path to your project root directory
set(PROJECT_ROOT ${CMAKE_SOURCE_DIR})
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g")

# Add the project root directory to the include directories
include_directories(${PROJECT_ROOT})

# Find MADARA (only successful if you build install MADARA using scripts/linux/cmake.sh or similar)
find_package(madara REQUIRED)

add_executable(my_app
    ${PROJECT_ROOT}/src/my_app.cpp
)

# Link your my_app.cpp to the MADARA lib
target_link_libraries(my_app PRIVATE madara)

# Feel free to add whatever standard you want to use for compilation (11, 14, 17, etc.)
set_property(TARGET ${TARGET_NAME} PROPERTY CXX_STANDARD 14)