-
Notifications
You must be signed in to change notification settings - Fork 0
episode 4 the node
"Five channels. One wristband. One robot." / "Cinco canales. Un brazalete. Un robot."
Commit and release version: Pre-Release: Episode 4 - The node
- 🎥 Watch the Short
- 📖 Episode Description
- Using the ESP32 for the Vambrace
- 💡 Lessons & Learnings
- 🔬 Micro-ROS Under the Hood
- 🎯 Challenge Posed
- ⏭️ Next Episode
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.
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.
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.hppThen 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 8888To find your current IP, run
ip aorifconfigon your machine.
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/ttyUSB0If the device appears as
/dev/ttyUSB1or another port, adjust accordingly.
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 differentTo list available serial ports:
ls /dev/ttyUSB*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 115200Or 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.XThe 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 8888Option B — Docker (no ROS 2 installation required):
docker run -it --rm --net=host microros/micro-ros-agent:jazzy udp4 --port 8888
--net=hostis required so the container can receive UDP packets from the ESP32 on the local network.
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_velThe 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.
- 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 → delayis the entire heartbeat of the vambrace. Everything else hangs off that rhythm. -
Values exist, sensors don't — yet:
TeleoperationCmdholds 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.
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.
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."
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-
allocatoris the budget — defines how memory is acquired and released.rcl_get_default_allocator()gives a safe default. -
supportis the infrastructure — wraps the ROS 2 context. Must be initialized before anything else. -
nodeis the station itself — it gets a name (orion_vambrace_node) and lives on the ROS 2 graph. -
executoris the broadcast scheduler — knows which callbacks are pending and runs them when you callspin_some().
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.
// 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;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.
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.
"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.
➡️ 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