ROS2 User Guide

ROS2 Interface

The topic and service paths in the examples below use the default namespace /hand_0/. When launched with wujihand_dual.launch.py, topics and services live under /hand_left/ and /hand_right/ — replace /hand_0/ accordingly.

Interface Reference

HandDiagnostics Message

wujihand_msgs/msg/HandDiagnostics: Hardware diagnostics, published at 10Hz by default (see diagnostics_rate).

std_msgs/Header header
string handedness               # "left" or "right"
float32 system_temperature      # System temperature (°C)
float32 input_voltage           # Input voltage (V)
float32[20] joint_temperatures  # Joint temperatures (°C)
uint32[20] error_codes          # Error codes (0=normal)
bool[20] enabled                # Joint enabled status
float32[20] effort_limits       # Effort limit settings (A)

JointState Message

sensor_msgs/msg/JointState: Joint states, published at 1000Hz by default (see publish_rate).

FieldTypeDescription
positionfloat64[20]Joint positions (radians)
velocityfloat64[20]Not used
effortfloat64[20]Joint effort (amperes)

About the Effort Field

Effort represents the actuator output in current space, filtered before output. It is not the actual measured current value. Consider it as relative drive intensity.

Typical use cases:

  • Collision detection: A sudden increase in effort indicates joint obstruction
  • Load monitoring: Calculate current output percentage via effort / effort_limit

Default effort_limit is 1.5A, maximum 3.5A.

SetEnabled Service

wujihand_msgs/srv/SetEnabled: Enable or disable joints.

# Request
uint8 finger_id   # 0-4 or 255 (all fingers)
uint8 joint_id    # 0-3 or 255 (all joints)
bool enabled      # true=enable, false=disable
---
# Response
bool success
string message

ResetError Service

wujihand_msgs/srv/ResetError: Reset joint error states.

# Request
uint8 finger_id   # 0-4 or 255 (all fingers)
uint8 joint_id    # 0-3 or 255 (all joints)
---
# Response
bool success
string message

Service Calls

Enable/Disable Joints

# Disable all joints
ros2 service call /hand_0/set_enabled wujihand_msgs/srv/SetEnabled \
  "{finger_id: 255, joint_id: 255, enabled: false}"

# Enable all joints
ros2 service call /hand_0/set_enabled wujihand_msgs/srv/SetEnabled \
  "{finger_id: 255, joint_id: 255, enabled: true}"

# Enable only the index finger (finger_id=1)
ros2 service call /hand_0/set_enabled wujihand_msgs/srv/SetEnabled \
  "{finger_id: 1, joint_id: 255, enabled: true}"

# Enable only the first joint of thumb
ros2 service call /hand_0/set_enabled wujihand_msgs/srv/SetEnabled \
  "{finger_id: 0, joint_id: 0, enabled: true}"

Reset Errors

# Reset all joint errors
ros2 service call /hand_0/reset_error wujihand_msgs/srv/ResetError \
  "{finger_id: 255, joint_id: 255}"

Topic Control

Send Joint Position Commands

Joint positions are sent via the /{hand_name}/joint_commands topic using the sensor_msgs/msg/JointState message type.

Method 1: Position Array (20 joints)

# Zero all joints
ros2 topic pub /hand_0/joint_commands sensor_msgs/msg/JointState \
  "{position: [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0]}" --once

Method 2: Specify Joint Names

# Control only two joints of the index finger
ros2 topic pub /hand_0/joint_commands sensor_msgs/msg/JointState \
  "{name: ['right_finger2_joint2', 'right_finger2_joint3'], position: [1.0, 1.0]}" --once

Smooth Homing

home.launch.py smoothly interpolates from the current pose back to zero, avoiding an abrupt snap:

# Home a single hand
ros2 launch wujihand_bringup home.launch.py

# Home both hands together over 1.5s (names match the topics wujihand_dual.launch.py publishes)
ros2 launch wujihand_bringup home.launch.py \
    hand_names:=hand_left,hand_right duration:=1.5

Read Joint States

# Read joint states once (published at 1000Hz by default)
ros2 topic echo /hand_0/joint_states --once

# Read diagnostics (published at 10Hz by default)
ros2 topic echo /hand_0/hand_diagnostics --once

Code Examples

The following examples demonstrate how to create a ROS2 node to control the dexterous hand:

  • Subscribe to joint state topics for real-time position feedback
  • Publish joint position commands to control hand movement

Python

import rclpy
from rclpy.node import Node
from rclpy.qos import qos_profile_sensor_data
from sensor_msgs.msg import JointState

class WujiHandController(Node):
    """Dexterous hand control node for subscribing to states and publishing commands"""

    def __init__(self, hand_name='hand_0'):
        super().__init__('wujihand_controller')
        self.hand_name = hand_name
        self.count = 0  # Message counter for throttling output

        # Create publisher: send joint position commands to joint_commands topic
        self.cmd_pub = self.create_publisher(
            JointState,
            f'/{hand_name}/joint_commands',
            qos_profile_sensor_data
        )

        # Create subscriber: receive joint states from joint_states topic (1000Hz)
        self.state_sub = self.create_subscription(
            JointState,
            f'/{hand_name}/joint_states',
            self.state_callback,
            qos_profile_sensor_data
        )

    def state_callback(self, msg):
        """
        Joint state callback
        msg.position: Current positions of 20 joints (radians)
        msg.effort: Current effort of 20 joints (amperes)
        """
        self.count += 1
        if self.count % 100 == 0:  # Print every 100 messages (~10Hz)
            # Print the 4 joint positions of the thumb (indices 0-3)
            self.get_logger().info(f'Position: {msg.position[:4]}')

    def send_positions(self, positions):
        """
        Send joint position command
        positions: Array of 20 target positions for joints (radians)
        """
        msg = JointState()
        msg.position = list(positions)
        self.cmd_pub.publish(msg)

def main():
    # Initialize ROS2
    rclpy.init()

    # Create control node
    node = WujiHandController(hand_name='hand_0')

    # Send zero command to all joints (set all 20 joints to 0 radians)
    node.send_positions([0.0] * 20)

    # Enter event loop, continuously receive joint states
    rclpy.spin(node)

    # Cleanup
    rclpy.shutdown()

if __name__ == '__main__':
    main()

How to Run

  1. Save the above code as wujihand_controller.py

  2. Start the driver in terminal 1:

# Load ROS2 environment, replace <distro> with your installed ROS2 distribution
source /opt/ros/<distro>/setup.bash
# Navigate to driver workspace and run the following commands to source it (skip if installed via Deb)
cd <workspace>
source install/setup.bash
ros2 launch wujihand_bringup wujihand.launch.py
  1. Run the example code in terminal 2:
# Load ROS2 environment, replace <distro> with your installed ROS2 distribution
source /opt/ros/<distro>/setup.bash
python3 wujihand_controller.py

Expected Output

The hand moves to zero position. Terminal continuously prints joint positions (~10Hz). Press Ctrl+C to exit:

[INFO] [1769589384.755943411] [wujihand_controller]: Position: array('d', [0.048882766883356236, 0.007923863595842663, 0.03073836071041754, 0.03718776672849719])
[INFO] [1769589384.855927131] [wujihand_controller]: Position: array('d', [0.048882766883356236, 0.007923863595842663, 0.03073836071041754, 0.03718776672849719])
...

C++

#include <rclcpp/rclcpp.hpp>
#include <sensor_msgs/msg/joint_state.hpp>

/**
 * Dexterous hand control node
 * Functions: subscribe to joint states, publish position commands
 */
class WujiHandController : public rclcpp::Node {
public:
    WujiHandController(const std::string& hand_name = "hand_0")
        : Node("wujihand_controller"), hand_name_(hand_name), count_(0)
    {
        // QoS configuration: use SensorDataQoS to match driver
        auto qos = rclcpp::SensorDataQoS();

        // Create publisher: send joint position commands to joint_commands topic
        cmd_pub_ = this->create_publisher<sensor_msgs::msg::JointState>(
            "/" + hand_name_ + "/joint_commands", qos);

        // Create subscriber: receive joint states from joint_states topic (1000Hz)
        state_sub_ = this->create_subscription<sensor_msgs::msg::JointState>(
            "/" + hand_name_ + "/joint_states", qos,
            std::bind(&WujiHandController::state_callback, this, std::placeholders::_1));
    }

    /**
     * Send joint position command
     * @param positions Array of 20 target positions for joints (radians)
     */
    void send_positions(const std::vector<double>& positions) {
        auto msg = sensor_msgs::msg::JointState();
        msg.position = positions;
        cmd_pub_->publish(msg);
    }

private:
    /**
     * Joint state callback
     * msg->position: Current positions of 20 joints (radians)
     * msg->effort: Current effort of 20 joints (amperes)
     */
    void state_callback(const sensor_msgs::msg::JointState::SharedPtr msg) {
        count_++;
        if (count_ % 100 == 0) {  // Print every 100 messages (~10Hz)
            RCLCPP_INFO(this->get_logger(), "Position: [%.3f, %.3f, %.3f, %.3f]",
                msg->position[0], msg->position[1], msg->position[2], msg->position[3]);
        }
    }

    std::string hand_name_;
    int count_;  // Message counter for throttling output
    rclcpp::Publisher<sensor_msgs::msg::JointState>::SharedPtr cmd_pub_;
    rclcpp::Subscription<sensor_msgs::msg::JointState>::SharedPtr state_sub_;
};

int main(int argc, char** argv) {
    // Initialize ROS2
    rclcpp::init(argc, argv);

    // Create control node
    auto node = std::make_shared<WujiHandController>("hand_0");

    // Send zero command to all joints (set all 20 joints to 0 radians)
    node->send_positions(std::vector<double>(20, 0.0));
    RCLCPP_INFO(node->get_logger(), "Sent zero position command");

    // Enter event loop, continuously receive joint states
    rclcpp::spin(node);

    // Cleanup
    rclcpp::shutdown();
    return 0;
}

How to Run

C++ examples require creating a ROS2 package to compile.

  1. Create a test package:
cd ~/test
# Load ROS2 environment, replace <distro> with your installed ROS2 distribution
source /opt/ros/<distro>/setup.bash
ros2 pkg create --build-type ament_cmake test_wujihand --dependencies rclcpp sensor_msgs
  1. Save the above code as ~/test/test_wujihand/src/wujihand_controller.cpp

  2. Add the following to the end of ~/test/test_wujihand/CMakeLists.txt:

add_executable(wujihand_controller src/wujihand_controller.cpp)
ament_target_dependencies(wujihand_controller rclcpp sensor_msgs)
install(TARGETS wujihand_controller DESTINATION lib/${PROJECT_NAME})
  1. Start the driver in terminal 1:
# Load ROS2 environment, replace <distro> with your installed ROS2 distribution
source /opt/ros/<distro>/setup.bash
# Navigate to driver workspace and run the following commands to source it (skip if installed via Deb)
cd <workspace>
source install/setup.bash
ros2 launch wujihand_bringup wujihand.launch.py
  1. Build and run in terminal 2:
# Load ROS2 environment, replace <distro> with your installed ROS2 distribution
source /opt/ros/<distro>/setup.bash
cd ~/test
colcon build --packages-select test_wujihand
source ~/test/install/setup.bash
ros2 run test_wujihand wujihand_controller

Expected Output

The hand moves to zero position. Terminal continuously prints joint positions (~10Hz). Press Ctrl+C to exit:

[INFO] [1769591460.127643591] [wujihand_controller]: Sent zero position command
[INFO] [1769591460.591707131] [wujihand_controller]: Position: [0.043, 0.022, 0.034, 0.035]
[INFO] [1769591460.691719595] [wujihand_controller]: Position: [0.044, 0.012, 0.035, 0.034]
...