• Specification
    • 1. Introduction
    • 2. Basic concepts
    • 3. Data structure description language
    • 4. CAN bus transport layer
    • 5. Application level conventions
    • 6. Application level functions
    • 7. List of standard data types
    • 8. Hardware design recommendations
  • Implementations
    • Libuavcan
      • Platforms
      • Tutorials
        • 1. Library build configuration
        • 2. Node initialization and startup
        • 3. Publishers and subscribers
        • 4. Services
        • 5. Timers
        • 6. Time synchronization
        • 7. Remote node reconfiguration
        • 8. Custom data types
        • 9. Node discovery
        • 10. Dynamic node ID allocation
        • 11. Firmware update
        • 12. Multithreading
        • 13. CAN acceptance filters
      • FAQ
    • Pyuavcan
      • Examples
        • Automated ESC enumeration
        • ESC throttle control
      • Tutorials
        • 1. Setup
        • 2. Basic usage
        • 3. Advanced usage
        • 4. Parsing DSDL definitions
    • Libcanard
    • Uavcan.rs
  • GUI Tool
    • Overview
    • Examples
    • User guide
  • Examples
    • Simple sensor node
  • Forum
Implementations /  Libuavcan /  Tutorials /  3. Publishers and subscribers

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 and uavcan.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.

Obsolete

This website is dedicated to the experimental version of the protocol known as UAVCAN v0 that is now obsolete. To learn more about the stable release UAVCAN v1.0, please visit the main website.

Libuavcan

  • Platforms
  • Tutorials
    • 1. Library build configuration
    • 2. Node initialization and startup
    • 3. Publishers and subscribers
    • 4. Services
    • 5. Timers
    • 6. Time synchronization
    • 7. Remote node reconfiguration
    • 8. Custom data types
    • 9. Node discovery
    • 10. Dynamic node ID allocation
    • 11. Firmware update
    • 12. Multithreading
    • 13. CAN acceptance filters
  • FAQ

License

This work is licensed under a Creative Commons Attribution 4.0 International License.
  • Discussion forum
  • GitHub organization
  • Report a problem with this website

Generated Thu, 17 Feb 2022 16:27:38 +0000 © UAVCAN development team