跳转至

ROS2 底层运动参考例程

底层运动开发例程实现了将机器人从任意初始关节位置,复位至预备状态,然后用两种不同模式控制机器人踝关节摆动和手指位置。例程源代码位于 pnd_ros2/example,可从 此处 访问。源码目录按照机器人型号分别对应sample代码例子:

├── 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

运行方式

  1. 确保ros2 msg接口编译完成。用户自定义$ROOT 目录

cd $ROOT
git clone https://github.com/pndbotics/pnd_ros2.git
cd pnd_ros2
colcon build
2. 使用网线连接机器人,具体参考 网络配置
3. 运行参考例程,以下均以Adam_Pro为例

cd $ROOT/pnd_ros2

# 注册ROS2环境
source /opt/ros/humble/setup.bash

# 注册pnd_adam ros2 消息接口
source install/setup.bash 

# 运行例子
python3 example/adam_pro/adam_pro_low_level_example.py

代码解析

以下代码针对Adam Pro 版本机型,其他版本机型的例程结构均类似,只是关节index定义不同。

核心类型与功能介绍

类型名称 描述
ADAMJointIndex 所有关节按名称排序索引。也是各个关节在LowCmd.MotorCmd命令数组中的下标
LowCmd 电机指令相关ROS2消息结构体
LowState 电机状态相关ROS2消息结构体
HandCmd 手指命令相关ROS2消息结构体
DemonController 核心控制逻辑类(ROS2 node)

例程的主程序定义在 adam_pro_low_level_example.py 中,主程序实例化 DemonController ROS2 node类。 在 DemonController 的构造函数中创建了两个发布器,一个订阅器和定时器,以及对应的ROS2消息结构体:

  • 定义12根手指在握拳时的位置参数 DemonController.close_hand,展开双手时的位置参数 DemonController.open_hand
  • 创建 lowcmd 主题电机位置发布器, handcmd 主题手指位置发布器
  • 底层状态接收 lowstate 主题回调,调用函数 DemonController.getLowState
  • DemonController.control_dt_定义的时间间隔定时调用Custom.LowCmdWrite

具体如下代码所示:

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)

底层状态接收回调函数

当接收到话题 lowstate 的底层状态消息时,程序会自动回调 CDemonController.getLowState 函数。在回调函数中提取了电机的状态。底层状态的结构体定义可参考 ROS2消息定义。实现了:

  • 记录当前各个关节所在位置,为闭环控制提供数据
  • 补齐MotorState对象个数,避免后续越界访问崩溃
  • 隔一段时间打印IMU欧拉角
def getLowState(self, msg):
        # 获取实际接收到的电机数量
        received_len = len(msg.motor_state)

        if received_len < ADAM_PRO_NUM_MOTOR:
            # 如果收到的数据不足,先复制已有的数据
            self.low_state = msg
            # 计算缺失的数量
            missing_count = ADAM_PRO_NUM_MOTOR - received_len
            # 用默认的 MotorState 对象补齐列表
            # 这样 self.low_state.motor_state[i].q 就会默认为 0.0
            self.low_state.motor_state.extend([MotorState() for _ in range(missing_count)])

            # 可选:打印警告信息(仅打印一次或限制频率)
            # print(f"Warning: Received only {received_len} motor states. Padding with {missing_count} default states.")
        else:
            # 如果数量足够或更多,直接赋值
            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)    

底层指令发送函数

CDemonController.LowCmdWrite 用于周期性发送底层指令,实现了:

  • 阶段 1:将 Adam 机器人从任意初始关节位置,复位至预备状态 (程序启动的前 3秒);
  • 阶段 2:采用踝关节 PR控制模式控制踝关节持续摆动 3秒;
  • 阶段 3:把左右手指打开,一直到程序运行终止。
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)