Publishers and subscribers
This tutorial presents two applications:
- Publisher, which broadcasts messages of type
uavcan.protocol.debug.KeyValue
once a second with random values. - Subscriber, which subscribes to messages of types
uavcan.protocol.debug.KeyValue
anduavcan.protocol.debug.LogMessage
, and prints them to stdout in the YAML format.
Note that the presented publisher will not publish messages of type uavcan.protocol.debug.LogMessage
.
The reader is advised to extend it with this functionality on their own.
Publisher
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <uavcan/uavcan.hpp>
/*
* We're going to use messages of type uavcan.protocol.debug.KeyValue, so the appropriate header must be included.
* Given a data type named X, the header file name would be:
* X.replace('.', '/') + ".hpp"
*/
#include <uavcan/protocol/debug/KeyValue.hpp> // uavcan.protocol.debug.KeyValue
extern uavcan::ICanDriver& getCanDriver();
extern uavcan::ISystemClock& getSystemClock();
constexpr unsigned NodeMemoryPoolSize = 16384;
typedef uavcan::Node<NodeMemoryPoolSize> Node;
static Node& getNode()
{
static Node node(getCanDriver(), getSystemClock());
return node;
}
int main(int argc, const char** argv)
{
if (argc < 2)
{
std::cerr << "Usage: " << argv[0] << " <node-id>" << std::endl;
return 1;
}
const int self_node_id = std::stoi(argv[1]);
auto& node = getNode();
node.setNodeID(self_node_id);
node.setName("org.uavcan.tutorial.publisher");
/*
* Dependent objects (e.g. publishers, subscribers, servers, callers, timers, ...) can be initialized only
* if the node is running. Note that all dependent objects always keep a reference to the node object.
*/
const int node_start_res = node.start();
if (node_start_res < 0)
{
throw std::runtime_error("Failed to start the node; error: " + std::to_string(node_start_res));
}
/*
* Create the publisher object to broadcast standard key-value messages of type uavcan.protocol.debug.KeyValue.
* Keep in mind that most classes defined in the library are not copyable; attempt to copy objects of
* such classes will result in compilation failure.
* A publishing node won't see its own messages.
*/
uavcan::Publisher<uavcan::protocol::debug::KeyValue> kv_pub(node);
const int kv_pub_init_res = kv_pub.init();
if (kv_pub_init_res < 0)
{
throw std::runtime_error("Failed to start the publisher; error: " + std::to_string(kv_pub_init_res));
}
/*
* This would fail because most of the objects - including publishers - are noncopyable.
* The error message may look like this:
* "error: ‘uavcan::Noncopyable::Noncopyable(const uavcan::Noncopyable&)’ is private"
*/
// auto pub_copy = kv_pub; // Don't try this at home.
/*
* TX timeout can be overridden if needed.
* Default value should be OK for most use cases.
*/
kv_pub.setTxTimeout(uavcan::MonotonicDuration::fromMSec(1000));
/*
* Priority of outgoing tranfers can be changed as follows.
* Default priority is 16 (medium).
*/
kv_pub.setPriority(uavcan::TransferPriority::MiddleLower);
/*
* Running the node.
*/
node.setModeOperational();
while (true)
{
/*
* Spinning for 1 second.
* The method spin() may return earlier if an error occurs (e.g. driver failure).
* All error codes are listed in the header uavcan/error.hpp.
*/
const int spin_res = node.spin(uavcan::MonotonicDuration::fromMSec(1000));
if (spin_res < 0)
{
std::cerr << "Transient failure: " << spin_res << std::endl;
}
/*
* Publishing a random value using the publisher created above.
* All message types have zero-initializing default constructors.
* Relevant usage info for every data type is provided in its DSDL definition.
*/
uavcan::protocol::debug::KeyValue kv_msg; // Always zero initialized
kv_msg.value = std::rand() / float(RAND_MAX);
/*
* Arrays in DSDL types are quite extensive in the sense that they can be static,
* or dynamic (no heap needed - all memory is pre-allocated), or they can emulate std::string.
* The last one is called string-like arrays.
* ASCII strings can be directly assigned or appended to string-like arrays.
* For more info, please read the documentation for the class uavcan::Array<>.
*/
kv_msg.key = "a"; // "a"
kv_msg.key += "b"; // "ab"
kv_msg.key += "c"; // "abc"
/*
* Publishing the message.
*/
const int pub_res = kv_pub.broadcast(kv_msg);
if (pub_res < 0)
{
std::cerr << "KV publication failure: " << pub_res << std::endl;
}
}
}
Subscriber
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <uavcan/uavcan.hpp>
#include <uavcan/protocol/debug/KeyValue.hpp>
#include <uavcan/protocol/debug/LogMessage.hpp>
extern uavcan::ICanDriver& getCanDriver();
extern uavcan::ISystemClock& getSystemClock();
constexpr unsigned NodeMemoryPoolSize = 16384;
typedef uavcan::Node<NodeMemoryPoolSize> Node;
static Node& getNode()
{
static Node node(getCanDriver(), getSystemClock());
return node;
}
int main(int argc, const char** argv)
{
if (argc < 2)
{
std::cerr << "Usage: " << argv[0] << " <node-id>" << std::endl;
return 1;
}
const int self_node_id = std::stoi(argv[1]);
auto& node = getNode();
node.setNodeID(self_node_id);
node.setName("org.uavcan.tutorial.subscriber");
/*
* Dependent objects (e.g. publishers, subscribers, servers, callers, timers, ...) can be initialized only
* if the node is running. Note that all dependent objects always keep a reference to the node object.
*/
const int node_start_res = node.start();
if (node_start_res < 0)
{
throw std::runtime_error("Failed to start the node; error: " + std::to_string(node_start_res));
}
/*
* Subscribing to standard log messages of type uavcan.protocol.debug.LogMessage.
*
* Received messages will be passed to the application via a callback, the type of which can be set via the second
* template argument.
* In C++11 mode, callback type defaults to std::function<>.
* In C++03 mode, callback type defaults to a plain function pointer; use a binder object to call member
* functions as callbacks (refer to uavcan::MethodBinder<>).
*
* N.B.: Some libuavcan users report that C++ lambda functions when used with GCC may actually break the code
* on some embedded targets, particularly ARM Cortex M0. These reports still remain unconfirmed though;
* please refer to the UAVCAN mailing list to learn more.
*
* The type of the argument of the callback can be either of these two:
* - T&
* - uavcan::ReceivedDataStructure<T>&
* For the first option, ReceivedDataStructure<T>& will be cast into a T& implicitly.
*
* The class uavcan::ReceivedDataStructure extends the received data structure with extra information obtained from
* the transport layer, such as Source Node ID, timestamps, Transfer ID, index of the redundant interface this
* transfer was picked up from, etc.
*/
uavcan::Subscriber<uavcan::protocol::debug::LogMessage> log_sub(node);
const int log_sub_start_res = log_sub.start(
[&](const uavcan::ReceivedDataStructure<uavcan::protocol::debug::LogMessage>& msg)
{
/*
* The message will be streamed in YAML format.
*/
std::cout << msg << std::endl;
/*
* If the standard iostreams are not available (they rarely available in embedded environments),
* use the helper class uavcan::OStream defined in the header file <uavcan/helpers/ostream.hpp>.
*/
// uavcan::OStream::instance() << msg << uavcan::OStream::endl;
});
/*
* C++03 WARNING
* The code above will not compile in C++03, because it uses a lambda function.
* In order to compile the code in C++03, move the code from the lambda to a standalone static function.
* Use uavcan::MethodBinder<> to invoke member functions.
*/
if (log_sub_start_res < 0)
{
throw std::runtime_error("Failed to start the log subscriber; error: " + std::to_string(log_sub_start_res));
}
/*
* Subscribing to messages of type uavcan.protocol.debug.KeyValue.
* This time we don't want to receive extra information about the received message, so the callback's argument type
* would be just T& instead of uavcan::ReceivedDataStructure<T>&.
* The callback will print the message in YAML format via std::cout (also refer to uavcan::OStream).
*/
uavcan::Subscriber<uavcan::protocol::debug::KeyValue> kv_sub(node);
const int kv_sub_start_res =
kv_sub.start([&](const uavcan::protocol::debug::KeyValue& msg) { std::cout << msg << std::endl; });
if (kv_sub_start_res < 0)
{
throw std::runtime_error("Failed to start the key/value subscriber; error: " + std::to_string(kv_sub_start_res));
}
/*
* Running the node.
*/
node.setModeOperational();
while (true)
{
/*
* The method spin() may return earlier if an error occurs (e.g. driver failure).
* All error codes are listed in the header uavcan/error.hpp.
*/
const int res = node.spin(uavcan::MonotonicDuration::getInfinite());
if (res < 0)
{
std::cerr << "Transient failure: " << res << std::endl;
}
}
}
Compatibility with older C++ standards
The following code demonstrates how to work-around the lack of important features in older C++ standards (prior C++11).
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <uavcan/uavcan.hpp>
#include <uavcan/protocol/debug/KeyValue.hpp>
#include <uavcan/protocol/debug/LogMessage.hpp>
extern uavcan::ICanDriver& getCanDriver();
extern uavcan::ISystemClock& getSystemClock();
/**
* This class demonstrates how to use uavcan::MethodBinder with subscriber objects in C++03.
* In C++11 and newer standards it is recommended to use lambdas and std::function<> instead, as this approach
* would be much easier to implement and to understand.
*/
class Node
{
static const unsigned NodeMemoryPoolSize = 16384;
uavcan::Node<NodeMemoryPoolSize> node_;
/*
* Instantiations of uavcan::MethodBinder<>
*/
typedef uavcan::MethodBinder<Node*, void (Node::*)(const uavcan::protocol::debug::LogMessage&) const>
LogMessageCallbackBinder;
typedef uavcan::MethodBinder<Node*,
void (Node::*)(const uavcan::ReceivedDataStructure<uavcan::protocol::debug::KeyValue>&) const>
KeyValueCallbackBinder;
uavcan::Subscriber<uavcan::protocol::debug::LogMessage, LogMessageCallbackBinder> log_sub_;
uavcan::Subscriber<uavcan::protocol::debug::KeyValue, KeyValueCallbackBinder> kv_sub_;
void logMessageCallback(const uavcan::protocol::debug::LogMessage& msg) const
{
std::cout << "Log message:\n" << msg << std::endl;
}
void keyValueCallback(const uavcan::ReceivedDataStructure<uavcan::protocol::debug::KeyValue>& msg) const
{
std::cout << "KV message:\n" << msg << std::endl;
}
public:
Node(uavcan::NodeID self_node_id, const std::string& self_node_name) :
node_(getCanDriver(), getSystemClock()),
log_sub_(node_),
kv_sub_(node_)
{
node_.setNodeID(self_node_id);
node_.setName(self_node_name.c_str());
}
void run()
{
const int start_res = node_.start();
if (start_res < 0)
{
throw std::runtime_error("Failed to start the node: " + std::to_string(start_res));
}
const int log_sub_start_res = log_sub_.start(LogMessageCallbackBinder(this, &Node::logMessageCallback));
if (log_sub_start_res < 0)
{
throw std::runtime_error("Failed to start the log subscriber; error: " + std::to_string(log_sub_start_res));
}
const int kv_sub_start_res = kv_sub_.start(KeyValueCallbackBinder(this, &Node::keyValueCallback));
if (kv_sub_start_res < 0)
{
throw std::runtime_error("Failed to start the KV subscriber; error: " + std::to_string(kv_sub_start_res));
}
node_.setModeOperational();
while (true)
{
const int res = node_.spin(uavcan::MonotonicDuration::getInfinite());
if (res < 0)
{
std::cerr << "Transient failure: " << res << std::endl;
}
}
}
};
int main(int argc, const char** argv)
{
if (argc < 2)
{
std::cerr << "Usage: " << argv[0] << " <node-id>" << std::endl;
return 1;
}
const int self_node_id = std::stoi(argv[1]);
Node node(self_node_id, "org.uavcan.tutorial.subscriber_cpp03");
node.run();
}
Running on Linux
Build the applications using the following CMake script:
cmake_minimum_required(VERSION 2.8)
project(tutorial_project)
find_library(UAVCAN_LIB uavcan REQUIRED)
set(CMAKE_CXX_FLAGS "-Wall -Wextra -pedantic -std=c++11")
add_executable(publisher publisher.cpp ${CMAKE_SOURCE_DIR}/../2._Node_initialization_and_startup/platform_linux.cpp)
target_link_libraries(publisher ${UAVCAN_LIB} rt)
add_executable(subscriber subscriber.cpp ${CMAKE_SOURCE_DIR}/../2._Node_initialization_and_startup/platform_linux.cpp)
target_link_libraries(subscriber ${UAVCAN_LIB} rt)
add_executable(subscriber_cpp03 subscriber_cpp03.cpp ${CMAKE_SOURCE_DIR}/../2._Node_initialization_and_startup/platform_linux.cpp)
target_link_libraries(subscriber_cpp03 ${UAVCAN_LIB} rt)
This build script assumes that the platform-specific functions are defined in
../2._Node_initialization_and_startup/platform_linux.cpp
.