ROS2 Low-Level Motion Reference Example
The low-level motion development example demonstrates how to reset the robot from any initial joint position to a ready state, and then control ankle joint swinging and finger positions of robot using two different control modes.
The example source code is located in pnd_ros2/example and can be accessed from here.
The source directory is organized by robot model, with corresponding sample code as shown below:
├── adam_lite
│ └── adam_lite_low_level_example.py
├── adam_sp
│ └── adam_sp_low_level_example.py
├── adam_pro
│ └── adam_pro_low_level_example.py
└── adam_u
└── adam_u_low_level_example.py
How to Run
- Make sure the ROS2 msg interfaces are built successfully. Users can define their own
$ROOTdirectory.
3. Run the reference example. The following steps use Adam_Pro as an example.
cd $ROOT/pnd_ros2
# Source ROS2 environment
source /opt/ros/humble/setup.bash
# Source pnd_adam ROS2 message interfaces
source install/setup.bash
# Run the example
python3 example/adam_pro/adam_pro_low_level_example.py
Code Explanation
The following code targets the Adam Pro model. Examples for other models share a similar structure, with differences mainly in joint index definitions.
Core Types and Functional Overview
| Type Name | Description |
|---|---|
| ADAMJointIndex | Joint indices sorted by joint name. Also serves as the index of each joint in the LowCmd.MotorCmd command array |
| LowCmd | ROS2 message structure related to motor commands |
| LowState | ROS2 message structure related to motor states |
| HandCmd | ROS2 message structure related to finger commands |
| DemonController | Core control logic class (ROS2 node) |
The main program of the example is defined in adam_pro_low_level_example.py, where a DemonController ROS2 node is instantiated.
In the constructor of DemonController, two publishers, one subscriber, and a timer are created, along with the corresponding ROS2 message structures:
- Define finger position parameters for a closed hand (
DemonController.close_hand) and open hand (DemonController.open_hand) - Create motor position publisher for the
lowcmdtopic and finger position publisher for thehandcmdtopic - Subscribe to the
lowstatetopic for low-level state feedback, handled byDemonController.getLowState - Periodically call
Custom.LowCmdWriteat the interval defined byDemonController.control_dt_
The corresponding code is shown below:
class DemonController(Node):
def __init__(self):
super().__init__('demon_controller')
self.time_ = 0.0
self.control_dt_ = 0.0025 # [2ms]
self.duration_ = 3.0 # [3 s]
self.counter_ = 0
self.lowcmd_pub_ = self.create_publisher(LowCmd, 'lowcmd', 10)
self.handcmd_pub_ = self.create_publisher(HandCmd, 'handcmd', 10)
self.getstate_flag = False
self.timer = self.create_timer(self.control_dt_, self.LowCmdWrite)
self.mutex = threading.Lock()
self.low_state = LowState()
self.low_state.motor_state = [MotorState() for _ in range(ADAM_PRO_NUM_MOTOR)]
# print("init len(low_state.motor_state)=", len(self.low_state.motor_state))
self.low_cmd = LowCmd()
self.low_cmd.motor_cmd = [MotorCmd() for _ in range(ADAM_PRO_NUM_MOTOR)]
self.hand_cmd = HandCmd()
self.close_hand = np.array([500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500, 500], dtype=int)
self.open_hand = np.array([1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000], dtype=int)
self.lowstate_sub_ = self.create_subscription(
LowState,
"lowstate",
self.getLowState,
10)
Low-Level State Callback Function
When a lowstate message is received, the program automatically calls the CDemonController.getLowState callback function.
In this callback, motor states are extracted. The low-level state structure definition can be found in ROS2 Message Definitions.
This function implements the following logic:
- Record the current position of each joint to support closed-loop control
- Pad missing
MotorStateobjects to avoid out-of-bounds access and crashes - Periodically print IMU Euler angles
def getLowState(self, msg):
# Get the actual number of received motors
received_len = len(msg.motor_state)
if received_len < ADAM_PRO_NUM_MOTOR:
# If received data is insufficient, copy existing data first
self.low_state = msg
# Calculate missing count
missing_count = ADAM_PRO_NUM_MOTOR - received_len
# Pad with default MotorState objects
# This ensures self.low_state.motor_state[i].q defaults to 0.0
self.low_state.motor_state.extend([MotorState() for _ in range(missing_count)])
# Optional: print warning (only once or with limited frequency)
# print(f"Warning: Received only {received_len} motor states. Padding with {missing_count} default states.")
else:
# If sufficient or more, assign directly
self.low_state = msg
self.getstate_flag = True
self.counter_ +=1
if (self.counter_ % 500 == 0) :
self.counter_ = 0
print(self.low_state.imu_state.ypr)
Low-Level Command Sending Function
CDemonController.LowCmdWrite is used to periodically send low-level commands and implements the following stages:
- Stage 1: Reset the Adam robot from any initial joint position to the ready posture (first 3 seconds after startup)
- Stage 2: Control ankle joints using PR control mode to perform continuous swinging for 3 seconds
- Stage 3: Open both hands and keep this state until the program terminates
def LowCmdWrite(self):
if(self.getstate_flag):
self.time_ += self.control_dt_
if self.time_ < self.duration_ :
# [Stage 1]: set robot to zero posture
for i in range(ADAM_PRO_NUM_MOTOR):
ratio = np.clip(self.time_ / self.duration_, 0.0, 1.0)
self.low_cmd.motor_cmd[i].mode = 1 # 1:Enable, 0:Disable
self.low_cmd.motor_cmd[i].tau = 0.
self.low_cmd.motor_cmd[i].q = (1.0 - ratio) * self.low_state.motor_state[i].q
self.low_cmd.motor_cmd[i].dq = 0.
self.low_cmd.motor_cmd[i].kp = Kp[i]
self.low_cmd.motor_cmd[i].kd = Kd[i]
for i in range(12):
self.hand_cmd.position[i] = self.close_hand[i]
elif self.time_ < self.duration_ * 2 :
max_P = np.pi * 30.0 / 180.0
max_R = np.pi * 10.0 / 180.0
t = self.time_ - self.duration_
L_P_des = max_P * np.sin(2.0 * np.pi * t)
L_R_des = max_R * np.sin(2.0 * np.pi * t)
R_P_des = max_P * np.sin(2.0 * np.pi * t)
R_R_des = -max_R * np.sin(2.0 * np.pi * t)
self.low_cmd.motor_cmd[ADAMJointIndex.LeftAnklePitch].q = L_P_des
self.low_cmd.motor_cmd[ADAMJointIndex.LeftAnkleRoll].q = L_R_des
self.low_cmd.motor_cmd[ADAMJointIndex.RightAnklePitch].q = R_P_des
self.low_cmd.motor_cmd[ADAMJointIndex.RightAnkleRoll].q = R_R_des
elif self.time_ < self.duration_ * 3 :
for i in range(ADAM_PRO_NUM_MOTOR):
self.low_cmd.motor_cmd[i].mode = 1 # 1:Enable, 0:Disable
self.low_cmd.motor_cmd[i].tau = 0.
self.low_cmd.motor_cmd[i].q = 0.
self.low_cmd.motor_cmd[i].dq = 0.
self.low_cmd.motor_cmd[i].kp = Kp[i]
self.low_cmd.motor_cmd[i].kd = Kd[i]
for i in range(12):
self.hand_cmd.position[i] = self.open_hand[i]
self.lowcmd_pub_.publish(self.low_cmd)
self.handcmd_pub_.publish(self.hand_cmd)