ROS 2 Integration
Buildo exposes its entire manipulation, navigation, and sensing surface through ROS 2 — no proprietary middleware to learn. This page covers bringup, from a first workspace build to a full simulated robot.
Software stack
Supported: ROS 2 Jazzy Jalisco (recommended) on Ubuntu 24.04, or Humble
Hawksbill (LTS) on Ubuntu 22.04. The default RMW is Fast DDS; CycloneDDS is supported and
swappable via RMW_IMPLEMENTATION if you're standardizing on it across a fleet. Everything ships
as ordinary colcon packages — no forked rclcpp/rclpy.
Every Buildo node lives under the /buildo namespace, so multiple units can share
a network without topic collisions (still set a distinct ROS_DOMAIN_ID per unit).
| Node | Package | Purpose |
|---|---|---|
buildo_hardware_interface | buildo_ros2_control | Owns the actuator bus, exposes command/state interfaces to the rest of the stack |
controller_manager | ros2_control | Spawns and manages the controllers below |
joint_state_broadcaster | ros2_control | Publishes /buildo/joint_states from the hardware interface |
left_arm_controller / right_arm_controller | ros2_control | JointTrajectoryController per arm |
hand_controller | buildo_hand | Drives the five-fingered hands, exposes grasp presets |
base_controller | ros2_control | Differential-drive controller for the wheeled base |
move_group | moveit_ros_move_group | Motion planning for left_arm, right_arm, and dual_arm |
| Nav2 stack | nav2_bringup | Localization and navigation for the base |
vision_node | buildo_perception | Publishes the depth/color streams from the head camera |
diagnostics_aggregator | buildo_diagnostics | Rolls up per-actuator health into /buildo/diagnostics |
Robot description
buildo_description holds the URDF/xacro source; ros2 launch buildo_description view.launch.py opens it in RViz on its own, no hardware required.
buildo_description/urdf/
├── buildo.urdf.xacro # top-level: includes everything below
├── base/
│ └── wheeled_base.xacro # chassis, 2 drive wheels, casters, IMU mount
├── arms/
│ └── buildo_arm.xacro # macro, instantiated twice: prefix="left_"/"right_"
├── hands/
│ └── buildo_hand.xacro # 5-finger end effector macro
└── sensors/
└── head_camera.xacro # binocular depth camera mount Joint names follow <side>_<joint> (e.g. left_shoulder_pitch, right_wrist_roll). The base contributes left_wheel_joint / right_wheel_joint (continuous) and a fixed base_to_torso joint. Per-joint bus IDs, gear ratios, and torque limits used by
the ros2_control hardware interface are declared as <ros2_control> tags inside each xacro, not hardcoded in the driver — reconfiguring for a non-standard build
means editing XML, not recompiling.
Coordinate frames
The TF tree follows the standard mobile-manipulator shape: a localization chain down to the base, and a static kinematic chain from the base out to each end effector.
map
└── odom
└── base_link
└── torso_link
├── left_arm_base_link → … → left_wrist_roll_link → left_hand_tcp
├── right_arm_base_link → … → right_wrist_roll_link → right_hand_tcp
└── head_camera_link → head_camera_optical_frame | Frame | Published by | Notes |
|---|---|---|
map → odom | amcl or slam_toolbox | Only present once Nav2 localization is running; absent on a bare arm bringup |
odom → base_link | base_controller | Wheel-encoder odometry; drifts over distance, corrected above by map |
base_link → everything else | robot_state_publisher | Driven by /buildo/joint_states against the URDF in buildo_description |
left_hand_tcp / right_hand_tcp | robot_state_publisher | The frame MoveIt plans to and the Grasp action closes its force loop against |
head_camera_optical_frame | robot_state_publisher | Optical convention (Z forward); depth and color images are both published in this frame |
If you only brought up use_nav2:=false, the map and odom frames simply won't exist — everything from base_link up
is published by robot_state_publisher regardless, so arm planning and TF lookups for
manipulation work the same with or without navigation running.
Actuator control
Every joint — arm and wheel alike — is a quasi-direct-drive actuator on a shared
CAN bus (1 Mbps, extended frame). buildo_ros2_control is a ros2_control SystemInterface plugin, so standard tooling (ros2 control list_hardware_interfaces, controller_manager spawners) works unmodified.
Command interfaces per joint: position, velocity, effort — selected per-controller, not per-robot; the arm controllers run position
mode, the base controller runs velocity mode.
State interfaces per joint: position, velocity, effort, temperature.
# buildo_bringup/config/controllers.yaml (excerpt)
controller_manager:
ros__parameters:
update_rate: 1000 # Hz
left_arm_controller:
ros__parameters:
type: joint_trajectory_controller/JointTrajectoryController
joints: [left_shoulder_pitch, left_shoulder_roll, left_elbow,
left_wrist_pitch, left_wrist_roll, left_wrist_yaw]
command_interfaces: [position]
state_interfaces: [position, velocity, effort]
base_controller:
ros__parameters:
type: diff_drive_controller/DiffDriveController
left_wheel_names: [left_wheel_joint]
right_wheel_names: [right_wheel_joint]
command_interfaces: [velocity] Per-joint torque limits (15 N·m rated) are enforced in the hardware interface itself, below the controller — a runaway trajectory command is clamped before it reaches the actuator, not just caught in software.
Manipulation: MoveIt 2
buildo_moveit_config defines three planning groups:
| Group | Joints | Typical use |
|---|---|---|
left_arm / right_arm | One arm, wrist included | Single-arm pick, point, push |
dual_arm | Both arms together | Bimanual tasks — lifting, holding while the other hand works |
left_hand / right_hand | 5-finger end effector | Grasp pose refinement, in each arm group's end-effector slot |
Planning defaults to OMPL (RRTConnect), with the Pilz industrial motion planner available for
Cartesian point-to-point moves where you want a predictable, non-sampled path. Collision
checking includes a self-collision matrix generated from the URDF plus whatever octomap the
head camera feeds into move_group's perception pipeline.
From code, this is standard MoveIt: moveit_py or MoveGroupInterface in Python/C++ work against the three groups exactly as they would against any other MoveIt-configured
arm. The one Buildo-specific addition is the grasp action described in API & SDK Reference, which closes the loop
between a Cartesian approach and the hand's own force-feedback grasp controller.
Mobile base: Nav2
The wheeled base runs a stock nav2_bringup stack, configured in buildo_nav2_config:
- Localization:
slam_toolboxfor building a map on first deployment in a new space,amclagainst a saved map thereafter. - Costmaps: footprint set to the base's physical envelope, not the full humanoid height — the arms are handled by MoveIt's own collision checking, not the nav costmap.
- Controller:
nav2_regulated_pure_pursuit_controller, tuned for the 1.5 m/s top speed and two-wheel differential drive.
# Manual drive
ros2 run teleop_twist_keyboard teleop_twist_keyboard --ros-args -r cmd_vel:=/buildo/cmd_vel Autonomous navigation goals go through the standard /buildo/navigate_to_pose action — send a nav2_msgs/action/NavigateToPose goal with a target frame and
pose exactly as you would for any Nav2-enabled robot, from the command line, RViz's "Nav2 Goal"
tool, or an action client in your own node.
If you already have Nav2 tooling, RViz panels, or a fleet manager built for a wheeled service
robot, it works against Buildo's base with no changes beyond pointing it at the /buildo namespace.
Simulation
Three simulation backends are supported, each suited to a different stage of development:
| Backend | Best for | Notes |
|---|---|---|
| Gazebo | ROS 2 graph-level testing — controllers, Nav2, MoveIt against the real node graph | ros2 launch buildo_bringup buildo.launch.py sim:=gazebo |
| Isaac Sim | Large-scale reinforcement learning and policy training | buildo_isaac_sim package |
| MuJoCo | Fast, accurate contact dynamics for manipulation research | buildo_mujoco package, lightest-weight option |
All three consume the same URDF/xacro from buildo_description, so a controller or
planning pipeline that works in Gazebo targets the real hardware interface unchanged —
only the ros2_control hardware plugin swaps between simulated and physical, everything
above that layer is identical.