Skip to main content

proton_cpp

proton_cpp is a C++ extension of proton_core. Using the same principles and concepts as proton_core, proton_cpp provides type-safe access to the Node Manager and Signal Registry API's, as well as runtime generation of the node and registry via feature flags.

In its base form without any feature flags set, proton_cpp can be used in embedded projects as it does not use any dynamic allocation, exceptions, RTTI, or smart pointers.

Accessor Functions

proton_cpp uses the same registry structs from proton_core. But to provide more ergonomic wrapping around the C API, non-owning accessor classes exist to read and write to the Registry, as well as updating the Node Manager

Signal Access

proton_cpp provides overload methods via the SignalAccess class for better shorthand when getting/setting Signal values from the Registry

  constexpr explicit SignalAccess(proton_registry_t * registry) noexcept : registry_(registry) {}

proton_status_e get(uint32_t id, double & out) const noexcept;
proton_status_e set(uint32_t id, double value) noexcept;

proton_status_e get(uint32_t id, float & out) const noexcept;
proton_status_e set(uint32_t id, float value) noexcept;

proton_status_e get(uint32_t id, int32_t & out) const noexcept;
proton_status_e set(uint32_t id, int32_t value) noexcept;

proton_status_e get(uint32_t id, int64_t & out) const noexcept;
proton_status_e set(uint32_t id, int64_t value) noexcept;

proton_status_e get(uint32_t id, uint32_t & out) const noexcept;
proton_status_e set(uint32_t id, uint32_t value) noexcept;

proton_status_e get(uint32_t id, uint64_t & out) const noexcept;
proton_status_e set(uint32_t id, uint64_t value) noexcept;

proton_status_e get(uint32_t id, bool & out) const noexcept;
proton_status_e set(uint32_t id, bool value) noexcept;

proton_status_e get(uint32_t id, char * buf, size_t cap, size_t & len) const noexcept;
proton_status_e set(uint32_t id, const char * buf, size_t len) const noexcept;

proton_status_e get(uint32_t id, uint8_t * buf, size_t cap, size_t & len) const noexcept;
proton_status_e set(uint32_t id, const uint8_t * buf, size_t len) const noexcept;

And can be used via the following pattern:

  double value;
SignalAccess access(&g_proton_registry);
proton_status_e status = access.get(PROTON_SIGNAL_DEFAULT_DOUBLE_ID, value);
status = access.set(PROTON_SIGNAL_DEFAULT_DOUBLE_ID, 1.111);

Templated Signal access on a specific ID can be arranged by the Signal<T> class

  Signal<double> signal(&g_proton_registry, PROTON_SIGNAL_DEFAULT_DOUBLE_ID);

double value;
proton_status_e status = signal.get(value);
status = signal.set(1.111);

Feature Flags

  • PROTON_ENABLE_ALLOC
    • Allow for Signal<std::string> and Signal<std::vector<uint8_t>> to represent string and bytes signals.
    • Enables RTTI to enable dynamic casting a SignalBase to a Signal<T> via the as() method
SignalBase sig_base(&g_proton_registry, PROTON_SIGNAL_DEFAULT_DOUBLE_ID);
Signal<double> sig_double = sig_base.as<double>();
  • C++20 and up
    • Convenience methods for bytes Signals using std::span<uint8_t>

Bundle Access

Bundle functionality is largely a thin wrapper over the Bundle API's in proton_core with some ergonomics

  explicit BundleAccess(proton_registry_t * registry, uint32_t id) noexcept;

uint32_t id() const noexcept { return id_; }

const bundle_desc_t * descriptor() const noexcept;

void set_period(uint32_t period_ms) noexcept;
void set_callback(proton_bundle_cb_f cb, void * ctx) noexcept;

std::optional<SignalBase> operator[](uint32_t signal_id) const noexcept;

Feature Flags

  • PROTON_ENABLE_ALLOC: Enables setting Bundle callbacks with std::function
note

calling BundleAccess::set_callback() with a std::function intentionally leaks memory. This is largely due to the fact that the function object is required to live as long as the Bundle (indefinitely). So the std::function is intentionally released from RAII automatic destruction to prevent dangling pointers.

Node Access

The same Node Manager API remains via NodeAccess

proton_status_e receive(const uint8_t * buffer, size_t len) noexcept
{
return proton_node_receive(node_, buffer, len);
}

uint32_t id() const noexcept { return node_->id; }

size_t num_peers() const noexcept { return node_->num_peers; }

proton_status_e update(
uint64_t uptime_ms, uint8_t * buffer, size_t buffer_len, size_t & out_len,
Endpoint * dest_peers, size_t num_dest_peers, size_t & num_selected_peers) noexcept
{
return proton_node_update(
node_, uptime_ms, buffer, buffer_len, &out_len, dest_peers, num_dest_peers,
&num_selected_peers);
}

proton_status_e trigger_bundle(uint32_t bundle_id) noexcept
{
return proton_node_trigger_bundle(node_, bundle_id);
}

Feature Flags

  • PROTON_ENABLE_ALLOC: Enables BundleAccess::on_bundle_update(uint32_t, std::function) as a passthrough for setting Bundle callbacks
  • C++20: Enables std::span overloads for methods that usually take a pointer + length combo.

Registry Locking

A utility class proton::ScopedLock enables RAII access to the Signal Registry if the Registry has the lock/unlock functions set.

void do_something_with_signal_registry(proton_registry_t * registry) {
proton::ScopedLock lock(registry);

// Signals can now be read/written in this scope
}

Transport API

The transport API in proton_cpp is largely untouched compared to proton_core, operating as helper methods, since the original transport API does not contain any stateful info.

Feature Flags

C++20: Enables std::span versions of functions that usually take a pointer + length combo

Node Builder

Enabled via -DPROTON_NODE_BUILDER=ON -DPROTON_ENABLE_ALLOC=ON, the Node Builder API allows for runtime generation of a Node and Signal Registry. Additional feature flags are useful for parsing different configuration languages

Dependencies and Feature Flags

  • PROTON_NODE_BUILDER_YAML_PARSER (requires libyaml-cpp-dev to be installed): parse YAML configuration files and to generate the Node
  • PROTON_NODE_BUILDER_JSON_PARSER (requires nlohmann-json3-dev to be installed): parse JSON configuration files and to generate the Node
note

Configuration parser feature flags require PROTON_ENABLE_ALLOC and PROTON_NODE_BUILDER to be enabled.

Using Node Builder

Loading the Configuration

Similar to the registry generator A configuration tree must be created from a configuration file, and must then be filtered down to the target node you care about.

class Config
{
public:
explicit Config() = default;

/**
* @brief Construct from a ConfigTree (format-agnostic)
*/
explicit Config(const ConfigTree & tree);

/**
* @brief Convenience: construct from YAML file path
*/
static Config from_yaml(const std::string & yaml_file);

/**
* @brief Convenience: construct from JSON file path
*/
static Config from_json(const std::string & json_file);

~Config() = default;

std::vector<BundleConfig> bundles;
std::map<std::string, NodeConfig> nodes;
std::vector<ConnectionConfig> connections;
std::vector<SignalConfig> signals;

private:
void parse(const ConfigTree & tree);
};

#include "protoncpp/node_builder/config.hpp"
#include <yaml-cpp/yaml.h>
#include <nlohmann/json.hpp>

void main() {
Config config = Config::from_yaml("test_configs/yaml/test.yaml");

// OR

YAML::Node node = YAML::LoadFile("test_configs/yaml/test.yaml");
std::stringstream ss;
ss << node;

ConfigTree config_tree = ConfigTree::from_yaml_string(ss.str());
Config config_2(config_tree);

// JSON
Config config = Config::from_json("test_configs/json/test.json");

// OR
std::ifstream f("test_configs/json/test.json");
std::stringstream ss;
ss << f.rdbuf();
ConfigTree config_tree = ConfigTree::from_json_string(ss.str());
Config config_2(config_tree);
}

Creating the Node

Creating a proton::node_builder::Config simply creates the entire proton configuration as an object. When building a node and registry from the config, a target name must be applied.

explicit GeneratedNode(const Config & config, const std::string & target_name);

Note that creating a GeneratedNode will also validate the config's integrity, looking for missing or broken fields such as missing ID's, capacities smaller than default values, existence of endpoints specified in connections, etc.

note

It is best to wrap the creation of a GeneratedNode in a try/catch block

void main() {
try {
Config config = Config::from_yaml("test_configs/yaml/test.yaml");
GeneratedNode node (config, "producer");
} catch (const NodeBuilderException & e) {
std::cerr << "something exploded: " << e.what() << std::endl;
}
}

Using the Generated Node

Once the node is generated, it can be used via the standard NodeAccess accessor functions

  Config config = create_default_values_config();
GeneratedNode node(config, "node_a");

double value = 0.0;
ASSERT_EQ(proton_signal_get_double(node.registry(), SIG_DEFAULT_DOUBLE_ID, &value), PROTON_OK);
EXPECT_DOUBLE_EQ(value, 3.14159);

Adding proton_cpp to Your Project

Very similar to proton_core's include model, proton_cpp can be included via cmake install, FetchContent or ExternalProject, or direct source inclusion.

cmake_minimum_required(VERSION 3.20)
project(cpp_include_example)
find_package(proton REQUIRED)
add_executable(${PROJECT_NAME}$ main.cpp)
target_link_libraries(${PROJECT_NAME}$ proton::proton_cpp)

An example of how to use ExternalProject to include proton_cpp is in proton_vendor, a shim package that is used to expose proton_cpp as a ROS 2 package.

include(ExternalProject)

set(PROTON_EXTERNAL_PROJECT_NAME proton_external_project)

set(EXTERNAL_PROJECT_PREFIX ${CMAKE_CURRENT_BINARY_DIR}/proton_external_build)
set(EXTERNAL_PROJECT_SOURCE_DIR ${EXTERNAL_PROJECT_PREFIX}/src/proton)
set(EXTERNAL_PROJECT_BINARY_DIR ${EXTERNAL_PROJECT_PREFIX}/build/proton)
# Proton library should install its files (libraries, headers) into proton_vendor package install space
set(EXTERNAL_PROJECT_INSTALL_DIR ${CMAKE_INSTALL_PREFIX})

# Use sequential build for ExternalProject to avoid file descriptor issues
set(EXTERNAL_PROJECT_BUILD_COMMAND ${CMAKE_COMMAND} --build <BINARY_DIR> --parallel 1)

ExternalProject_Add(${PROTON_EXTERNAL_PROJECT_NAME}
# download step
GIT_REPOSITORY "https://github.com/clearpathrobotics/proton.git"
GIT_TAG "2.0.0"

# directiories config
PREFIX ${EXTERNAL_PROJECT_PREFIX}
SOURCE_DIR ${EXTERNAL_PROJECT_SOURCE_DIR}
BINARY_DIR ${EXTERNAL_PROJECT_BINARY_DIR}
INSTALL_DIR ${EXTERNAL_PROJECT_INSTALL_DIR}

# after initial download, don't re-fetch source on subsequent CMake runs
UPDATE_COMMAND ""

# configure step
CONFIGURE_COMMAND ${CMAKE_COMMAND} -S <SOURCE_DIR> -B <BINARY_DIR>
-DCMAKE_INSTALL_PREFIX=<INSTALL_DIR>
-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}
-DBUILD_SHARED_LIBS=ON
-DCMAKE_CXX_STANDARD=20
-DCMAKE_CXX_STANDARD_REQUIRED=ON
-DPROTON_BUILD_TESTS=OFF
-DPROTON_ENABLE_ALLOC=ON
-DPROTON_NODE_BUILDER=ON
-DPROTON_NODE_BUILDER_YAML_PARSER=ON
-DPROTON_NODE_BUILDER_JSON_PARSER=ON
-DPROTON_INSTALL=ON

# build step
BUILD_COMMAND ${EXTERNAL_PROJECT_BUILD_COMMAND}

# install step
INSTALL_COMMAND ""

# logging
LOG_CONFIGURE YES
LOG_BUILD YES
LOG_INSTALL YES
)

# ros buildfarm fix: separate ExternalProject install step from the clone/build steps.
# Reference: https://github.com/ros-industrial/ros2_canopen/blob/master/lely_core_libraries/CMakeLists.txt
install(CODE "execute_process(COMMAND ${CMAKE_COMMAND} --install ${EXTERNAL_PROJECT_BINARY_DIR})")

target_include_directories(proton_vendor
INTERFACE
$<INSTALL_INTERFACE:include>
)

# Propagate compile definitions that were used to build proton
# These must match the flags in the ExternalProject CONFIGURE_COMMAND above
target_compile_definitions(proton_vendor
INTERFACE
PROTON_ENABLE_ALLOC=1
PROTON_NODE_BUILDER=1
PROTON_NODE_BUILDER_YAML_PARSER=1
PROTON_NODE_BUILDER_JSON_PARSER=1
)

target_link_libraries(proton_vendor
INTERFACE
${CMAKE_INSTALL_PREFIX}/lib/libproton_core.so
${CMAKE_INSTALL_PREFIX}/lib/libproton_cpp.so
)