6.1. Getting started micro-ROS

6.1.1. Overview

This tutorial provides step-by-step instructions to use Vulcanexus micro tools to build and execute a micro-ROS publisher/subscriber demo.

6.1.2. Prerequisites

Ensure that the Vulcanexus installation includes Vulcanexus micro (either vulcanexus-humble-desktop, vulcanexus-humble-micro, or vulcanexus-humble-base). Also, remember to source the environment in every terminal in this tutorial.

source /opt/vulcanexus/humble/setup.bash

6.1.3. Create a micro-ROS workspace

A workspace is a directory containing ROS 2 packages. Before using ROS 2, it’s necessary to source your ROS 2 installation workspace in the terminal you plan to work in. This makes ROS 2’s packages available for you to use in that terminal.

source /opt/vulcanexus/humble/setup.bash

# Best practice is to create a new directory for every new workspace.
mkdir -p ~/microros_ws/src
# Another best practice is to put any packages in your workspace into the src directory.
cd ~/microros_ws/src

6.1.3.1. Build micro-ROS library

The first step is to download and build micro-ROS sources into the created workspace. Vulcanexus micro includes the micro_ros_setup tool, which will handle all the needed steps.

Let’s begin using the create_firmware_ws.sh command with host as target:

# Create firmware step.
ros2 run micro_ros_setup create_firmware_ws.sh host

This command will download all needed sources and tools to build micro-ROS as a library. A folder named firmware must be present in your workspace, which will old platform specific tools and source code:

~/microros_ws/src/
├── firmware
│   └── dev_ws
└── src
    ├── eProsima
    │   ├── Micro-CDR
    │   └── Micro-XRCE-DDS-Client
    ├── ros2
    │   ├── common_interfaces
    │   ├── example_interfaces
    │   ├── rcl_interfaces
    │   └── unique_identifier_msgs
    └── uros
        ├── micro-ROS-demos
        ├── micro_ros_msgs
        ├── micro_ros_utilities
        ├── rclc
        ├── rmw_microxrcedds
        └── rosidl_typesupport_microxrcedds

As there is no need for extra configuration steps usually needed for cross-compilation or RTOS configuration, let’s build:

# Build step.
ros2 run micro_ros_setup build_firmware.sh

This completes the micro-ROS library build. Remember sourcing this workspace before building or running a micro-ROS app.

# Source micro-ROS installation.
source install/local_setup.bash

Note

A set of examples apps for Linux is also present and build under the src/uros/micro-ROS-demos/rclc directory.

6.1.3.2. Create a package

Recall that packages should be created in the src directory, not the root of the workspace. So, navigate into microros_ws/src, and run the package creation command:

ros2 pkg create --build-type ament_cmake micro_pubsub
micro_pubsub/
├── CMakeLists.txt
├── package.xml
├── include
│   └── micro_pubsub
└── src

6.1.4. Add micro-ROS apps to the workspace

After the workspace is ready and sourced, micro-ROS applications can be added, build and run.

Note

Certain topics as message memory handling or micro-ROS API details are not covered on this tutorial. Check the micro-ROS User API tutorial for more details.

6.1.4.1. Write the micro-ROS publisher

On this example app, a publisher will send a periodic string message using a configurable timer.

Add the publisher source code on micro_pubsub/src/micro_publisher.c:

// C standard includes.
#include <stdio.h>
#include <unistd.h>

// micro-ROS general includes with general functionality.
#include <rcl/rcl.h>
#include <rcl/error_handling.h>
#include <rclc/rclc.h>
#include <rclc/executor.h>

// Include subscriber message type.
#include <std_msgs/msg/string.h>

// Macros to handle micro-ROS return codes, error handling should be customized for the target system.
#define RCCHECK(fn) { rcl_ret_t temp_rc = fn; if((temp_rc != RCL_RET_OK)){ printf("Failed status on line %d: %d. Aborting.\n", __LINE__, (int) temp_rc); return 1; }}
#define RCSOFTCHECK(fn) { rcl_ret_t temp_rc = fn; if((temp_rc != RCL_RET_OK)){ printf("Failed status on line %d: %d. Continuing.\n", __LINE__, (int) temp_rc); }}

// Set maximum publisher string length.
#define STRING_LEN 200

// The publisher and string message objects are declared as global so they are available on `timer_callback`.
rcl_publisher_t publisher;
std_msgs__msg__String msg;
int counter = 0;

// The ``timer_callback`` function will be in charge of publishing and increment the message data on each timer period.
void timer_callback(rcl_timer_t * timer, int64_t last_call_time)
{
    (void) last_call_time;

    if (timer != NULL)
    {
        // Update string message with new value and size before its published.
        sprintf(msg.data.data, "Hello from micro-ROS #%d", counter++);
        msg.data.size = strlen(msg.data.data);

        RCSOFTCHECK(rcl_publish(&publisher, &msg, NULL));
        printf("Publishing: \"%s\"\n", msg.data.data);
    }
}

int main()
{
    // Get configured allocator.
    rcl_allocator_t allocator = rcl_get_default_allocator();

    // Initialize support object.
    rclc_support_t support;
    RCCHECK(rclc_support_init(&support, 0, NULL, &allocator));

    // Node name and namespace.
    const char * node_name = "minimal_micro_publisher";
    const char * node_namespace = "";

    // Create node with default configuration.
    rcl_node_t node;
    RCCHECK(rclc_node_init_default(&node, node_name, node_namespace, &support));

    // Topic name and type support.
    const char * topic_name = "topic";
    const rosidl_message_type_support_t * type_support =
        ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, String);

    // Create publisher.
    RCCHECK(rclc_publisher_init_default(
        &publisher,
        &node,
        type_support,
        topic_name));

    // Initialize publisher message memory.
    char string_memory[STRING_LEN];
    msg.data.data = &string_memory[0];
    msg.data.size = 0;
    msg.data.capacity = STRING_LEN;

    // Create timer with its callback and its trigger period.
    rcl_timer_t timer;
    const unsigned int timer_period = 500;
    RCCHECK(rclc_timer_init_default(
        &timer,
        &support,
        RCL_MS_TO_NS(timer_period),
        timer_callback));

    // Initialize executor with one handle.
    rclc_executor_t executor;
    const size_t number_of_handles = 1;
    RCCHECK(rclc_executor_init(&executor, &support.context, number_of_handles, &allocator));

    // Add timer callback to the executor.
    RCCHECK(rclc_executor_add_timer(&executor, &timer));

    // Spin forever
    RCSOFTCHECK(rclc_executor_spin(&executor));

    // Free used resources on spin error and exit.
    // This methods will free used memory even if connection with the Agent is lost.
    RCSOFTCHECK(rclc_executor_fini(&executor));
    RCSOFTCHECK(rcl_timer_fini(&timer));
    RCSOFTCHECK(rcl_publisher_fini(&publisher, &node));
    RCSOFTCHECK(rcl_node_fini(&node));
    RCSOFTCHECK(rclc_support_fini(&support));

    return 1;
}

6.1.4.2. Write the micro-ROS subscriber

This subscriber example is very similar to the publisher, but here the message reception will be handle by the subscriber callback if there is enough memory available.

Add the subscriber source code on micro_pubsub/src/micro_subscriber.c:

// C standard includes.
#include <stdio.h>
#include <unistd.h>

// micro-ROS general includes with general functionality.
#include <rcl/rcl.h>
#include <rcl/error_handling.h>
#include <rclc/rclc.h>
#include <rclc/executor.h>

// Include subscriber message type.
#include <std_msgs/msg/string.h>

// Macros to handle micro-ROS return codes, error handling should be customized for the target system.
#define RCCHECK(fn) { rcl_ret_t temp_rc = fn; if((temp_rc != RCL_RET_OK)){ printf("Failed status on line %d: %d. Aborting.\n", __LINE__, (int) temp_rc); return 1; }}
#define RCSOFTCHECK(fn) { rcl_ret_t temp_rc = fn; if((temp_rc != RCL_RET_OK)){ printf("Failed status on line %d: %d. Continuing.\n", __LINE__, (int) temp_rc); }}

// Set maximum received string length.
// Received publications with strings larger than this size will be discarded.
#define STRING_LEN 200

// Callback to handle incoming subscriber messages.
void subscription_callback(const void * msgin)
{
    // Message type shall be casted to expected type from void pointer.
    const std_msgs__msg__String * msg = (const std_msgs__msg__String *)msgin;
    printf("I heard: \"%s\"\n", msg->data.data);
}

int main()
{
    // Get configured allocator.
    rcl_allocator_t allocator = rcl_get_default_allocator();

    // Initialize support object.
    rclc_support_t support;
    RCCHECK(rclc_support_init(&support, 0, NULL, &allocator));

    // Node name and namespace.
    const char * node_name = "minimal_micro_subscriber";
    const char * node_namespace = "";

    // Create node.
    rcl_node_t node;
    RCCHECK(rclc_node_init_default(&node, node_name, node_namespace, &support));

    // Topic name and message type support.
    const char * topic_name = "topic";
    const rosidl_message_type_support_t * type_support =
    ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, String);

    // Create subscriber.
    rcl_subscription_t subscriber;
    RCCHECK(rclc_subscription_init_default(
        &subscriber,
        &node,
        type_support,
        topic_name));

    // Initialize executor.
    rclc_executor_t executor;
    const size_t number_of_handles = 1;
    RCCHECK(rclc_executor_init(&executor, &support.context, number_of_handles, &allocator));

    // Add subscriber callback.
    std_msgs__msg__String msg;
    RCCHECK(rclc_executor_add_subscription(&executor, &subscriber, &msg, &subscription_callback, ON_NEW_DATA));

    // Initialize subscriber message memory.
    char string_memory[STRING_LEN];
    msg.data.data = &string_memory[0];
    msg.data.size = 0;
    msg.data.capacity = STRING_LEN;

    // Spin forever
    RCSOFTCHECK(rclc_executor_spin(&executor));

    // Free used resources on spin error and exit.
    // This methods will free used memory even if connection with the Agent is lost.
    RCSOFTCHECK(rclc_executor_fini(&executor));
    RCSOFTCHECK(rcl_subscription_fini(&subscriber, &node));
    RCSOFTCHECK(rcl_node_fini(&node));
    RCSOFTCHECK(rclc_support_fini(&support));

    return 1;
}

6.1.4.3. Add dependencies

Before building this examples, micro-ROS used headers and tools need to be included on the created ROS2 package:

  1. Open package.xml on microros_ws/src/micro_pubsub directory

  2. make sure to fill in the <description>, <maintainer> and <license> tags:

<description>Examples of minimal publisher/subscriber using rclcpp</description>
<maintainer email="you@email.com">Your Name</maintainer>
<license>Apache License 2.0</license>
  1. Add a new line after the ament_cmake buildtool dependency and paste the following dependencies corresponding to your node’s include statements:

<depend>rcl</depend>
<depend>rclc</depend>
<depend>std_msgs</depend>
<depend>rmw_microxrcedds</depend>
  1. Add the dependencies to the CMakeLists.txt file, the following example can be used:

cmake_minimum_required(VERSION 3.5)
project(micro_pubsub)

find_package(ament_cmake REQUIRED)
find_package(rcl REQUIRED)
find_package(rclc REQUIRED)
find_package(std_msgs REQUIRED)
find_package(rmw_microxrcedds REQUIRED)

add_executable(publisher src/micro_publisher.c)
ament_target_dependencies(publisher
  rcl
  rclc
  std_msgs
  rmw_microxrcedds)

add_executable(subscriber src/micro_subscriber.c)
ament_target_dependencies(subscriber
  rcl
  rclc
  std_msgs
  rmw_microxrcedds)

install(TARGETS
        publisher
        subscriber
        DESTINATION lib/${PROJECT_NAME})

ament_package()

6.1.5. Build

Once the source code is ready, build your new package:

# ~/microros_ws
colcon build --packages-select micro_pubsub

6.1.6. Run

6.1.6.1. Start the Agent

The agent is included on Vulcanexus micro tool set. The default transport configuration for micro-ROS is UDPv4 with the Agent on 127.0.0.1:8888:

# Start the agent
ros2 run micro_ros_agent micro_ros_agent udp4 --port 8888 -v5

This command will start the Agent, allowing communication between our micro-ROS apps and ROS2 environment.

Note

At the top of the apps main function, micro-ROS is initialized using the rclc_support_init method. It is important that the Agent is reachable on this step, as the method will fail if a connection cannot be established.

6.1.6.2. Run the apps

Before running the apps, set Micro XRCE-DDS as RMW implementation for each terminal:

# Set Micro XRCE-DDS as RMW implementation
export RMW_IMPLEMENTATION=rmw_microxrcedds

Run the publisher in one terminal:

# ~/microros_ws
ros2 run micro_pubsub publisher

Then run listener in another terminal:

# ~/microros_ws
ros2 run micro_pubsub subscriber

The listener will start printing messages to the console. Notice how the creation of entities and each publication appears on the Agent log.