This is a modular system for micro-ros under PlatformIO. It allows you to write modules that can interact with ROS2 sending and receiving topics, publishing services, and so on.
For an example of an application that uses micro-rosso, see oruga, a tracked robot. In its platformio.ini file you'll see that it's an ESP32 project that uses Arduino framework, uses ROS2 jazzy, and depends on micro-rosso library. It provides its functionality in modules, such as the mobility system. It also depends on external modules, such as mpu6050 IMU.
To use micro-rosso, you need a working micro-ros environment and a PlatformIO environment.
First, you will need ROS2 installed.
Then you have to install micro-ros. There are two ways: native build (harder) or a docker image (easier).
You can run a micro-ros installation directly from a docker image. For example, when using serial transport to communicate with the microcontroller (change the humble
for jazzy
as needed):
docker run -it --rm --device=/dev/ttyUSB_ESP32 --net=host microros/micro-ros-agent:jazzy serial --dev /dev/ttyUSB_ESP32 -b 115200
When using wifi transport:
docker run -it --rm --net=host microros/micro-ros-agent:humble udp4 --port 2024
You can also build the micro-ros environment yourself.
sudo apt install -y git cmake python3-pip python3-rosdep
mkdir -p ~/microros_ws/src
cd ~/microros_ws
git clone -b $ROS_DISTRO https://github.com/micro-ROS/micro_ros_setup.git src/micro_ros_setup
sudo apt update && rosdep update
rosdep install --from-paths src --ignore-src -y
colcon build
source install/local_setup.bash
ros2 run micro_ros_setup create_agent_ws.sh
ros2 run micro_ros_setup build_agent.sh
source ~/microros_ws/install/local_setup.bash
Once micro-ros is installed, when using serial transport to communicate with the microcontroller you can run it as follows:
ros2 run micro_ros_agent micro_ros_agent serial --dev /dev/ttyUSB0
When using wifi transport:
ros2 run micro_ros_agent micro_ros_agent udp4 --port 2024
You can install PlatformIO inside VSCode following this tutorial.
The files in the micro_rosso library are:
-
include/micro_rosso.h
,src/micro_rosso.cpp
: library used to create and run modules. -
include/micro_rosso_config.h
: internal configuration formicro_rosso
. -
include/logger.h
,src/logger.cpp
: utility module pre-loaded bymicro_rosso
and used to send/rosout
topics. -
include/sync_time.h
,src/sync_time.cpp
: utility module for a service to synchronize the board's clock to the agent. -
include/ros_status.h
,src/ros_status.cpp
: utility module that watches the connection status and can provide output, like lighting a LED when the board is connected to the agent.. -
ticker.include/h
,src/ticker.cpp
: utility module that creates a 1Hz timer and uses it to send to a topic regularly.
You can create an empty project using the IDE by selecting the framework, board, etc. Then you must edit the platformio.ini
file to look something like this:
[env:pico32]
platform = espressif32
board = pico32 ; or whatever esp32 board you have
framework = arduino
board_microros_distro = humble ; or jazzy, etc.
board_microros_transport = serial
lib_deps =
xopxe/micro_rosso
This project is developed on ESP32 boards but can be adapted to other Arduino-compatible boards.
A very minimal main.cpp looks like this:
#include <Arduino.h>
#include "micro_rosso.h"
void setup() {
Serial.begin(115200);
set_microros_serial_transports(Serial);
if (!micro_rosso::setup( "my_node_name" )) {
D_println("FAIL micro_rosso.setup()");
}
}
void loop() {
micro_rosso::loop();
}
The board_microros_transport
field specifies how the board will communicate with the agent. This example uses the Serial transport, which is the first UART or the same USB link used to flash the board. You can change the transport. Each possible transport (serial, Wi-Fi, etc.) must be configured in your code in the setup() method before starting micro_rosso.
We recommend placing your project modules in the /lib/ project folder.
For external modules, add the corresponding entry in lib_deps
.
First, we will show how to use a third-party module; later, we will describe how to create your own.
A module is imported as a standard PlatformIO library in any standard way. As an example, we will import the MPU6050 module from GitHub, adding the following entry to the platformio.ini` file:
lib_deps =
https://github.com/xopxe/micro_rosso_platformio.git
https://github.com/xopxe/micro_rosso_mpu6050.git
/TIP: include from a local folder to reduce rebuilding time/
To start a module, you must include and, if needed, configure it in your main.cpp
. Following mpu6050 example, add the following somewhere near the top and after the #include "micro_rosso.h"
:
#include "micro_rosso_mpu6050.h"
ImuMPU6050 imu;
Then, call the initialization in the setup function:
void setup() {
...
if (!imu.setup()) {
D_println("FAIL imu.setup()");
};
}
Check the setup()
call to see what optional parameter you can pass to it, such as the I2C channel, topic names, etc.
Usually, the modules' setup method returns false
if something fails. D_print
and D_println
are macros that print to a debug console. You can configure the serial debug consoles by passing build flags from your project's platformio.ini
. For example:
build_flags =
-DDEBUG_CONSOLE=Serial1
-DDEBUG_CONSOLE_PIN_RX=10
-DDEBUG_CONSOLE_PIN_TX=9
-DDEBUG_CONSOLE_BAUD=115200
A micro_rosso module is a mostly static object that provides a setup method where it registers ros2 resources using micro_rosso.h
. It then uses micro_ros_platformio and other modules to implement its functionality.
Things a module can do:
You have various options for where to place libraries in your project:
- Both .h and .cpp files in the src/ directory. (quick and dirty)
- The .h in the include/ folder, the .cpp in src/ (good for when you are writing a library you will publish)
- In a lib/my_module/ folder (good for private modules that only make sense for your project.)
The following example is derived from the mobility_tracked
module. It will subscribe to /cmd_vel
topics of type cmd_vel
.
In the my_module.h
file, create the module class:
class MyModule {
static bool setup();
}
In my_module.cpp
, create and register the subscription. We will create a static object to store the messages when they arrive and a subscription object:
static geometry_msgs__msg__Twist msg_twist;
static subscriber_descriptor my_subscription;
Then, define a method to attend to the messages when they arrive:
static void cb(const void* msgin) {
// We can read the message from the global message object
D_println(msg_twist.linear.x);
// or we can retrieve the object from the call, which is
// useful when sharing the callback between topics:
// const geometry_msgs__msg__Twist* msg =
// (const geometry_msgs__msg__Twist*)msgin;
}
Finally, in the setup method, we initialize and register the subscription object:
bool MyModule::setup() {
my_subscription.type_support =
ROSIDL_GET_MSG_TYPE_SUPPORT(geometry_msgs, msg, Twist);
my_subscription.topic_name = "/cmd_vel";
my_subscription.msg = &msg_twist;
my_subscription.callback = &cb;
micro_rosso::subscribers.push_back(&my_subscription_cmd_vel);
return true;
}
The following example is derived from the ticker
module. It will publish to /tick
topics of type int32
.
In my_module.cpp
create and register the publisher. We will create a static object to store the message to be sent and a publisher object:
static std_msgs__msg__Int32 msg_tick;
static publisher_descriptor my_topic;
Then, in the setup method, we initialize and register the publisher object :
msg_tick.data = 0; // also initialize the topic message as needed
my_topic.qos = QOS_DEFAULT; // can also be QOS_BEST_EFFORT
my_topic.type_support =
ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Int32);
my_topic.topic_name = "/tick";
micro_rosso::publishers.push_back(&my_topic);
The topic then can be published from methods, timers, event handlers, etc., as follows:
rcl_publish(&my_topic.publisher, &msg_tick, NULL);
msg_tick.data++;
micro_rosso
provides two timers by default, micro_rosso::timer_control
at 50Hz (20ms period) and micro_rosso::timer_report
at 5Hz (200ms period). To use a timer, create a callback function and add it to the timer's callbacks vector:
void report_cb(int64_t last_call_time) {
D_println("called at 5Hz!");
}
bool setup() {
...
micro_rosso::timer_report.callbacks.push_back(&report_cb);
...
}
The last_call_time
parameter is the time since last time the timer triggered, in nanoseconds.
It is possible to create new timers. For an example, see the ticker
module, which creates and provides a 1Hz timer. To do this, first instantiate a timer descriptor. If you want to make it usable by other modules, you can do it in the class definition.
class MyModule {
static bool setup();
static timer_descriptor my_timer;
}
Create a timer handler function in the .cpp
file. The timer descriptor has a callback vector property, so a basic handler will have to call all the registered callbacks:
static void timer_handler (rcl_timer_t* timer, int64_t last_call_time) {
for (int i = 0; i < Ticker::my_timer.callbacks.size(); i++) {
Ticker::my_timer.callbacks[i](last_call_time);
}
return;
}
Finally, in the setup method, initialize the timer descriptor object and register it.
bool MyModule::setup() {
...
Ticker::timer_tick.timeout_ns = RCL_MS_TO_NS(1000); // 1 sec
Ticker::timer_tick.timer_handler = timer_handler;
micro_rosso::timers.push_back(&Ticker::timer_tick);
...
}
See the example in the sync_time
module to create and announce a service. First, create a service descriptor and the request and response objects:
static service_descriptor my_service;
std_srvs__srv__Trigger_Request req_service;
std_srvs__srv__Trigger_Response res_service;
Then, create the callback function for the service:
static void service_cb(const void* req, void* res) {
// request and response can be cast from the call:
// std_srvs__srv__Trigger_Response* res_in =
// (std_srvs__srv__Trigger_Response*)res;
res_service.success = micro_rosso::time_sync(); // implement the service, return status
}
Finally, configure and register the service descriptor:
my_service.qos = QOS_DEFAULT; // can also be QOS_BEST_EFFORT
my_service.type_support =
ROSIDL_GET_SRV_TYPE_SUPPORT(std_srvs, srv, Trigger);
my_service_sync_time.service_name = "/my_service_name";
my_service.request = &req_service;
my_service.response = &res_service;
my_service.callback = service_cb;
micro_rosso::services.push_back(&my_service);
You can respond to ROS state changes, for example, by disabling and enabling hardware when micro-ros get connected or disconnected. For that, you must register an appropriate listener:
static void ros_state_cb(ros_states state) {
switch (state) {
case WAITING_AGENT: // the client is waiting the agent
break;
case AGENT_AVAILABLE: // agent detected, connecting
break;
case AGENT_CONNECTED: // client connected to agent
break;
case AGENT_DISCONNECTED: // connection to agent broken, disconnecting
break;
default:
break;
}
}
bool MyModule::setup() {
...
micro_rosso::ros_state_listeners.push_back(ros_state_cb);
...
return true;
}
picocom --baud 115200 /dev/ttyUSB1
ros2 topic list
ros2 topic echo "/imu/raw"
ros2 topic pub --once /cmd_vel geometry_msgs/msg/Twist "{linear: {x: 0.1, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 0.0}}"
ros2 service call /sync_time std_srvs/srv/Trigger "{}"
ros2 topic echo /rosout --field msg
jvisca@fing.edu.uy - Grupo MINA, Facultad de Ingeniería - Udelar, 2024
MIT