Dynamic node ID allocation
This advanced-level tutorial demonstrates how to implement dynamic node ID allocation using libuavcan. The reader must be familiar with the corresponding section of the specification, since the content of this chapter heavily relies on principles and concepts introduced there.
In this tutorial, three applications will be implemented:
- Allocatee - a generic application that requests a dynamic node ID. It does not implement any specific application-level logic.
- Centralized allocator - one of two possible types of allocators.
- Distributed allocator - the other type of allocators, that can operate in a highly reliable redundant cluster. Note that the Linux platform driver contains an implementation of a distributed dynamic node ID allocator - learn more on the chapter dedicated to the Linux platform driver.
Allocatee
Use this example as a guideline when implementing dynamic node ID allocation feature in your application.
#include <iostream>
#include <cstdlib>
#include <array>
#include <unistd.h>
#include <uavcan/uavcan.hpp>
#include <uavcan/protocol/dynamic_node_id_client.hpp>
#if __linux__
/*
* This inclusion is specific to Linux.
* Refer to the function getUniqueID() below to learn why do we need this header.
*/
# include <uavcan_linux/system_utils.hpp> // Pulls uavcan_linux::makeApplicationID()
#endif
extern uavcan::ICanDriver& getCanDriver();
extern uavcan::ISystemClock& getSystemClock();
static const std::string NodeName = "org.uavcan.tutorial.allocatee";
/**
* This function is supposed to obtain the unique ID from the hardware the node is running on.
* The source of the unique ID depends on the platform; the examples provided below are valid for some of the
* popular platforms.
*
* - STM32:
* The unique ID can be read from the Unique Device ID Register.
* Its location varies from family to family; please refer to the user manual to find out the correct address
* for your STM32. Note that the ID is 96-bit long, so it will have to be extended to 128-bit with zeros,
* or with some additional vendor-specific values.
*
* - LPC11C24:
* The 128-bit unique ID can be read using the IAP command ReadUID.
* For example:
* void readUniqueID(std::uint8_t out_uid[UniqueIDSize])
* {
* unsigned aligned_array[5] = {}; // out_uid may be unaligned, so we need to use temp array
* unsigned iap_command = 58;
* reinterpret_cast<void(*)(void*, void*)>(0x1FFF1FF1)(&iap_command, aligned_array);
* std::memcpy(out_uid, &aligned_array[1], 16);
* }
*
* - Linux:
* Vast majority of Linux distributions (possibly all?) provide means of obtaining the unique ID from the hardware.
* Libuavcan provides a class named uavcan_linux::MachineIDReader and a function uavcan_linux::makeApplicationID(),
* which allow to automatically determine the unique ID of the hardware the application is running on.
* Please refer to their definitions to learn more.
* In this example, we're using uavcan_linux::makeApplicationID().
*/
static std::array<std::uint8_t, 16> getUniqueID()
{
#if __linux__
/*
* uavcan_linux::MachineIDReader merely reads the ID of the hardware, whereas the function
* uavcan_linux::makeApplicationID() is more complicated - it takes the following two or three inputs,
* and then hashes them together to produce a unique ID:
* - Machine ID, as obtained from uavcan_linux::MachineIDReader.
* - Name of the node, as a string.
* - Instance ID (OPTIONAL), which allows to distinguish different instances of similarly named nodes
* running on the same Linux machine.
*
* The reason we can't use the machine ID directly is that it wouldn't allow us to run more than one UAVCAN node
* on a given Linux system, as it would lead to multiple nodes having the same unique ID, which is prohibited.
* The function makeApplicationID() works around that problem by means of extending the unique ID with a hash of
* the name of the local node, which allows to run differently named nodes on the same Linux system without
* conflicts.
*
* In situations where it is necessary to run multiple identically named nodes on the same Linux system, the
* function makeApplicationID() can be supplied with the third argument, representing the ID of the instance.
* The instance ID does only have to be unique for the given node name on the given Linux system, so
* avoiding conflicts should be pretty straightforward.
*
* In this example we don't want to run the application in multiple instances,
* therefore the instance ID is not used.
*/
return uavcan_linux::makeApplicationID(uavcan_linux::MachineIDReader().read(), NodeName);
#else
# error "Add support for your platform"
#endif
}
int main(int argc, const char** argv)
{
/*
* The dynamic node ID allocation protocol allows the allocatee to ask the allocator for some particular node ID
* value, if necessary. This feature is optional. By default, if no preference has been declared, the allocator
* will pick any free node ID at its own discretion.
*/
int preferred_node_id = 0;
if (argc > 1)
{
preferred_node_id = std::stoi(argv[1]);
}
else
{
std::cout << "No preference for a node ID value.\n"
<< "To assign a preferred node ID, pass it as a command line argument:\n"
<< "\t" << argv[0] << " <preferred-node-id>" << std::endl;
}
/*
* Configuring the node.
*/
uavcan::Node<16384> node(getCanDriver(), getSystemClock());
node.setName(NodeName.c_str());
const auto unique_id = getUniqueID(); // Reading the unique ID of this node
uavcan::protocol::HardwareVersion hwver;
std::copy(unique_id.begin(), unique_id.end(), hwver.unique_id.begin());
std::cout << hwver << std::endl; // Printing to stdout to show the values
node.setHardwareVersion(hwver); // Copying the value to the node's internals
/*
* Starting the node normally, in passive mode (i.e. without node ID assigned).
*/
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 dynamic node ID allocation client.
* By default, the client will use TransferPriority::OneHigherThanLowest for communications with the allocator;
* this can be overriden through the third argument to the start() method.
*/
uavcan::DynamicNodeIDClient client(node);
int client_start_res = client.start(node.getHardwareVersion().unique_id, // USING THE SAME UNIQUE ID AS ABOVE
preferred_node_id);
if (client_start_res < 0)
{
throw std::runtime_error("Failed to start the dynamic node ID client: " + std::to_string(client_start_res));
}
/*
* Waiting for the client to obtain for us a node ID.
* This may take a few seconds.
*/
std::cout << "Allocation is in progress" << std::flush;
while (!client.isAllocationComplete())
{
const int res = node.spin(uavcan::MonotonicDuration::fromMSec(200)); // Spin duration doesn't matter
if (res < 0)
{
std::cerr << "Transient failure: " << res << std::endl;
}
std::cout << "." << std::flush;
}
std::cout << "\nDynamic node ID "
<< int(client.getAllocatedNodeID().get())
<< " has been allocated by the allocator with node ID "
<< int(client.getAllocatorNodeID().get()) << std::endl;
/*
* When the allocation is done, the client object can be deleted.
* Now we need to assign the newly allocated ID to the node object.
*/
node.setNodeID(client.getAllocatedNodeID());
/*
* Now we can run the node normally.
*/
node.setModeOperational();
while (true)
{
const int spin_res = node.spin(uavcan::MonotonicDuration::getInfinite());
if (spin_res < 0)
{
std::cerr << "Transient failure: " << spin_res << std::endl;
}
}
}
Possible output:
No preference for a node ID value.
To assign a preferred node ID, pass it as a command line argument:
./allocatee <preferred-node-id>
major: 0
minor: 0
unique_id: [68, 192, 139, 99, 94, 5, 244, 188, 76, 138, 148, 82, 94, 146, 130, 178]
certificate_of_authenticity: ""
Allocation is in progress...................................................
Dynamic node ID 125 has been allocated by the allocator with node ID 1
Centralized allocator
#include <iostream>
#include <cstdlib>
#include <array>
#include <unistd.h>
#include <uavcan/uavcan.hpp>
#include <uavcan/protocol/dynamic_node_id_server/centralized.hpp>
/*
* We're using POSIX-dependent classes in this example.
* This means that the example will only work as-is on a POSIX-compliant system (e.g. Linux, NuttX),
* otherwise the said classes will have to be re-implemented.
*/
#include <uavcan_posix/dynamic_node_id_server/file_storage_backend.hpp>
#include <uavcan_posix/dynamic_node_id_server/file_event_tracer.hpp>
#if __linux__
/*
* This inclusion is specific to Linux.
* Refer to the function getUniqueID() below to learn why do we need this header.
*/
# include <uavcan_linux/system_utils.hpp> // Pulls uavcan_linux::makeApplicationID()
#endif
extern uavcan::ICanDriver& getCanDriver();
extern uavcan::ISystemClock& getSystemClock();
static const std::string NodeName = "org.uavcan.tutorial.centralized_allocator";
/**
* Refer to the Allocatee example to learn more about this function.
*/
static std::array<std::uint8_t, 16> getUniqueID(uint8_t instance_id)
{
#if __linux__
return uavcan_linux::makeApplicationID(uavcan_linux::MachineIDReader().read(), NodeName, instance_id);
#else
# error "Add support for your platform"
#endif
}
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]);
/*
* Configuring the node.
*/
uavcan::Node<16384> node(getCanDriver(), getSystemClock());
node.setNodeID(self_node_id);
node.setName(NodeName.c_str());
const auto unique_id = getUniqueID(self_node_id); // Using the node ID as instance ID
uavcan::protocol::HardwareVersion hwver;
std::copy(unique_id.begin(), unique_id.end(), hwver.unique_id.begin());
std::cout << hwver << std::endl; // Printing to stdout to show the values
node.setHardwareVersion(hwver); // Copying the value to the node's internals
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));
}
/*
* Event tracer is used to log events from the allocator class.
*
* Each event contains two attributes: event code and event argument (64-bit signed integer).
*
* If such logging is undesirable, an empty tracer can be implemented through the interface
* uavcan::dynamic_node_id_server::IEventTracer.
*
* The interface also provides a static method getEventName(), which maps event codes to human-readable names.
*
* The tracer used here just logs events to a text file.
*/
uavcan_posix::dynamic_node_id_server::FileEventTracer event_tracer;
const int event_tracer_res = event_tracer.init("uavcan_db_centralized/event.log"); // Using a hard-coded path here.
if (event_tracer_res < 0)
{
throw std::runtime_error("Failed to start the event tracer; error: " + std::to_string(event_tracer_res));
}
/*
* Storage backend implements the interface uavcan::dynamic_node_id_server::IStorageBackend.
* It is used by the allocator to access and modify the persistent key/value storage, where it keeps data.
*
* The implementation used here uses the file system to keep the data, where file names are KEYS, and
* the contents of the files are VALUES. Note that the allocator only uses ASCII alphanumeric keys and values.
*/
uavcan_posix::dynamic_node_id_server::FileStorageBackend storage_backend;
const int storage_res = storage_backend.init("uavcan_db_centralized"); // Using a hard-coded path here.
if (storage_res < 0)
{
throw std::runtime_error("Failed to start the storage backend; error: " + std::to_string(storage_res));
}
/*
* Starting the allocator itself.
* Its constructor accepts references to the node, to the event tracer, and to the storage backend.
*/
uavcan::dynamic_node_id_server::CentralizedServer server(node, storage_backend, event_tracer);
// USING THE SAME UNIQUE ID HERE
const int server_init_res = server.init(node.getHardwareVersion().unique_id);
if (server_init_res < 0)
{
throw std::runtime_error("Failed to start the server; error " + std::to_string(server_init_res));
}
std::cout << "Centralized server started successfully" << std::endl;
/*
* Running the node, and printing some basic status information of the server.
*/
node.setModeOperational();
while (true)
{
const int spin_res = node.spin(uavcan::MonotonicDuration::fromMSec(500));
if (spin_res < 0)
{
std::cerr << "Transient failure: " << spin_res << std::endl;
}
/*
* Printing some basic info.
*/
std::cout << "\x1b[1J" // Clear screen from the current cursor position to the beginning
<< "\x1b[H" // Move cursor to the coordinates 1,1
<< std::flush;
std::cout << "Node ID " << int(node.getNodeID().get()) << "\n"
<< "Node failures " << node.getInternalFailureCount() << "\n"
<< std::flush;
}
}
Distributed allocator
The size of the cluster can be configured via a command line argument. Once a majority of the nodes participating in the cluster are up, the cluster can serve allocation requests.
For example, a three-node cluster can be started as follows (execute each command in a different terminal):
# Terminal 1
$ ./distributed_allocator 1 3
# Terminal 2
$ ./distributed_allocator 2 3
# Terminal 3
$ ./distributed_allocator 3 3
Also, a distributed allocator can work in a non-redundant configuration, in which case it behaves the same way as a centralized allocator:
$ ./distributed_allocator 1 1
The source code is provided below.
#include <iostream>
#include <cstdlib>
#include <array>
#include <unistd.h>
#include <uavcan/uavcan.hpp>
#include <uavcan/protocol/dynamic_node_id_server/distributed.hpp>
/*
* We're using POSIX-dependent classes in this example.
* This means that the example will only work as-is on a POSIX-compliant system (e.g. Linux, NuttX),
* otherwise the said classes will have to be re-implemented.
*/
#include <uavcan_posix/dynamic_node_id_server/file_storage_backend.hpp>
#include <uavcan_posix/dynamic_node_id_server/file_event_tracer.hpp>
#if __linux__
/*
* This inclusion is specific to Linux.
* Refer to the function getUniqueID() below to learn why do we need this header.
*/
# include <uavcan_linux/system_utils.hpp> // Pulls uavcan_linux::makeApplicationID()
#endif
extern uavcan::ICanDriver& getCanDriver();
extern uavcan::ISystemClock& getSystemClock();
static const std::string NodeName = "org.uavcan.tutorial.distributed_allocator";
/**
* Refer to the Allocatee example to learn more about this function.
*/
static std::array<std::uint8_t, 16> getUniqueID(uint8_t instance_id)
{
#if __linux__
return uavcan_linux::makeApplicationID(uavcan_linux::MachineIDReader().read(), NodeName, instance_id);
#else
# error "Add support for your platform"
#endif
}
int main(int argc, const char** argv)
{
if (argc < 3)
{
std::cerr << "Usage: " << argv[0] << " <node-id> <cluster-size>" << std::endl;
return 1;
}
const int self_node_id = std::stoi(argv[1]);
const int cluster_size = std::stoi(argv[2]);
/*
* Configuring the node.
*/
uavcan::Node<16384> node(getCanDriver(), getSystemClock());
node.setNodeID(self_node_id);
node.setName(NodeName.c_str());
const auto unique_id = getUniqueID(self_node_id); // Using the node ID as instance ID
uavcan::protocol::HardwareVersion hwver;
std::copy(unique_id.begin(), unique_id.end(), hwver.unique_id.begin());
std::cout << hwver << std::endl; // Printing to stdout to show the values
node.setHardwareVersion(hwver); // Copying the value to the node's internals
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 event tracer - refer to the Centralized Allocator example for details.
*/
uavcan_posix::dynamic_node_id_server::FileEventTracer event_tracer;
const int event_tracer_res = event_tracer.init("uavcan_db_distributed/event.log"); // Using a hard-coded path here.
if (event_tracer_res < 0)
{
throw std::runtime_error("Failed to start the event tracer; error: " + std::to_string(event_tracer_res));
}
/*
* Initializing the storage backend - refer to the Centralized Allocator example for details.
*/
uavcan_posix::dynamic_node_id_server::FileStorageBackend storage_backend;
const int storage_res = storage_backend.init("uavcan_db_distributed"); // Using a hard-coded path here.
if (storage_res < 0)
{
throw std::runtime_error("Failed to start the storage backend; error: " + std::to_string(storage_res));
}
/*
* Starting the allocator itself.
*/
uavcan::dynamic_node_id_server::DistributedServer server(node, storage_backend, event_tracer);
// USING THE SAME UNIQUE ID HERE
const int server_init_res = server.init(node.getHardwareVersion().unique_id, cluster_size);
if (server_init_res < 0)
{
throw std::runtime_error("Failed to start the server; error " + std::to_string(server_init_res));
}
std::cout << "Distributed server started successfully" << std::endl;
/*
* Running the node, and printing some basic status information of the server.
*/
node.setModeOperational();
while (true)
{
const int spin_res = node.spin(uavcan::MonotonicDuration::fromMSec(500));
if (spin_res < 0)
{
std::cerr << "Transient failure: " << spin_res << std::endl;
}
/*
* Printing some basic info.
* The reader is adviced to refer to the dynamic node ID allocator application provided with the
* Linux platform driver to see how to retrieve more detailed status information from the library.
*/
std::cout << "\x1b[1J" // Clear screen from the current cursor position to the beginning
<< "\x1b[H" // Move cursor to the coordinates 1,1
<< std::flush;
const auto time = node.getMonotonicTime();
const auto raft_state_to_string = [](uavcan::dynamic_node_id_server::distributed::RaftCore::ServerState s)
{
switch (s)
{
case uavcan::dynamic_node_id_server::distributed::RaftCore::ServerStateFollower: return "Follower";
case uavcan::dynamic_node_id_server::distributed::RaftCore::ServerStateCandidate: return "Candidate";
case uavcan::dynamic_node_id_server::distributed::RaftCore::ServerStateLeader: return "Leader";
default: return "BADSTATE";
}
};
static const auto duration_to_string = [](uavcan::MonotonicDuration dur)
{
uavcan::MakeString<16>::Type str; // N.B.: this is faster than std::string, as it doesn't use heap
str.appendFormatted("%.1f", dur.toUSec() / 1e6);
return str;
};
const uavcan::dynamic_node_id_server::distributed::StateReport report(server);
std::cout << "Node ID " << int(node.getNodeID().get()) << "\n"
<< "State " << raft_state_to_string(report.state) << "\n"
<< "Last log index " << int(report.last_log_index) << "\n"
<< "Commit index " << int(report.commit_index) << "\n"
<< "Last log term " << report.last_log_term << "\n"
<< "Current term " << report.current_term << "\n"
<< "Voted for " << int(report.voted_for.get()) << "\n"
<< "Since activity " << duration_to_string(time - report.last_activity_timestamp).c_str() << "\n"
<< "Random timeout " << duration_to_string(report.randomized_timeout).c_str() << "\n"
<< "Unknown nodes " << int(report.num_unknown_nodes) << "\n"
<< "Node failures " << node.getInternalFailureCount() << "\n"
<< std::flush;
}
}
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(allocatee
allocatee.cpp
${CMAKE_SOURCE_DIR}/../2._Node_initialization_and_startup/platform_linux.cpp)
target_link_libraries(allocatee ${UAVCAN_LIB} rt)
add_executable(centralized_allocator
centralized_allocator.cpp
${CMAKE_SOURCE_DIR}/../2._Node_initialization_and_startup/platform_linux.cpp)
target_link_libraries(centralized_allocator ${UAVCAN_LIB} rt)
add_executable(distributed_allocator
distributed_allocator.cpp
${CMAKE_SOURCE_DIR}/../2._Node_initialization_and_startup/platform_linux.cpp)
target_link_libraries(distributed_allocator ${UAVCAN_LIB} rt)