Documentation
Docs ROS 2 Integration
ros 2 integration

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).

NodePackagePurpose
buildo_hardware_interfacebuildo_ros2_controlOwns the actuator bus, exposes command/state interfaces to the rest of the stack
controller_managerros2_controlSpawns and manages the controllers below
joint_state_broadcasterros2_controlPublishes /buildo/joint_states from the hardware interface
left_arm_controller / right_arm_controllerros2_controlJointTrajectoryController per arm
hand_controllerbuildo_handDrives the five-fingered hands, exposes grasp presets
base_controllerros2_controlDifferential-drive controller for the wheeled base
move_groupmoveit_ros_move_groupMotion planning for left_arm, right_arm, and dual_arm
Nav2 stacknav2_bringupLocalization and navigation for the base
vision_nodebuildo_perceptionPublishes the depth/color streams from the head camera
diagnostics_aggregatorbuildo_diagnosticsRolls 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
FramePublished byNotes
map → odomamcl or slam_toolboxOnly present once Nav2 localization is running; absent on a bare arm bringup
odom → base_linkbase_controllerWheel-encoder odometry; drifts over distance, corrected above by map
base_link → everything elserobot_state_publisherDriven by /buildo/joint_states against the URDF in buildo_description
left_hand_tcp / right_hand_tcprobot_state_publisherThe frame MoveIt plans to and the Grasp action closes its force loop against
head_camera_optical_framerobot_state_publisherOptical 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]
Note

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:

GroupJointsTypical use
left_arm / right_armOne arm, wrist includedSingle-arm pick, point, push
dual_armBoth arms togetherBimanual tasks — lifting, holding while the other hand works
left_hand / right_hand5-finger end effectorGrasp 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.

The wheeled base runs a stock nav2_bringup stack, configured in buildo_nav2_config:

  • Localization: slam_toolbox for building a map on first deployment in a new space, amcl against 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:

BackendBest forNotes
GazeboROS 2 graph-level testing — controllers, Nav2, MoveIt against the real node graphros2 launch buildo_bringup buildo.launch.py sim:=gazebo
Isaac SimLarge-scale reinforcement learning and policy trainingbuildo_isaac_sim package
MuJoCoFast, accurate contact dynamics for manipulation researchbuildo_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.