• Specification
    • 1. Introduction
    • 2. Basic concepts
    • 3. Data structure description language
    • 4.1 CAN bus transport layer
    • 4.2 UDP bus transport layer
    • 4.3 MAVLink bus transport layer
    • 4.4 CANFD bus transport layer
    • 5. Application level conventions
    • 6. Application level functions
    • 7. List of standard data types
    • 8. Hardware design recommendations
    • dsdl
      • ardupilot
      • uavcan
  • 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
    • Pydronecan
      • Examples
        • Automated ESC enumeration
        • Dump All Messages
        • ESC throttle control
      • Tutorials
        • 1. Setup
        • 2. Basic usage
        • 3. Advanced usage
        • 4. Parsing DSDL definitions
    • AP Periph
    • Libcanard
    • dronecan dsdlc
  • GUI Tool
    • Overview
    • Examples
    • User guide
  • Examples
    • Simple sensor node
  • Discussions
Implementations /  Libuavcan /  Tutorials /  4. Services

Services

This tutorial presents two applications:

  • Server - provides a service of type uavcan.protocol.file.BeginFirmwareUpdate.
  • Client - calls the same service type on a specified node. Server’s node ID is provided to the application as a command-line argument.

Server

#include <cstdio>
#include <iostream>
#include <unistd.h>
#include <uavcan/uavcan.hpp>

/*
 * This example uses the service type uavcan.protocol.file.BeginFirmwareUpdate.
 */
#include <uavcan/protocol/file/BeginFirmwareUpdate.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.server");

    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));
    }

    /*
     * Starting the server.
     * This server doesn't do anything useful; it just prints the received request and returns some meaningless
     * response.
     *
     * The service callback accepts two arguments:
     *  - a reference to a request structure (input)
     *  - a reference to a default-initialized response structure (output)
     * The type of the input can be either of these two:
     *  - T::Request&
     *  - uavcan::ReceivedDataStructure<T::Request>&
     * The type of the output is strictly T::Response&.
     * Note that for the service data structure, it is not possible to instantiate T itself, nor does it make any
     * sense.
     *
     * 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<>).
     */
    using uavcan::protocol::file::BeginFirmwareUpdate;
    uavcan::ServiceServer<BeginFirmwareUpdate> srv(node);

    const int srv_start_res = srv.start(
        [&](const uavcan::ReceivedDataStructure<BeginFirmwareUpdate::Request>& req, BeginFirmwareUpdate::Response& rsp)
        {
            std::cout << req << std::endl;
            rsp.error = rsp.ERROR_UNKNOWN;
            rsp.optional_error_message = "Our sun is dying";
        });
    /*
     * 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 (srv_start_res < 0)
    {
        std::exit(1);                   // TODO proper error handling
    }

    /*
     * Node loop.
     */
    node.setModeOperational();
    while (true)
    {
        const int res = node.spin(uavcan::MonotonicDuration::fromMSec(1000));
        if (res < 0)
        {
            std::printf("Transient failure: %d\n", res);
        }
    }
}

Client

#include <iostream>
#include <unistd.h>
#include <uavcan/uavcan.hpp>

/*
 * This example uses the service type uavcan.protocol.file.BeginFirmwareUpdate.
 */
#include <uavcan/protocol/file/BeginFirmwareUpdate.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 < 3)
    {
        std::cerr << "Usage: " << argv[0] << " <node-id> <server-node-id>" << std::endl;
        return 1;
    }

    const uavcan::NodeID self_node_id = std::stoi(argv[1]);
    const uavcan::NodeID server_node_id = std::stoi(argv[2]);

    auto& node = getNode();
    node.setNodeID(self_node_id);
    node.setName("org.uavcan.tutorial.client");

    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));
    }

    /*
     * Initializing the client. Remember that client objects are noncopyable.
     * Note that calling client.init() is not necessary - the object can be initialized ad hoc during the first call.
     */
    using uavcan::protocol::file::BeginFirmwareUpdate;
    uavcan::ServiceClient<BeginFirmwareUpdate> client(node);

    const int client_init_res = client.init();
    if (client_init_res < 0)
    {
        throw std::runtime_error("Failed to init the client; error: " + std::to_string(client_init_res));
    }

    /*
     * Setting the callback.
     * This callback will be executed when the call is completed.
     * Note that the callback will ALWAYS be called even if the service call has timed out; this guarantee
     * allows to simplify error handling in the application.
     */
    client.setCallback([](const uavcan::ServiceCallResult<BeginFirmwareUpdate>& call_result)
        {
            if (call_result.isSuccessful())  // Whether the call was successful, i.e. whether the response was received
            {
                // The result can be directly streamed; the output will be formatted in human-readable YAML.
                std::cout << call_result << std::endl;
            }
            else
            {
                std::cerr << "Service call to node "
                          << static_cast<int>(call_result.getCallID().server_node_id.get())
                          << " has failed" << std::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.
     */

    /*
     * Service call timeout can be overridden if needed, though it's not recommended.
     */
    client.setRequestTimeout(uavcan::MonotonicDuration::fromMSec(200));

    /*
     * It is possible to adjust priority of the outgoing service request transfers.
     * According to the specification, the services are supposed to use the same priority for response transfers.
     * Default priority is medium, which is 16.
     */
    client.setPriority(uavcan::TransferPriority::OneHigherThanLowest);

    /*
     * Calling the remote service.
     * Generated service data types have two nested types:
     *   T::Request  - request data structure
     *   T::Response - response data structure
     * For the service data structure, it is not possible to instantiate T itself, nor does it make any sense.
     */
    BeginFirmwareUpdate::Request request;
    request.image_file_remote_path.path = "/foo/bar";

    /*
     * It is possible to perform multiple concurrent calls using the same client object.
     * The class ServiceClient provides the following methods that allow to control execution of each call:
     *
     *  int call(NodeID server_node_id, const RequestType& request)
     *      Initiate a new non-blocking call.
     *
     *  int call(NodeID server_node_id, const RequestType& request, ServiceCallID& out_call_id)
     *      Initiate a new non-blocking call and return its ServiceCallID descriptor by reference.
     *      The descriptor allows to query the progress of the call or cancel it later.
     *
     *  void cancelCall(ServiceCallID call_id)
     *      Cancel a specific call using its descriptor.
     *
     *  void cancelAllCalls()
     *      Cancel all calls.
     *
     *  bool hasPendingCallToServer(NodeID server_node_id) const
     *      Whether the client object has pending calls to the given server at the moment.
     *
     *  unsigned getNumPendingCalls() const
     *      Returns the total number of pending calls at the moment.
     *
     *  bool hasPendingCalls() const
     *      Whether the client object has any pending calls at the moment.
     */
    const int call_res = client.call(server_node_id, request);
    if (call_res < 0)
    {
        throw std::runtime_error("Unable to perform service call: " + std::to_string(call_res));
    }

    /*
     * Spin until the call is completed, then exit.
     */
    node.setModeOperational();
    while (client.hasPendingCalls())  // Whether the call has completed (doesn't matter successfully or not)
    {
        const int res = node.spin(uavcan::MonotonicDuration::fromMSec(10));
        if (res < 0)
        {
            std::cerr << "Transient failure: " << res << std::endl;
        }
    }

    return 0;
}

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 <unistd.h>
#include <uavcan/uavcan.hpp>
#include <uavcan/protocol/file/BeginFirmwareUpdate.hpp>

extern uavcan::ICanDriver& getCanDriver();
extern uavcan::ISystemClock& getSystemClock();

using uavcan::protocol::file::BeginFirmwareUpdate;

/**
 * This class demonstrates how to use uavcan::MethodBinder with service clients 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_;

    /*
     * Instantiation of MethodBinder
     */
    typedef uavcan::MethodBinder<Node*, void (Node::*)(const uavcan::ServiceCallResult<BeginFirmwareUpdate>&) const>
        BeginFirmwareUpdateCallbackBinder;

    void beginFirmwareUpdateCallback(const uavcan::ServiceCallResult<BeginFirmwareUpdate>& res) const
    {
        if (res.isSuccessful())
        {
            std::cout << res << std::endl;
        }
        else
        {
            std::cerr << "Service call to node "
                      << static_cast<int>(res.getCallID().server_node_id.get())
                      << " has failed" << std::endl;
        }
    }

public:
    Node(uavcan::NodeID self_node_id, const std::string& self_node_name) :
        node_(getCanDriver(), getSystemClock())
    {
        node_.setNodeID(self_node_id);
        node_.setName(self_node_name.c_str());
    }

    void start()
    {
        const int start_res = node_.start();
        if (start_res < 0)
        {
            throw std::runtime_error("Failed to start the node: " + std::to_string(start_res));
        }
    }

    void execute(uavcan::NodeID server_node_id)
    {
        /*
         * Initializing the request structure
         */
        BeginFirmwareUpdate::Request request;
        request.image_file_remote_path.path = "/foo/bar";

        /*
         * Initializing the client object
         */
        uavcan::ServiceClient<BeginFirmwareUpdate, BeginFirmwareUpdateCallbackBinder> client(node_);

        client.setCallback(BeginFirmwareUpdateCallbackBinder(this, &Node::beginFirmwareUpdateCallback));

        /*
         * Calling
         */
        const int call_res = client.call(server_node_id, request);
        if (call_res < 0)
        {
            throw std::runtime_error("Unable to perform service call: " + std::to_string(call_res));
        }

        /*
         * Spinning the node until the call is completed
         */
        node_.setModeOperational();
        while (client.hasPendingCalls())  // Whether the call has completed (doesn't matter successfully or not)
        {
            const int res = node_.spin(uavcan::MonotonicDuration::fromMSec(10));
            if (res < 0)
            {
                std::cerr << "Transient failure: " << res << std::endl;
            }
        }
    }
};

int main(int argc, const char** argv)
{
    if (argc < 3)
    {
        std::cerr << "Usage: " << argv[0] << " <node-id> <server-node-id>" << std::endl;
        return 1;
    }

    const uavcan::NodeID self_node_id = std::stoi(argv[1]);
    const uavcan::NodeID server_node_id = std::stoi(argv[2]);

    Node node(self_node_id, "org.uavcan.tutorial.clientcpp03");

    node.start();

    node.execute(server_node_id);

    return 0;
}

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")

# Make sure to provide correct path to 'platform_linux.cpp'! See earlier tutorials for more info.
add_executable(server server.cpp ${CMAKE_SOURCE_DIR}/../2._Node_initialization_and_startup/platform_linux.cpp)
target_link_libraries(server ${UAVCAN_LIB} rt)

add_executable(client client.cpp ${CMAKE_SOURCE_DIR}/../2._Node_initialization_and_startup/platform_linux.cpp)
target_link_libraries(client ${UAVCAN_LIB} rt)

add_executable(client_cpp03 client_cpp03.cpp ${CMAKE_SOURCE_DIR}/../2._Node_initialization_and_startup/platform_linux.cpp)
target_link_libraries(client_cpp03 ${UAVCAN_LIB} rt)


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.
Much of the content of this site is based upon prior work by Pavel Kirienko and the UAVCAN Development Team.
  • https://dronecan.org/discord
  • https://github.com/DroneCAN
  • Report a problem with this website

Generated Sun, 02 Jun 2024 21:47:21 +0000 © DroneCAN development team