2026, Jan 05 01:00

Fix micro-ROS ESP32 subscriber error loops: avoid RCCHECK on bool-returning message initializers

Troubleshooting micro-ROS on ESP32: fix subscriber error loops from RCCHECK vs bool/rcl_ret_t initializers. Use proper checks to avoid startup failures.

micro-ROS on ESP32 can look deceptively simple when you start with a single publisher. The trouble may begin the moment you add a subscriber and the board falls into an error loop right as the micro-ROS agent starts. If your setup mirrors a serial link over USB at 921600 baud with a Python ROS 2 node on the PC side, and publisher-only runs fine, read on: the failure can be caused by a subtle but deterministic type mismatch in how initialization is checked.

Problem summary

Publisher-only works. As soon as a subscriber is added on the ESP32, the board immediately enters the error loop triggered by a failing check macro. The launch on the host brings up micro_ros_agent and a ROS 2 node that subscribes to the encoder and publishes back a velocity command. The error appears the moment the agent is launched.

Code sample that reproduces the issue

The essential pattern below shows the problem: wrapping a message initializer that returns a bool with a macro that expects an rcl_ret_t.

#include <micro_ros_arduino.h>
#include <rcl/rcl.h>
#include <rclc/rclc.h>
#include <rclc/executor.h>
#include <std_msgs/msg/int32.h>
#include <std_msgs/msg/float64.h>
// check helpers
#define RCCHECK(fn)                                   \
  {                                                   \
    rcl_ret_t rc_ = fn;                               \
    if (rc_ != RCL_RET_OK) {                          \
      error_loop();                                   \
    }                                                 \
  }
#define RCSOFTCHECK(fn) { (void)(fn); }
// minimal placeholders
void error_loop() {
  while (1) { /* toggle LED etc. */ }
}
rcl_allocator_t alloc_ctx;
rclc_support_t sup_ctx;
rcl_node_t core_node;
rcl_publisher_t enc_pub_hdl;
rcl_subscription_t vel_sub_hdl;
std_msgs__msg__Int32 enc_msg_obj;
std_msgs__msg__Float64 vel_in_msg_obj;
void app_init() {
  alloc_ctx = rcl_get_default_allocator();
  RCCHECK(rclc_support_init(&sup_ctx, 0, NULL, &alloc_ctx));
  RCCHECK(rclc_node_init_default(&core_node, "esp_core", "", &sup_ctx));
  RCCHECK(
    rclc_publisher_init_default(
      &enc_pub_hdl,
      &core_node,
      ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Int32),
      "axis_1_encoder"));
  // Problem: this returns bool, but it's wrapped by RCCHECK expecting rcl_ret_t
  RCCHECK(std_msgs__msg__Float64__init(&vel_in_msg_obj));
  RCCHECK(
    rclc_subscription_init_default(
      &vel_sub_hdl,
      &core_node,
      ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Float64),
      "motor_velocity_1"));
}

What actually goes wrong

The boolean initializer is the culprit. The line std_msgs__msg__Float64__init(&sub_msg) returns a bool. The macro RCCHECK expects a function returning rcl_ret_t and considers RCL_RET_OK (which equals 0) as success. When you pass a function that returns true instead, the comparison true != RCL_RET_OK evaluates as a failure every time, so the error handler is triggered immediately. This is why adding the subscriber caused the ESP32 to jump into the error loop as soon as the agent came up. The same pitfall applies to other message initializers of this type.

The fix

Do not wrap bool-returning initializers with RCCHECK. Only use RCCHECK with functions that return rcl_ret_t. For the message initializer, invoke it directly. The rest of your initialization—node, publisher, subscriber, executor—can continue to be guarded by RCCHECK as they return rcl_ret_t.

#include <micro_ros_arduino.h>
#include <rcl/rcl.h>
#include <rclc/rclc.h>
#include <rclc/executor.h>
#include <std_msgs/msg/int32.h>
#include <std_msgs/msg/float64.h>
#define RCCHECK(fn)                                   \
  {                                                   \
    rcl_ret_t rc_ = fn;                               \
    if (rc_ != RCL_RET_OK) {                          \
      error_loop();                                   \
    }                                                 \
  }
#define RCSOFTCHECK(fn) { (void)(fn); }
void error_loop() {
  while (1) { /* toggle LED etc. */ }
}
rcl_allocator_t alloc_ctx;
rclc_support_t sup_ctx;
rcl_node_t core_node;
rcl_publisher_t enc_pub_hdl;
rcl_subscription_t vel_sub_hdl;
std_msgs__msg__Int32 enc_msg_obj;
std_msgs__msg__Float64 vel_in_msg_obj;
void app_init() {
  alloc_ctx = rcl_get_default_allocator();
  RCCHECK(rclc_support_init(&sup_ctx, 0, NULL, &alloc_ctx));
  RCCHECK(rclc_node_init_default(&core_node, "esp_core", "", &sup_ctx));
  RCCHECK(
    rclc_publisher_init_default(
      &enc_pub_hdl,
      &core_node,
      ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Int32),
      "axis_1_encoder"));
  // Correct: call the bool-returning initializer directly, not via RCCHECK
  std_msgs__msg__Float64__init(&vel_in_msg_obj);
  RCCHECK(
    rclc_subscription_init_default(
      &vel_sub_hdl,
      &core_node,
      ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Float64),
      "motor_velocity_1"));
}

Why this matters

In mixed APIs like micro-ROS on embedded targets, it’s easy to assume every initializer or builder follows the same error model. They don’t. Message initializers commonly return bool, while rcl and rclc initialization routines return rcl_ret_t. Using one-size-fits-all checks leads to startup failures that look like transport or timing issues but are actually type mismatches at compile-time boundaries. The same pattern will bite again if you apply RCCHECK to other message type initializers.

Takeaways

If you see an immediate error loop when adding a subscriber in a micro-ROS + ESP32 setup, check how you guard your initializers. Only wrap rcl/rclc calls that return rcl_ret_t with RCCHECK. Call bool-returning message initializers directly. Keeping those paths distinct prevents false negatives that abort the application at launch, and lets you focus on actual runtime interactions between your publisher, subscriber, and the micro-ROS agent.