Custom data types
This tutorial covers how to define and use vendor-specific data types (DSDL definitions).
The following topics are explained:
- How to define a vendor-specific data type.
- How to allow the end user to assign/override a data type ID for a certain type.
- How to invoke the DSDL compiler to generate C++ headers for custom data types.
Two applications are implemented in this tutorial:
- Server - the node that provides two vendor-specific services.
- Client - the node that calls the vendor-specific services provided by the server.
This tutorial requires the reader to be familiar with UAVCAN specification and to have completed all the basic tutorials.
Defining data types
Abstract
Suppose we have a vendor named “Sirius Cybernetics Corporation” for whom we need to define the following vendor-specific data types:
sirius_cybernetics_corporation.GetCurrentTime
- returns the current time on the server. The default data type ID for this service is 242.sirius_cybernetics_corporation.PerformLinearLeastSquaresFit
- accepts a set of 2D coordinates and returns the coefficients for the best-fit linear function. This service does not have a default data type ID.
Note that both data types are located in the namespace sirius_cybernetics_corporation
,
which unambiguously indicates that these types are defined by Sirius Cybernetics Corporation and
allows to avoid name clashing if similarly named data types from different vendors are used in the same application.
Note that, according to the naming requirements, name of a DSDL namespace must start with an alphabetic character;
therefore, a company whose name starts with a digit will have to resort to a mangled name
(e.g., “42 Computing” → fourtytwo_computing
, or computing42
, etc.).
Even though this tutorial deals with service types, the same concepts and procedures are also applicable to message types. The reader is encouraged to extend this tutorial with at least one vendor-specific message data type.
The procedure
As explained in the specification, a namespace for DSDL definitions is just a directory,
so we need to create a directory named sirius_cybernetics_corporation
.
This directory can be placed anywhere; normally you’d probably want to put it in your project’s root directory,
as we’ll do in this tutorial.
You should not create it inside the libuavcan’s DSDL directory (libuavcan/dsdl/...
)
in order to not pollute the libuavcan’s source tree with your custom data types.
In the newly created directory, place the following DSDL definition in a file named 242.GetCurrentTime.uavcan
:
#
# This service accepts nothing and returns the current time on the server node.
#
# All DSDL definitions should contain a header comment (like this one) that
# explains what this data type is designed for and how to use it.
#
# This service does not accept any parameters, so the request part is empty
---
# Current time.
# Note that the data type "uavcan.Timestamp" is defined by the UAVCAN specification.
uavcan.Timestamp time
Note that the filename starts with the number 242 followed by a dot, which tells the DSDL compiler that the default data type ID for this type is 242. You can learn more about naming in the relevant part of the specification.
In the same directory, place the following DSDL definition in a file named PerformLinearLeastSquaresFit.uavcan
:
#
# This service accepts a dynamic array of 2D coordinates and returns
# the coefficients for the best-fit linear function.
#
# This service doesn't have a default Data Type ID.
#
PointXY[<64] points
---
float64 slope
float64 y_intercept
Now, observe that the definition above refers to the data type PointXY
.
Let’s define it - place the following in a file named PointXY.uavcan
:
#
# This nested type contains 2D point coordinates.
#
float16 x
float16 y
Compiling
Normally, the compilation should be performed by the build system, which is explained later in this tutorial. For the sake of a demonstration, let’s compile the data types defined above by manually invoking the DSDL compiler.
If the host OS is Linux and libuavcan is installed, the following command can be used:
$ libuavcan_dsdlc ./sirius_cybernetics_corporation -I/usr/local/share/uavcan/dsdl/uavcan
Alternatively, if the library is not installed, use relative paths (the command invocation example below assumes that the libuavcan directory and our vendor-specific namespace directory are both located in the project’s root):
$ libuavcan/libuavcan/dsdl_compiler/libuavcan_dsdlc ./sirius_cybernetics_corporation -Iuavcan/dsdl/uavcan/
Note that if the output directory is not specified explicitly via the command line option --outdir
or -O
,
the default will be used, which is ./dsdlc_generated
.
It is recommended to exclude the output directory from version control,
e.g. for git, add dsdlc_generated
to the .gitignore
config file.
The code
Server
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <uavcan/uavcan.hpp>
#include <uavcan/protocol/debug/LogMessage.hpp> // For purposes of example; not actually necessary.
/*
* The custom data types.
* If a data type has a default Data Type ID, it will be registered automatically once included
* (the registration will be done before main() from a static constructor).
*/
#include <sirius_cybernetics_corporation/GetCurrentTime.hpp>
#include <sirius_cybernetics_corporation/PerformLinearLeastSquaresFit.hpp>
using sirius_cybernetics_corporation::GetCurrentTime;
using sirius_cybernetics_corporation::PerformLinearLeastSquaresFit;
extern uavcan::ICanDriver& getCanDriver();
extern uavcan::ISystemClock& getSystemClock();
constexpr unsigned NodeMemoryPoolSize = 16384;
int main(int argc, const char** argv)
{
if (argc < 2)
{
std::cerr << "Usage: " << argv[0] << " <node-id>" << std::endl;
return 1;
}
const uavcan::NodeID self_node_id = std::stoi(argv[1]);
uavcan::Node<NodeMemoryPoolSize> node(getCanDriver(), getSystemClock());
node.setNodeID(self_node_id);
node.setName("org.uavcan.tutorial.custom_dsdl_server");
/*
* We defined two data types, but only one of them has a default Data Type ID (DTID):
* - sirius_cybernetics_corporation.GetCurrentTime - default DTID 242
* - sirius_cybernetics_corporation.PerformLinearLeastSquaresFit - default DTID is not set
* The first one can be used as is; the second one needs to be registered first.
*/
auto regist_result =
uavcan::GlobalDataTypeRegistry::instance().registerDataType<PerformLinearLeastSquaresFit>(243); // DTID = 243
if (regist_result != uavcan::GlobalDataTypeRegistry::RegistrationResultOk)
{
/*
* Possible reasons for a failure:
* - Data type name or ID is not unique
* - Data Type Registry has been frozen and can't be modified anymore
*/
throw std::runtime_error("Failed to register the data type: " + std::to_string(regist_result));
}
/*
* Now we can use both data types:
* - sirius_cybernetics_corporation.GetCurrentTime - DTID 242
* - sirius_cybernetics_corporation.PerformLinearLeastSquaresFit - DTID 243
*
* But here's more:
* The specification requires that "the end user must be able to change the ID of any non-standard data type".
* So assume that the end user needs to change the default value of 242 to 211. No problem, as long as the
* Data Type Registry is not frozen. Once frozen, the registry can't alter the established data type configuration
* anymore.
*
* Note that non-default Data Type IDs should normally be kept as node configuration params to make them easily
* accessible for the user. Please refer to the relevant tutorial to learn how to make the node configuration
* accessible via UAVCAN.
* Also, this part of the specification is highly relevant - it describes parameter naming conventions for DTID:
* https://uavcan.org/Specification/6._Application_level_functions/#node-configuration
*/
regist_result =
uavcan::GlobalDataTypeRegistry::instance().registerDataType<GetCurrentTime>(211); // DTID: 242 --> 211
if (regist_result != uavcan::GlobalDataTypeRegistry::RegistrationResultOk)
{
throw std::runtime_error("Failed to register the data type: " + std::to_string(regist_result));
}
/*
* The DTID of standard types can be changed also.
*/
regist_result =
uavcan::GlobalDataTypeRegistry::instance().registerDataType<uavcan::protocol::debug::LogMessage>(20999);
if (regist_result != uavcan::GlobalDataTypeRegistry::RegistrationResultOk)
{
throw std::runtime_error("Failed to register the data type: " + std::to_string(regist_result));
}
/*
* The current configuration is as follows:
* - sirius_cybernetics_corporation.GetCurrentTime - DTID 211
* - sirius_cybernetics_corporation.PerformLinearLeastSquaresFit - DTID 243
* - uavcan.protocol.debug.LogMessage - DTID 20999
* Let's check it.
* We can query data type info either by full name or by data type ID using the method find().
* If there's no such type, find() returns nullptr.
*/
auto descriptor = uavcan::GlobalDataTypeRegistry::instance().find("sirius_cybernetics_corporation.GetCurrentTime");
assert(descriptor != nullptr);
assert(descriptor->getID() == uavcan::DataTypeID(211));
assert(descriptor->getID() != GetCurrentTime::DefaultDataTypeID); // There's a T::DefaultDataTypeID if the default DTID is set
descriptor = uavcan::GlobalDataTypeRegistry::instance().find(uavcan::DataTypeKindService, uavcan::DataTypeID(243));
assert(descriptor != nullptr);
assert(std::string(descriptor->getFullName()) == "sirius_cybernetics_corporation.PerformLinearLeastSquaresFit");
descriptor = uavcan::GlobalDataTypeRegistry::instance().find("uavcan.protocol.debug.LogMessage");
assert(descriptor != nullptr);
assert(descriptor->getID() == uavcan::DataTypeID(20999));
/*
* Starting the node as usual.
* The Data Type Registry will be frozen once the node is started, then it can't be unfrozen again.
*/
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));
}
/*
* Warning: you can't change the data type configuration once the node is started because the Data Type Registry
* is frozen now.
*/
assert(uavcan::GlobalDataTypeRegistry::instance().isFrozen()); // It is frozen indeed.
/*
* Don't try this at home.
*/
regist_result =
uavcan::GlobalDataTypeRegistry::instance().registerDataType<GetCurrentTime>(GetCurrentTime::DefaultDataTypeID);
assert(regist_result == uavcan::GlobalDataTypeRegistry::RegistrationResultFrozen); // Will fail.
/*
* Now we can start the services.
* There's nothing unusual at all.
*/
uavcan::ServiceServer<GetCurrentTime> srv_get_current_time(node);
int res = srv_get_current_time.start(
[&](const GetCurrentTime::Request& request, GetCurrentTime::Response& response)
{
(void)request; // It's empty
response.time = node.getUtcTime(); // Note: uavcan::UtcTime implicitly converts to uavcan.Timestamp!
});
if (res < 0)
{
throw std::runtime_error("Failed to start the GetCurrentTime server: " + std::to_string(res));
}
uavcan::ServiceServer<PerformLinearLeastSquaresFit> srv_least_squares(node);
res = srv_least_squares.start(
[](const PerformLinearLeastSquaresFit::Request& request, PerformLinearLeastSquaresFit::Response& response)
{
double sum_x = 0, sum_y = 0, sum_xy = 0, sum_xx = 0;
for (auto point : request.points)
{
sum_x += point.x;
sum_y += point.y;
sum_xy += point.x * point.y;
sum_xx += point.x * point.x;
}
const double a = sum_x * sum_y - request.points.size() * sum_xy;
const double b = sum_x * sum_x - request.points.size() * sum_xx;
if (std::abs(b) > 1e-12)
{
response.slope = a / b;
response.y_intercept = (sum_y - response.slope * sum_x) / request.points.size();
}
});
if (res < 0)
{
throw std::runtime_error("Failed to start the PerformLinearLeastSquaresFit server: " + std::to_string(res));
}
/*
* Running the node as usual.
*/
node.setModeOperational();
while (true)
{
const int res = node.spin(uavcan::MonotonicDuration::getInfinite());
if (res < 0)
{
std::cerr << "Transient failure: " << res << std::endl;
}
}
}
Client
#include <cstdlib>
#include <iostream>
#include <unistd.h>
#include <uavcan/uavcan.hpp>
/*
* The custom data types.
*/
#include <sirius_cybernetics_corporation/GetCurrentTime.hpp>
#include <sirius_cybernetics_corporation/PerformLinearLeastSquaresFit.hpp>
using sirius_cybernetics_corporation::GetCurrentTime;
using sirius_cybernetics_corporation::PerformLinearLeastSquaresFit;
extern uavcan::ICanDriver& getCanDriver();
extern uavcan::ISystemClock& getSystemClock();
constexpr unsigned NodeMemoryPoolSize = 16384;
int main(int argc, const char** argv)
{
if (argc < 3)
{
std::cerr << "Usage: " << argv[0] << " <node-id> <remote-node-id>" << std::endl;
return 1;
}
const uavcan::NodeID self_node_id = std::stoi(argv[1]);
const uavcan::NodeID remote_node_id = std::stoi(argv[2]);
uavcan::Node<NodeMemoryPoolSize> node(getCanDriver(), getSystemClock());
node.setNodeID(self_node_id);
node.setName("org.uavcan.tutorial.custom_dsdl_client");
/*
* Configuring the Data Type IDs.
* See the server sources for details.
*/
auto regist_result =
uavcan::GlobalDataTypeRegistry::instance().registerDataType<PerformLinearLeastSquaresFit>(243);
if (regist_result != uavcan::GlobalDataTypeRegistry::RegistrationResultOk)
{
throw std::runtime_error("Failed to register the data type: " + std::to_string(regist_result));
}
regist_result =
uavcan::GlobalDataTypeRegistry::instance().registerDataType<GetCurrentTime>(211);
if (regist_result != uavcan::GlobalDataTypeRegistry::RegistrationResultOk)
{
throw std::runtime_error("Failed to register the data type: " + std::to_string(regist_result));
}
/*
* Starting the node
*/
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));
}
/*
* Calling both services once; the result will be printed to stdout as YAML.
*/
uavcan::ServiceClient<GetCurrentTime> cln_time(node);
cln_time.setCallback([](const uavcan::ServiceCallResult<GetCurrentTime>& res)
{
std::cout << res << std::endl;
});
int res = cln_time.call(remote_node_id, GetCurrentTime::Request());
if (res < 0)
{
throw std::runtime_error("Failed to call GetCurrentTime: " + std::to_string(res));
}
uavcan::ServiceClient<PerformLinearLeastSquaresFit> cln_least_squares(node);
cln_least_squares.setCallback([](const uavcan::ServiceCallResult<PerformLinearLeastSquaresFit>& res)
{
std::cout << res << std::endl;
});
PerformLinearLeastSquaresFit::Request request;
for (unsigned i = 0; i < 30; i++)
{
sirius_cybernetics_corporation::PointXY p;
p.x = i * 2.5 + 10;
p.y = i;
request.points.push_back(p);
}
res = cln_least_squares.call(remote_node_id, request);
if (res < 0)
{
throw std::runtime_error("Failed to call PerformLinearLeastSquaresFit: " + std::to_string(res));
}
/*
* Spinning the node until both calls are finished.
*/
node.setModeOperational();
while (cln_time.hasPendingCalls() && cln_least_squares.hasPendingCalls())
{
const int res = node.spin(uavcan::MonotonicDuration::fromMSec(10));
if (res < 0)
{
std::cerr << "Transient failure: " << res << std::endl;
}
}
}
Building
This example shows how to build the above applications and compile the vendor-specific data types using CMake. It’s quite easy to adapt it to other platforms and build systems - use the existing projects as a reference.
It is assumed that the libuavcan directory and our vendor-specific namespace directory are both located in the project’s root, although it is not necessary.
CMakeLists.txt
:
cmake_minimum_required(VERSION 2.8)
project(tutorial_project)
find_library(UAVCAN_LIB uavcan REQUIRED)
set(CMAKE_CXX_FLAGS "-Wall -Wextra -std=c++11")
#
# For the next step, we're going to need to know where the standard DSDL definitions are located.
#
# For the sake of simplicity, this example assumes that the OS we're working on is Linux, so
# we can use the default installation directory for DSDL definitions in /usr/local/share/.
#
# If you're using it on an embedded system or if the library isn't installed in the host OS,
# use a relative path to the DSDL subproject.
#
set(UAVCAN_DSDL_DEFINITIONS "/usr/local/share/uavcan/dsdl/uavcan") # For Linux, if the library is installed
# If the library is not installed on the host OS, use relative path to the standard DSDL definitions, for example:
#set(UAVCAN_DSDL_DEFINITIONS "dsdl/uavcan")
#
# Invoke the DSDL compiler to generate headers for our custom data types.
# The default output directory is "dsdlc_generated"; it can be overridden if needed.
# If libuavcan is installed, we can use directly the compiler's executable "libuavcan_dsdlc".
# If the library isn't installed, use a relative path to the compiler in the libuavcan source tree.
#
add_custom_target(dsdlc libuavcan_dsdlc # If the library is installed
"./sirius_cybernetics_corporation" -I${UAVCAN_DSDL_DEFINITIONS}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
# Using relative path to the DSDL compiler if the library is not installed:
#add_custom_target(dsdlc libuavcan/libuavcan/dsdl_compiler/libuavcan_dsdlc
# "./sirius_cybernetics_corporation" -I${UAVCAN_DSDL_DEFINITIONS}
# WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
include_directories("dsdlc_generated") # Adding the default output directory to the include paths
#
# Side note:
# A possible way to invoke the DSDL compiler using plain make is the following:
# include libuavcan/libuavcan/include.mk
# $(info $(shell $(LIBUAVCAN_DSDLC) $(UAVCAN_DSDL_DIR) "./sirius_cybernetics_corporation"))
# Assuming that the library is not installed.
# Don't forget to add "dsdlc_generated" to the include directories.
#
#
# Client and server targets.
# Note that both depend on the dsdlc target.
# 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_dependencies(server dsdlc)
add_executable(client client.cpp ${CMAKE_SOURCE_DIR}/../2._Node_initialization_and_startup/platform_linux.cpp)
target_link_libraries(client ${UAVCAN_LIB} rt)
add_dependencies(client dsdlc)
Running
These instructions assume a Linux environment. First, make sure that the system has at least one CAN interface. You may want to refer to the first tutorials to learn how to add a virtual CAN interface on Linux.
Running the server with node ID 111:
$ ./server 111
The server just sits there waiting for requests, not doing anything on its own. Start another terminal and execute the client with node ID 112:
$ ./client 112 111
The client should print something like this:
# Service call result [sirius_cybernetics_corporation.GetCurrentTime] OK server_node_id=111 tid=0
# Received struct ts_m=25935.913686 ts_utc=1442844327.400572 snid=111
time:
usec: 1442844327400560
# Service call result [sirius_cybernetics_corporation.PerformLinearLeastSquaresFit] OK server_node_id=111 tid=0
# Received struct ts_m=25935.913891 ts_utc=1442844327.400725 snid=111
slope: 0.4
y_intercept: -4
Now, execute the client providing a non-existent server Node ID and see what happens:
$ ./client 112 1
# Service call result [sirius_cybernetics_corporation.GetCurrentTime] FAILURE server_node_id=1 tid=0
# (no data)
# Service call result [sirius_cybernetics_corporation.PerformLinearLeastSquaresFit] FAILURE server_node_id=1 tid=0
# (no data)