Skip to content

episode 4 the node

DanielFLopez1620 edited this page Apr 8, 2026 · 8 revisions

Episode 4: The Node / El Nodo

"Five channels. One wristband. One robot." / "Cinco canales. Un brazalete. Un robot."

Commit and release version: Pre-Release: Episode 4 - The node

📑 Contents


🎥 Watch the Short

English Version

Vambrace Adventure - Episode 4: The Node (English)

Versión en Español

Vambrace Adventure - Episode 4: El Nodo (Español)



▶️ Watch in English | ▶️ Ver en Español


📖 Episode Description

The team has the components, the topics, and the plan — now it's time to write the first real piece of the vambrace: the node. Danna wonders if that tiny thing — the ESP32 — is really enough to talk to ORION. Felipe confirms it is, and that they'll use the tools from the previous episode to write the code. David asks what the logic behind it all looks like, which sends Felipe on a quick tour through vambrace_app, vambrace_config, and vambrace_ros. Danna then asks the real question: is ORION moving yet? ORION cuts in — he's receiving data, but it's all zeros for now, and he's patiently waiting for Felipe to fix that next episode.


Using the ESP32 for the Vambrace

This section covers everything needed to build, upload, and run the Vambrace firmware on your machine (PC, Laptop, RPi) via micro-ROS over Wi-Fi.


1. Configure Network Credentials

The file lib/vambrace_secret/vambrace_secret.hpp holds your network credentials and is gitignored — you must create it locally before building.

Copy the example template and fill in your values:

cp lib/vambrace_secret/vambrace_secret_ex.hpp lib/vambrace_secret/vambrace_secret.hpp

Then edit vambrace_secret.hpp:

#define WIFI_SSID    "YOUR_NETWORK_NAME"
#define WIFI_PASS    "YOUR_PASSWORD"
#define AGENT_IP_STR "192.168.X.X"   // IP of the machine running the micro-ROS agent
#define AGENT_PORT   8888

To find your current IP, run ip a or ifconfig on your machine.


2. Connect the ESP32 and Grant Port Access

Plug the ESP32 into your computer via USB. Then grant read/write access to the serial port:

ls -al /dev | grep USB
sudo chmod 666 /dev/ttyUSB0

If the device appears as /dev/ttyUSB1 or another port, adjust accordingly.


3. Verify the Upload Port in platformio.ini

Open platformio.ini and confirm the upload_port matches your device:

[env:esp32doit-devkit-v1]
...
upload_port = /dev/ttyUSB0   ; change this if your port is different

To list available serial ports:

ls /dev/ttyUSB*

4. Build and Upload the Firmware

From the project root, build and upload to the ESP32:

# Build only
pio run

# Build and upload
pio run --target upload

# Build, upload, and open serial monitor
pio run --target upload && pio device monitor --baud 115200

Or use the GUI on VS Code for the build and upload.

On a successful upload you should see in the serial monitor:

Vambrace ESP32 started!
Connected. IP: 192.168.X.X

5. Run the micro-ROS Agent

The micro-ROS agent must be running on ORION before powering up the ESP32, so it is ready to receive the connection.

Option A — Native (ROS 2 Jazzy installed on ORION):

ros2 run micro_ros_agent micro_ros_agent udp4 --port 8888

Option B — Docker (no ROS 2 installation required):

docker run -it --rm --net=host microros/micro-ros-agent:jazzy udp4 --port 8888

--net=host is required so the container can receive UDP packets from the ESP32 on the local network.


6. Verify the Connection

Once the agent is running and the ESP32 is powered, verify that topics are being published from a separate terminal on your machine:

# List the nodes and /orion_vambrace_micro_ros should appear
ros2 node list

# List all active topics
ros2 topic list

# Echo velocity commands
ros2 topic echo /mobile_base_controller/cmd_vel

💡 Lessons & Learnings

Key Concept: The Three-Layer Architecture

The vambrace code is split into three deliberate layers, each answering a different question:

Layer File Question it answers
Logic vambrace_app What do we want to tell ORION?
Addressing vambrace_config Where does each message go?
Transport vambrace_ros How does the message actually travel?

This separation matters: vambrace_app knows nothing about ROS. It only knows that a velocity, an arm position, an emotion, and a speech command exist. That means as the project grows — new sensors, new transport methods — the core logic doesn't need to change.

What We Learned

  • Separation of concerns pays off early: Keeping app logic away from ROS plumbing makes testing and iteration much easier, even on embedded systems.
  • The node loop is simple by design: publish → spin → delay is the entire heartbeat of the vambrace. Everything else hangs off that rhythm.
  • Values exist, sensors don't — yet: TeleoperationCmd holds velocity, arm positions, emotion, and speech — but right now all values are zero. The structure is ready; the inputs aren't wired in yet. That's the next episode.

🔬 Micro-ROS Under the Hood

This section is for those who want to go a bit deeper into how micro-ROS works on the ESP32 — no prior ROS embedded experience needed.

From rclcpp to rclc — A Change of Philosophy

If you've written ROS 2 nodes before, you're probably familiar with rclcpp — the C++ client library that handles nodes, publishers, and subscriptions with a clean object-oriented API. On a microcontroller, that luxury disappears. There's no heap allocator, no exceptions, no STL. Enter rclc: the C client library for ROS 2, designed specifically for resource-constrained systems.

rclcpp (full ROS 2) rclc (micro-ROS)
Object-oriented API Procedural C API
Dynamic memory, STL Static buffers, manual allocation
Node, Publisher as classes rcl_node_t, rcl_publisher_t as structs
spin() managed by executor class rclc_executor_spin_some() called manually
Exceptions for error handling Return codes checked manually

The mental model shifts from "I create objects that manage themselves" to "I initialize structs and I am responsible for everything."


The Core Structs — What Each One Does

rcl_node_t node;           // The node itself — its identity on the ROS 2 graph
rclc_support_t support;    // Bootstrap context: ROS clock, init options, context
rcl_allocator_t allocator; // Memory strategy — who allocates and frees memory
rclc_executor_t executor;  // The spin loop manager — processes callbacks when called
  • allocator is the budget — defines how memory is acquired and released. rcl_get_default_allocator() gives a safe default.
  • support is the infrastructure — wraps the ROS 2 context. Must be initialized before anything else.
  • node is the station itself — it gets a name (orion_vambrace_node) and lives on the ROS 2 graph.
  • executor is the broadcast scheduler — knows which callbacks are pending and runs them when you call spin_some().

Initialization Flow

The order of initialization is strict and meaningful:

1. allocator  ←  rcl_get_default_allocator()
2. support    ←  rclc_support_init(&support, 0, nullptr, &allocator)
3. node       ←  rclc_node_init_default(&node, "orion_vambrace_node", "", &support)
4. publishers ←  rclc_publisher_init_default(&pub_*, &node, type_support, topic_name)
5. executor   ←  rclc_executor_init(&executor, &support.context, num_handles, &allocator)

Each step depends on what came before it. You cannot initialize publishers before the node, and you cannot initialize the node before support. This is the dependency chain.


Publishers — Declaration, Init, and Use

// 1. Declare (static, lives for the entire program)
rcl_publisher_t pub_cmd_vel;

// 2. Initialize once in setup
rclc_publisher_init_default(
    &pub_cmd_vel,
    &node,
    ROSIDL_GET_MSG_TYPE_SUPPORT(geometry_msgs, msg, TwistStamped),
    TOPIC_CMD_VEL   // defined in vambrace_config.hpp
);

// 3. Publish every loop
rcl_publish(&pub_cmd_vel, &msg_cmd_vel, nullptr);

ROSIDL_GET_MSG_TYPE_SUPPORT links the publisher to the correct message type at compile time — this is how micro-ROS knows how to serialize data before sending it over WiFi. For array-type messages like Float64MultiArray, the data buffer must be manually pointed to a pre-allocated array — no dynamic allocation, ever:

double left_arm_buffer[1];
msg_left_arm.data.data     = left_arm_buffer;
msg_left_arm.data.size     = 1;
msg_left_arm.data.capacity = 1;

RCCHECK and RCSOFTCHECK — Error Handling Without Exceptions

Since there are no exceptions in C, every micro-ROS function returns an rcl_ret_t code. The vambrace uses two macros:

#define RCCHECK(fn)     { rcl_ret_t rc = fn; if(rc != RCL_RET_OK){ error_loop(); }}
#define RCSOFTCHECK(fn) { rcl_ret_t rc = fn; if(rc != RCL_RET_OK){ /* silent */ }}
Macro Used for On failure
RCCHECK Critical initialization steps Enters error_loop() — halts execution
RCSOFTCHECK Recurring runtime operations (publish, spin) Silently continues

Initialization must succeed — if the node can't be created, there's nothing useful the vambrace can do. But a missed publish is recoverable; the next loop iteration will try again. This asymmetry is a deliberate embedded systems design choice.


The Loop — Heartbeat of the Vambrace

void loop() {
    orion_micro_ros_publish(vambrace_cmds);  // Send all 5 topics
    orion_micro_ros_spin(10);                // Process callbacks (10ms window)
    delay(20);                               // ~33Hz cycle
}

rclc_executor_spin_some() is non-blocking — it processes whatever callbacks are ready within the given timeout and returns immediately. This is critical on embedded systems where a blocking spin() would freeze the program. Right now the executor has zero handles registered (no subscribers yet), but the structure is already in place for when they're added.


🎯 Challenge Posed

"I am receiving data, but it is just zeros or void." — ORION

For now, only the structures matters as this is the base of the rest of the application. Also, this will allow us to focus on the different elements integration without mixing them.


⏭️ Next Episode

➡️ Episode 5: The Joystick — The first real input arrives. The ESP32 reads a joystick, TeleoperationCmd gets its first real values, and ORION will move for the first time.


Characters: Danna, Felipe, David & ORION | Date: 2025-03-18

⬅️ Previous: The Tools | 🏠 Home | Next ➡️: The Joystick

Clone this wiki locally