TeleAvatar 开发者指南

更新记录

版本

备注

20260407

v0.1.1

更新了末端和夹爪的demo程序,新增关节限位信息

20260417

v0.1.2

更新了bridge服务的说明,摄像头相关依赖的说明,数据格式的转换(rosbag_to_lerobot),模型部署的说明链接

20260427

v0.1.3

更新了升降电机api的demo代码

注意:本文档专为TA1.0适配,TA2.0文档即将上线

1 快速开始

1.1 基础配置

Teleavatar二次开发版本本体内置ROS2二次开发API,用户可以根据自身需求,修改配置使机器人进入不同模式并可以通过Python或C++编写ROS2程序访问各种接口控制机器人。用户无需自行配置SDK或系统,真正实现开箱即用。

1.2 网络连接

我们推荐用户选择接入机器人自带路由器进入网络环境,详细ip配置需要登陆路由器管理界面确认 192.168.31.xx (在配套的小米路由器网络环境下)。用户可以选择无线接入或网线接入。在接入网络后,用户可以远程访问机器人的核心板(RK3588),接入方法如下

ssh root@yourRobotAddress (default passwd: root)

1.3 Docker的使用

RK3588系统内部没有安装原生ROS2环境,用户可以自行打包镜像并安装ROS2,版本为humble,ROS_DOMAIN_ID为29。

当用户将自己的Docker容器或系统内的ROS_DOMAIN_ID设置为29后将可以连接至本体ROS2系统,在此环境下用户可以访问Teleavatar内部的Topic接口。

1.4 模式配置

RK3588内部存在机器人配置文件(/root/system_config.yaml),用户可以通过修改配置文件内容,按需配置机器人进入不同的模式,再通过重启机器人(网页重启)以生效修改。

http://your3588ip:8501
robot:
  meta_mode: 1
  enable_collision_check: true
  left:
    arm_control_mode: 0
    enable: true  
  right:
    arm_control_mode: 0
    enable: true
  chassis:
    enable: true
    lift_drop_range: 0
  adjust_pose:
    right:
      - [0.0, -1.57, 0.0, -0.26, 0.0, 0.0, 0.0]
      - [0.5, -1.57, 0.0, -0.5, 0.0, 0.0, 0.0]
      - [1.0, -1.57, 0.0, -1.4, 0.0, 0.0, 0.0]
      - [1.7, -1.57, 0.0, -2.4, 0.0, 0.0, -0.68]
      - [-0.8, -0.9, 0.0, -2.3, 0.0, 0.0, -0.68]
      - [-0.8, -0.9, 0.5, -1.12, -0.38, 0.0, -0.12]

Mode

Description

Value

meta_mode

元模式

0(遥操作/VR模式)/ 1(API模式)

enable_collision_check

是否开启碰撞过滤

0(关闭)/ 1(开启)

left/right - enable

是否开启左/右臂

0(关闭)/ 1(开启)

left/right - arm_control_mode

机械臂控制模式

0(末端)/ 1(关节)

chassis - enable

是否开启底盘

0(关闭)/ 1(开启)

chassis - lift_drop_range

升降自由度

默认为0 无升降

adjust_pose

初始关节角数据

默认只需填写right,left为right取反

1.5 step-by-step 上手说明

1.5.1 关于副控制板的使用

副控板的使用主要包含两种情况:

  1. 用户直接将程序运行在副控板上时:此时用户的节点将和主控板处于同一网络内,只需要将ROS_DOMAIN_ID设置为29,即可与主控板内部的话题互相发现。这种情况下,用户可以按照一般的ROS2机器人开发流程,通过开放的/api接口控制机器人。
  1. 用户在自己的主机上运行程序时:此时如果用户的主机连接提供的小米路由器,用户的主机将与副控板在同一网络内,此时用户无法直接访问主控板,但副控板上运行了bridge服务可以让您通过副控板实现话题转发,bridge服务的配置文件可以通过配置选择哪些话题转发,哪些不需要转发,ROS_DOMAIN_ID为0,下文将对此做详细说明(1.5.3)。

1.5.2 api模式的使用步骤

用户想要体验二次开发版本的功能仅需按照如下步骤操作:

  1. 配置文件的编辑:按照上述配置文件的说明,配置好您想要的模式,之后重启机器人(网页重启)即可。
  1. topic列表的确认:远程登陆副控板,选择出厂提供的包含ROS2环境的容器,或者您自己的容器,将ROS_DOMAIN_ID设置为29,查看您需要的topic是否都能被发现。如发现有缺少的情况,需检查配置,网络等设置。
  1. demo程序的验证:推荐先通过提供的3.5程序验证,假设您将模式配置调整为关节模式,直接运行该程序,您将看到机械臂来到一个水平位置。当然您也可以自行设置位置,具体的joint_state/target_ee_pose信息您可以在VR模式下自行示教获取。
  1. 校准模式:无论您选择了何种api模式,您都可以通过校准模式测试机械臂的运动,具体使用方法见2.1.7
  1. 用户自定义控制:用户可以通过自己的模型预测位置或者提前示教好运动点,参考demo例程编写程序,完成闭环控制。

1.5.3 bridge服务的说明

如上文所述,当用户使用自己的主机进行二次开发时,主控中的topic是由bridge服务转出,而用户发送的/api相关topic也是通过bridge服务转入的。用户可以自行配置bridge服务的转入转出列表,该配置文件的目录为副控板的/rosdata/config_file/,在该目录下您将看到topic_config.yaml该文件内包含了bridge服务的转入转出topic列表,用户可以根据自己的需求在该文件中添加和删除topic信息。该服务内置在副控板的files-share-server容器中,上文提到的目录已被挂载在了此容器中,不建议用户对此容器在内的任何内置容器做删除操作,这些容器可能包含Teleavatar的基本运行构成。当用户修改了配置文件之后,同样地,需要重启files-share-server容器以生效,启动脚本的目录为/userdata/file-server/app_server.sh,重启指令为:

./app_server.sh restart

重启之后修改将生效,用户可以通过在用户机上查看topic情况验证是否生效。

关于不同类型的topic的转入转出情况,namespace为/api的控制类话题(2.1章节中介绍的接口)为转入话题,这些话题从用户主机(ROS_DOMAIN_ID为0)转入至机器人内部,状态类话题(2.2章节中介绍的接口)从机器人内部(ROS_DOMAIN_ID为29)转出至用户主机。用户机上可以看到转出话题,完整话题列表推荐在副控板上查看

2 接口说明

2.1 控制相关TOPIC

以下是用户在二次开发中会常用到的ROS2topic,其中有用户可以直接发送的api接口,也有用户可以读取查看的关节状态topic,下面将对这些接口一一介绍。

接口名称

topic_name

interface_type

range

remarks

末端控制接口(右)

/api/right_arm/target_pose

geometry_msgs/msg/Pose

末端控制接口(左)

/api/left_arm/target_pose

geometry_msgs/msg/Pose

关节控制接口(右)

/api/right_arm/joint_cmd

sensor_msgs/msg/JointState

关节控制接口(左)

/api/left_arm/joint_cmd

sensor_msgs/msg/JointState

夹爪控制接口(左)

/api/left_gripper/cmd

std_msgs/msg/Float32

(0, 1)

represent effort

夹爪控制接口(右)

/api/right_gripper/cmd

std_msgs/msg/Float32

(0, 1)

represent effort

底盘控制接口

/api/chassis/velocity

std_msgs/msg/Float32MultiArray

(-1, 1)

[vx, vy, rx]

机械臂关节状态反馈(右)

/right_arm/joint_states

sensor_msgs/msg/JointState

Read only

机械臂关节状态反馈(左)

/left_arm/joint_states

sensor_msgs/msg/JointState

Read only

底盘状态反馈

/chassis/joint_states

sensor_msgs/msg/JointState

Read only

夹爪状态反馈(右)

/right_gripper/joint_states

sensor_msgs/msg/JointState

Read only

夹爪状态反馈(左)

/left_gripper/joint_states

sensor_msgs/msg/JointState

Read only

状态机使能控制接口

/api/fsm/enable

std_msgs/msg/Float32

0(default), 1, -1

1 enable -1 disable

A heartbeat must be sent continuously at intervals of less than 1 second.

校准模式控制接口

/api/fsm/adjust

std_msgs/msg/Float32

0(default), 1, -1

1 enable -1 disable

To enter ADJUST MODE, send the command 1. To exit manually, send -1. The ADJUST MODE will also exit automatically once the adjustment is complete.

升降机伺服控制接口

/api/servo/cmd

std_msgs/msg/Float32MultiArray

[target_speed, enable(1.0, 0.0)]

lifting mechanism motor servo

2.1.1 /api/{$arm_side}_arm/target_pose 末端控制接口

robot:  
  meta_mode: 1  # 1代表API控制模式 用户可以在此模式发送API接口topic控制Teleavatar
  left: # 或right
    arm_control_mode: 0 # 0代表末端控制模式,至少要保证一边机械臂应用
    enable: true # 至少要保证一边机械臂可用
  • 接口类型
Point position
        float64 x
        float64 y
        float64 z
Quaternion orientation
        float64 x 0
        float64 y 0
        float64 z 0
        float64 w 1
  • 接口介绍

本接口为机械臂末端执行器的高级控制系统,专为Teleavatar平台设计。系统通过接收用户指定的末端目标坐标,内置逆运动学(IK)解算器自动完成从任务空间到关节空间的转换。Pose中的position所对应的为末端在base坐标系的坐标,orientation所对应的是夹爪旋转。

  • 注意事项

用户层代码需要负责规划一条从“当前实际位置”到“期望目标位置”的平滑路径,并以固定的频率(50-77Hz)将路径上的点依次喂给这个接口。

2.1.2 /api/{$arm_side}_arm/joint_cmd 关节控制接口

robot:  
  meta_mode: 1  # 1代表API控制模式 用户可以在此模式发送API接口topic控制Teleavatar
  left: # 或right
    arm_control_mode: 1 # 1代表关节控制模式,至少要保证一边机械臂应用
    enable: true # 至少要保证一边机械臂可用
  • 接口类型
std_msgs/Header header
        builtin_interfaces/Time stamp
                int32 sec
                uint32 nanosec
        string frame_id

string[] name
float64[] position
float64[] velocity
float64[] effort
  • 接口介绍

本接口提供对机械臂最底层、最直接的运动控制能力。用户通过发送精确的关节角度目标数组,Teleavatar平台将驱动各关节伺服系统,使机械臂直接运动至指定构型。该接口绕过了笛卡尔空间规划,适用于对运动轨迹有特殊定制需求或需与外部系统(如视觉伺服、力控)进行高速闭环的高级应用场景。

  • 注意事项

当您使用此接口时,即表示您全权接管了机械臂的运动规划与安全责任。系统将完全信任并执行您发送的每一个关节角度指令。因此,您发送的指令序列必须是经过充分验证、在规划上可行且在物理上安全的。任何不当指令都可能导致机械臂发生非预期运动、剧烈抖动、与自身或环境发生碰撞,进而造成设备损坏、工作中断或人员安全风险。

注:推荐在50-77Hz的频率下调用

2.1.3 /api/{$arm_side}_gripper/cmd 夹爪控制接口

robot:
    meta_mode: 1 # 1代表API控制模式 用户可以在此模式发送API接口topic控制Teleavatar
left: # 或right
    arm_control_mode: 0 #或1
    enable: true #至少要保证一边机械臂可用
  • 接口类型
float32 data (0, 1) #0为张到最大,1为合紧
  • 接口介绍

本接口提供对夹爪的控制,用户可以通过发送float32类型的控制夹爪的力。夹爪的控制指令在程序中会以sensor_msgs/msg/JointState的形式发送给电机,但是对于夹爪我们只关心effort的值,data会经过以下计算转换为effort发送。

  • 注意事项

当使用api模式时,需要同时发送机械臂控制指令,由于夹爪和机械臂共用一路CAN而在关节模式时上层程序不会向电机发送运动指令,所以需要用户层单独发送

2.1.4 /api/chassis/velocity 底盘控制接口

robot:
    meta_mode: 1 # 1代表API控制模式 用户可以在此模式发送API接口topic控制Teleavatar
chassis:
    enable: true
  • 接口类型
float32[]         data          # array of data (vx, vy, rx) range (-1, 1)
  • 接口介绍

此接口为底盘移动的api控制接口。用户可以在数组中按vx, vy, rx(x轴方向速度,y轴方向速度,绕z轴的角速度)的顺序依次填入速度值,程序已做归一化处理用户只需将data控制在[-1, 1]范围内即可。程序会以一定的频率输出速度,超时时间为1s,当用户超过1s没有通过api接口发送速度时,底盘会自动停止,同样的通过该接口发送为0的速度一样可以让底盘停止

2.1.5 /api/servo/cmd 升降机伺服控制接口

robot:
    meta_mode: 1 
chassis:
    enable: 1 
    lift_drop_range: 100 #需要大于0 这里举例100
  • 接口类型
float32[]         data          # array of data (speed, enable)
  • 接口介绍

该接口用于控制机器人的腰部升降机构(伺服电机),通过速度模式进行控制。

  • 注意事项

超时保护: 如果 节点在 api_timeout (默认1秒) 内没有收到新的指令,会自动将速度归零。建议以 10Hz 或更高频率 持续发送指令。

紧急停止: 只有在 Enable 状态下才能运动,如果遇到紧急情况,发送 data[1] = 0.0 可以立即切断使能

2.1.6 /api/fsm/enable 状态机使能控制接口

robot:
    meta_mode: 1
  • 接口类型
float32 data (-1, 1)
  • 接口介绍

这个接口主要负责控制状态机的使能状态,以及充当心跳与安全开关的角色。

  • 注意事项

用户必须以高于 1Hz (建议 10Hz-20Hz) 的频率持续发送 data=1.0。如果 FSM 超过 1 秒没有收到 data=1.0,它会认为连接断开或程序崩溃,自动触发 Force PAUSE,将机器人停下来。

2.1.7 /api/fsm/adjust 校准模式控制接口

robot:
    meta_mode: 1
left: # 或right
    arm_control_mode: 0 #或1
    enable: 1 #至少要保证一边机械臂可用
  • 接口类型
float32 data (-1, 1)
  • 接口介绍

该接口用于触发机器人的自动校准/复位动作。通常用于在系统启动后,将机械臂从任意位置自动移动到一个预设的“安全位置”或“举手位置”,以便后续操作。

  • 注意事项

上电时,节点会从配置文件中读取 robot.adjust_pose 列表。如果没有配置,则使用代码中硬编码的默认值(一组将手臂举起的动作序列)。当所有点都执行完毕,状态机会自动切换回 PAUSE 状态。此时你可以发送 /api/fsm/enable 为 1.0 来进入 READY 状态开始正常控制。

ros2 topic pub -1 /api/fsm/adjust std_msgs/msg/Float32 '{data: 1}'
ros2 topic pub -1 /api/fsm/adjust std_msgs/msg/Float32 '{data: -1}'

2.2 状态相关TOPIC

2.2.1 /{$arm_side}_arm/joint_states 机械臂关节状态反馈

  • 接口类型
std_msgs/Header header
        builtin_interfaces/Time stamp
                int32 sec
                uint32 nanosec
        string frame_id

string[] name
float64[] position
float64[] velocity
float64[] effort
  • 接口介绍

这个接口中的是反馈数据(Feedback)。它们报告了机械臂当前的真实物理状态(关节角度、速度、力矩)。在 API 控制闭环中,这是最重要的数据来源。

2.2.2 /{arm_side}_gripper/joint_states 夹爪状态反馈

  • 接口类型
std_msgs/Header header
        builtin_interfaces/Time stamp
                int32 sec
                uint32 nanosec
        string frame_id

string[] name
float64[] position
float64[] velocity
float64[] effort
  • 接口介绍

这个接口中的是反馈数据(Feedback)。它们报告了夹爪当前的真实物理状态(主要关注力矩)。在 API 控制闭环中,这是最重要的数据来源。

2.2.3 /chassis/joint_states 底盘状态反馈

  • 接口类型
std_msgs/Header header
        builtin_interfaces/Time stamp
                int32 sec
                uint32 nanosec
        string frame_id

string[] name
float64[] position
float64[] velocity
float64[] effort
  • 接口介绍

这个接口中的是反馈数据(Feedback)。它们报告了底盘当前的真实物理状态(主要关注速度)。在 API 控制闭环中,这是最重要的数据来源。

2.2.4 /{$arm_side}_arm/current_ee_pose 机械臂末端状态反馈

Point position
        float64 x
        float64 y
        float64 z
Quaternion orientation
        float64 x 0
        float64 y 0
        float64 z 0
        float64 w 1
  • 接口介绍

这个接口中的是反馈数据(Feedback)。它们报告了机械臂当前的末端在笛卡尔空间中的位姿信息。在 API 控制闭环中,这是最重要的数据来源。

2.3 其它TOPIC说明

2.3.1 CAN相关的TOPIC说明

  • /canX/error_code: 接收来自上层控制节点的电机控制命令
  • /canX/motor_cmd: 发布CAN总线上电机的实时状态反馈
  • /canX/motor_states: 发布CAN总线上电机的错误代码和诊断信息
  • /canX/set_zero: 用于设置指定电机的零点位置(发送一个整数值,表示要设置零点的电机编号)

2.3.2 MUX相关的TOPIC说明

/mux/chassis/cmd: 底盘控制命令的多路复用输出,该话题接收来自API模式的底盘速度命令,并通过mux节点转发到底盘控制节点,用于控制机器人底盘的移动。

  • /mux/{$arm_side}_gripper/cmd: 左/右夹爪控制命令的多路复用输出,mux节点将来自API或VR手柄的夹爪控制命令转换为Joy消息格式,通过这些话题发布,用于控制左右夹爪的开合。
  • /mux/{$arm_side}_target_ee_pose: 左/右臂末端执行器目标姿态的多路复用输出, 这些话题发布目标末端执行器(end effector)的姿态信息,用于逆运动学(IK)计算,支持VR模式和API模式的末端执行器控制。
  • /mux/{$arm_side}_arm/mux_joint_cmd: 左/右臂关节命令的多路复用输出,mux节点将来自API模式的关节空间命令通过此话题发布,用于直接控制左/右臂的关节位置。
  • /mux/xr/{$arm_side}_hand_inputs: VR手柄输入的多路复用输出,这些话题将VR手柄的原始输入数据通过mux节点转发,用于VR模式下的机器人控制。手柄输入包括按钮、轴、触发器等数据。
  • /mux/servo/cmd: 伺服电机控制命令的多路复用输出,用于控制系统的伺服电机,可能包括使能/禁用状态或速度命令。

2.3.3 图像相关的TOPIC说明

相机分组

  • left/ - 左腕部相机的所有数据流
  • right/ - 右腕部相机的所有数据流

彩色图像相关话题

  • /{$arm_side}/color/camera_info: 彩色相机的标定参数信息,包括内参矩阵、畸变系数、分辨率等
  • /{$arm_side}/color/image_raw: 未压缩的原始彩色图像数据流
  • /{$arm_side}/color/image_raw/ffmpeg: 使用FFmpeg编码压缩后的彩色视频流,用于降低带宽占用
  • /{$arm_side}/color/metadata: 彩色图像的元数据,如时间戳、曝光参数、增益等

深度图像相关话题

  • /{$arm_side}/depth/camera_info: 深度相机的标定参数信息
  • /{$arm_side}/depth/image_raw: 未压缩的原始深度图像数据(每个像素表示距离值)
  • /{$arm_side}/depth/image_raw/compressedDepth: 压缩后的深度图像,减少传输数据量
  • /{$arm_side}/depth/metadata: 深度图像的元数据信息

对齐话题

  • /{$arm_side}/depth_to_color: 将深度图像对齐到彩色图像坐标系的数据,实现RGB-D配准,使得深度信息和彩色信息的像素一一对应

主摄像头

  • /xr_video_topic/ffmpeg: 主摄像头的图像数据

摄像头相关依赖的说明

对于需要使用高频camera sensor数据的场景,我们建议您使用ffmpeg格式的话题(/{$arm_side}/color/image_raw/ffmpeg,/xr_video_topic/ffmpeg)。相比较raw格式它对带宽占用等更加友好。在使用时您需要通过transport plugin republish解码后的图像topic,详细的使用请参见下方链接:

数据格式的转换(rosbag_to_lerobot)

对于有需要使用lerebot格式数据需求的用户,我们提供了转换工具,详细的使用请参见下方链接:

模型部署的说明

关于模型部署,我们提供了开箱即用的docker镜像,你可以参考下方链接的说明书完成模型部署的操作:

示例程序

3.1 末端模式demo

#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Pose
from std_msgs.msg import Float32
import numpy as np

def quaternion_slerp(q0, q1, t, shortest=True):
    q0 = np.array(q0, dtype=np.float64)
    q1 = np.array(q1, dtype=np.float64)

    q0 /= np.linalg.norm(q0)
    q1 /= np.linalg.norm(q1)

    dot = np.dot(q0, q1)
    if shortest and dot < 0.0:
        q1 = -q1
        dot = -dot
    dot = np.clip(dot, -1.0, 1.0)

    if np.abs(dot - 1.0) < 1e-6:
        return q0.tolist()

    theta = np.arccos(dot) * t
    sin_theta = np.sin(np.arccos(dot))
    q_rot = q0 * np.cos(theta) + (q1 - q0 * dot) * np.sin(theta) / sin_theta
    return q_rot.tolist()

class SinglePointMoveNode(Node):
    def __init__(self):
        super().__init__('single_point_move_node')

        # 发布器
        self.left_pose_pub = self.create_publisher(Pose, '/api/left_arm/target_pose', 10)
        self.enable_pub = self.create_publisher(Float32, '/api/fsm/enable', 10)

        # 订阅当前末端位姿
        self.left_pose_sub = self.create_subscription(
            Pose, '/left_arm/current_ee_pose', self.left_pose_callback, 10)

        self.enable_timer = self.create_timer(0.05, self.enable_callback)
        self.traj_timer = self.create_timer(0.02, self.timer_callback)

        # 目标位姿(用户定义)
        self.target_pose = Pose()
        self.target_pose.position.x = -0.014433992985935767
        self.target_pose.position.y = 0.0181230355198717
        self.target_pose.position.z = -0.5506818540802845
        self.target_pose.orientation.x = -0.651863775688076
        self.target_pose.orientation.y = 0.260180077341881
        self.target_pose.orientation.z = 0.7053938323707012
        self.target_pose.orientation.w = 0.0989923560353721

        self.current_left = None         
        self.traj_points = []             
        self.traj_index = 0               
        self.traj_active = False          
        self.plan_done = False            
        self.reached = False             

        self.pos_tolerance = 0.005        
        self.quat_dot_tolerance = 0.99    

    def enable_callback(self):
        msg = Float32()
        msg.data = 1.0
        self.enable_pub.publish(msg)

    def left_pose_callback(self, msg):
        self.current_left = msg

        if not self.plan_done and self.current_left is not None:
            self.generate_trajectory()
            self.plan_done = True

        if not self.reached and self.current_left is not None:
            self.check_reached()

    def generate_trajectory(self):
        start = self.current_left
        p_start = [start.position.x, start.position.y, start.position.z]
        q_start = [start.orientation.x, start.orientation.y,
                   start.orientation.z, start.orientation.w]

        p_target = [self.target_pose.position.x, self.target_pose.position.y,
                    self.target_pose.position.z]
        q_target = [self.target_pose.orientation.x, self.target_pose.orientation.y,
                    self.target_pose.orientation.z, self.target_pose.orientation.w]

        # 根据直线距离决定插值步数(至少 20 步)
        dist = np.linalg.norm(np.array(p_target) - np.array(p_start))
        steps = max(int(dist / 0.01), 20)

        self.traj_points = []
        for i in range(steps + 1):
            t = i / steps

            px = p_start[0] + (p_target[0] - p_start[0]) * t
            py = p_start[1] + (p_target[1] - p_start[1]) * t
            pz = p_start[2] + (p_target[2] - p_start[2]) * t

            q_interp = quaternion_slerp(q_start, q_target, t)

            pose = Pose()
            pose.position.x = px
            pose.position.y = py
            pose.position.z = pz
            pose.orientation.x = q_interp[0]
            pose.orientation.y = q_interp[1]
            pose.orientation.z = q_interp[2]
            pose.orientation.w = q_interp[3]

            self.traj_points.append(pose)

        self.traj_index = 0
        self.traj_active = True
        self.get_logger().info(f"Trajectory generated with {len(self.traj_points)} points")

    def timer_callback(self):
        if not self.traj_active:
            return

        if self.traj_index < len(self.traj_points):
            self.left_pose_pub.publish(self.traj_points[self.traj_index])
            self.traj_index += 1
        else:
            self.traj_active = False
            self.get_logger().info("All trajectory points published, waiting for actual arrival...")

    def check_reached(self):
        """检查是否已到达目标位姿"""
        dx = self.current_left.position.x - self.target_pose.position.x
        dy = self.current_left.position.y - self.target_pose.position.y
        dz = self.current_left.position.z - self.target_pose.position.z
        pos_err = np.sqrt(dx*dx + dy*dy + dz*dz)

        q_curr = [self.current_left.orientation.x, self.current_left.orientation.y,
                  self.current_left.orientation.z, self.current_left.orientation.w]
        q_tar = [self.target_pose.orientation.x, self.target_pose.orientation.y,
                 self.target_pose.orientation.z, self.target_pose.orientation.w]

        q_curr = np.array(q_curr) / np.linalg.norm(q_curr)
        q_tar = np.array(q_tar) / np.linalg.norm(q_tar)
        dot = abs(np.dot(q_curr, q_tar))

        if pos_err < self.pos_tolerance and dot > self.quat_dot_tolerance:
            self.reached = True
            self.traj_active = False
            # 停止轨迹发布定时器
            self.traj_timer.cancel()
            # 停止 enable 定时器,并发送一次禁用信号
            if hasattr(self, 'enable_timer'):
                self.enable_timer.cancel()
            disable_msg = Float32()
            disable_msg.data = 0.0
            self.enable_pub.publish(disable_msg)
            self.left_pose_pub.publish(self.target_pose)
            self.get_logger().info(f"Target reached! Position error = {pos_err:.4f} m, "
                                   f"Quaternion dot = {dot:.4f}. Timer stopped.")

def main(args=None):
    rclpy.init(args=args)
    node = SinglePointMoveNode()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()

if __name__ == '__main__':
    main()

3.2 夹爪控制demo

#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from std_msgs.msg import Float32
from sensor_msgs.msg import JointState
from builtin_interfaces.msg import Time

class ApiDualGripperJointNode(Node):
    def __init__(self):
        super().__init__('api_dual_gripper_joint_test')
                
        
        # Gripper Publishers (Corrected topic names)
        self.left_gripper_pub = self.create_publisher(Float32, '/api/left_gripper/cmd', 10)
        self.right_gripper_pub = self.create_publisher(Float32, '/api/right_gripper/cmd', 10)

        self.left_joint_cmd_pub = self.create_publisher(JointState, '/api/left_arm/joint_cmd', 10)
        self.right_joint_cmd_pub = self.create_publisher(JointState, '/api/right_arm/joint_cmd', 10)       
        
        self.create_subscription(
            JointState,
            "/left_arm/joint_states",
            self.left_joint_state_callback,
            10
        )
        self.create_subscription(
            JointState,
            "/right_arm/joint_states",
            self.right_joint_state_callback,
            10
        )
        self.current_left_joint_state = None
        self.current_right_joint_state = None
        
        # Timers
        self.create_timer(0.02, self.timer_callback) # 50Hz
        
        self.get_logger().info("API Dual Gripper Joint Test Node Started")

        
    def left_joint_state_callback(self, msg):
        self.current_left_joint_state = msg

    def right_joint_state_callback(self, msg):
        self.current_right_joint_state = msg

    def timer_callback(self):
        current_time = self.get_clock().now()
        
        # Publish Gripper Command
        gripper_msg = Float32()
        # Target values: 0.04 (Open)
        gripper_msg.data = 0.04
        
        if self.current_left_joint_state is None or self.current_right_joint_state is None:
            self.get_logger().warn("Waiting for joint state data...")
            return
        
        left_joint_cmd = JointState()
        left_joint_cmd.header.stamp = current_time.to_msg()  
        left_joint_cmd.name = self.current_left_joint_state.name
        left_joint_cmd.position = self.current_left_joint_state.position
        left_joint_cmd.velocity = [0.0] * len(self.current_left_joint_state.name)  
        left_joint_cmd.effort = [0.0] * len(self.current_left_joint_state.name)
        
        right_joint_cmd = JointState()
        right_joint_cmd.header.stamp = current_time.to_msg()
        right_joint_cmd.name = self.current_right_joint_state.name
        right_joint_cmd.position = self.current_right_joint_state.position
        right_joint_cmd.velocity = [0.0] * len(self.current_right_joint_state.name)
        right_joint_cmd.effort = [0.0] * len(self.current_right_joint_state.name)
        
        self.left_joint_cmd_pub.publish(left_joint_cmd)
        self.right_joint_cmd_pub.publish(right_joint_cmd)
        
        self.left_gripper_pub.publish(gripper_msg)
        self.right_gripper_pub.publish(gripper_msg)

def main(args=None):
    rclpy.init(args=args)
    node = ApiDualGripperJointNode()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()

if __name__ == '__main__':
    main()

3.3 关节模式demo

#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import JointState
from std_msgs.msg import Float32
import numpy as np

class ApiDualArmJointNode(Node):
    def __init__(self):
        super().__init__('api_dual_arm_joint_test')
        
        # Publishers
        self.left_joint_cmd_pub = self.create_publisher(JointState, '/api/left_arm/joint_cmd', 10)
        self.right_joint_cmd_pub = self.create_publisher(JointState, '/api/right_arm/joint_cmd', 10)
        
        # Enable signal
        self.enable_pub = self.create_publisher(Float32, '/api/fsm/enable', 10)
        
        # Subscribers
        self.create_subscription(
            JointState,
            "/left_arm/joint_states",
            self.left_joint_state_callback,
            10
        )
        self.create_subscription(
            JointState,
            "/right_arm/joint_states",
            self.right_joint_state_callback,
            10
        )
        
        self.current_left_joint_state = None
        self.current_right_joint_state = None
        
        # Target values
        # Left: [0, 1.57, 0, 0.26, 0, 0, 0]
        # Right: [0, -1.57, 0, -0.26, 0, 0, 0] (Symmetric)
        self.target_left_position = np.array([0, 1.57, 0, 0.26, 0, 0, 0])
        self.target_right_position = np.array([0, -1.57, 0, -0.26, 0, 0, 0])
        
        # Timers
        self.create_timer(0.05, self.enable_callback)
        self.create_timer(0.02, self.timer_callback)
        
        self.get_logger().info("API Dual Arm Joint Test Node Started")

    def enable_callback(self):
        msg = Float32()
        msg.data = 1.0
        self.enable_pub.publish(msg)

    def left_joint_state_callback(self, msg):
        self.current_left_joint_state = msg

    def right_joint_state_callback(self, msg):
        self.current_right_joint_state = msg

    def timer_callback(self):
        kp = 1.0
        clip_alpha = 0.25

        # Process Left Arm
        if self.current_left_joint_state is not None:
            self._process_arm(
                self.current_left_joint_state, 
                self.target_left_position, 
                self.left_joint_cmd_pub, 
                kp, clip_alpha, "Left"
            )
        else:
            self.get_logger().warning("Waiting for left joint states...", throttle_duration_sec=1.0)

        # Process Right Arm
        if self.current_right_joint_state is not None:
            self._process_arm(
                self.current_right_joint_state, 
                self.target_right_position, 
                self.right_joint_cmd_pub, 
                kp, clip_alpha, "Right"
            )
        else:
            self.get_logger().warning("Waiting for right joint states...", throttle_duration_sec=1.0)

    def _process_arm(self, current_state, target_pos, pub, kp, clip_alpha, arm_name):
        current_pos = np.array(current_state.position)
        
        if len(current_pos) != len(target_pos):
            self.get_logger().warning(f"{arm_name} dimension mismatch: {len(current_pos)} vs {len(target_pos)}", throttle_duration_sec=1.0)
            return
        
        velocity = np.clip(
            kp * (target_pos - current_pos),
            -clip_alpha, clip_alpha
        ).tolist()
        
        msg = JointState()
        msg.header.stamp = self.get_clock().now().to_msg()
        msg.name = current_state.name
        msg.position = target_pos.tolist()
        msg.velocity = velocity
        msg.effort = [0.0] * len(target_pos)
        
        pub.publish(msg)

def main(args=None):
    rclpy.init(args=args)
    node = ApiDualArmJointNode()
    try:
        rclpy.spin(node)
    except KeyboardInterrupt:
        pass
    finally:
        node.destroy_node()
        rclpy.shutdown()

if __name__ == '__main__':
    main()

3.4 底盘控制demo

#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from std_msgs.msg import Float32MultiArray,Float32

class ChassisVelocityPublisher(Node):
    """
    底盘速度命令发布节点(持续发布一小段时间后自动退出)
    发布到 /api/chassis/velocity 话题(请确认实际订阅话题)
    """

    def __init__(self):
        super().__init__('chassis_velocity_api_publisher')

        # 创建发布者(如果实际订阅话题是 /mux/chassis/cmd,请修改话题名)
        self.chassis_vel_pub = self.create_publisher(
            Float32MultiArray,
            '/api/chassis/velocity',   # 根据实际情况修改
            10
        )

        # 发布频率 10Hz
        self.publish_rate = 10.0
        self.timer = self.create_timer(1.0 / self.publish_rate, self.publish_velocity_cmd)

        self.enable_pub = self.create_publisher(Float32, '/api/fsm/enable', 10)
        self.create_timer(0.05, self.enable_callback)

        # 速度命令(初始为0)
        self.linear_x = 0.0
        self.linear_y = 0.0
        self.angular_z = 0.0

        # 2秒后自动停止的定时器
        self.shutdown_timer = self.create_timer(2.0, self.shutdown_callback)

        self.get_logger().info('Chassis velocity publisher started (will run for 2 seconds)')


    def enable_callback(self):
        msg = Float32()
        msg.data = 1.0
        self.enable_pub.publish(msg)


    def publish_velocity_cmd(self):
        """定时发布速度命令"""
        msg = Float32MultiArray()
        msg.data = [self.linear_x, self.linear_y, self.angular_z]
        self.chassis_vel_pub.publish(msg)
        self.get_logger().info(
            f'Published: linear_x={self.linear_x:.2f}, '
            f'linear_y={self.linear_y:.2f}, angular_z={self.angular_z:.2f}'
        )

    def shutdown_callback(self):
        """2秒后停止:发布零速度并关闭节点"""
        self.get_logger().info('Test finished, stopping robot.')

        # 发布零速度确保停止
        stop_msg = Float32MultiArray()
        stop_msg.data = [0.0, 0.0, 0.0]
        self.chassis_vel_pub.publish(stop_msg)

        # 取消所有定时器
        self.timer.cancel()
        self.shutdown_timer.cancel()

        # 设置标志,让主循环退出
        self.shutdown_flag = True

    def set_velocity(self, linear_x, linear_y, angular_z):
        """设置速度值"""
        self.linear_x = linear_x
        self.linear_y = linear_y
        self.angular_z = angular_z


def main(args=None):
    rclpy.init(args=args)

    # 创建节点
    chassis_publisher = ChassisVelocityPublisher()

    # 设置微小直线速度(前进0.05 m/s,不转弯)
    chassis_publisher.set_velocity(
        linear_x=0.05,   # 可调整,建议0.02~0.1
        linear_y=0.0,
        angular_z=0.0
    )

    # 自定义主循环,检测退出标志
    try:
        while rclpy.ok() and not hasattr(chassis_publisher, 'shutdown_flag'):
            rclpy.spin_once(chassis_publisher, timeout_sec=0.1)
    except KeyboardInterrupt:
        chassis_publisher.get_logger().info('Interrupted by user')
    finally:
        # 确保最后发布零速度
        stop_msg = Float32MultiArray()
        stop_msg.data = [0.0, 0.0, 0.0]
        chassis_publisher.chassis_vel_pub.publish(stop_msg)

        chassis_publisher.destroy_node()
        rclpy.shutdown()


if __name__ == '__main__':
    main()

初始位置demo(基于关节模式)

robot_name: TeleAvatar
arms:
  dof_num: 7
  max_arm_len: 0.55
  right_arm:
    upper: [ 1.8, 0.0, 2.6, -0.25, 1.8, 1.4, 0.6 ]
    lower: [ -1.8, -1.9, -1.3, -1.9, -1.8, -1.4, -0.6 ]
  left_arm:
    upper: [ 1.8, 1.9, 1.3, 1.90, 1.8, 1.4, 0.6 ]
    lower: [ -1.8, 0.0, -2.6, 0.25, -1.8, -1.4, -0.6 ]
  vel_limit: [ 15.0, 15.0, 20.0, 20.0, 44.0, 33.0, 33.0 ]
  acc_limit: [ 120.0, 120.0, 60.0, 60.0, 17.0, 14.0, 14.0 ]

ik_solver:
  debug: false
  arm_ctrl_dt: 0.01
  ee_predict_alpha: 1.0
  base_frame: base
  ik_pub_topic: ik_joint_cmd
  states_sub_topic: joint_states
  right_arm:
    ee_sub_topic_name: right_target_ee_pose
    shoulder_frame_name: right_shoulder_base
    target_ee_frame_name: right_target_ee_frame
    curret_ee_frame_name: right_current_ee_frame
    joint_name_prefix: r_joint
    T_lee2pee: [ [ 0, -1, 0, 0 ], [ 1, 0, 0, 0 ], [ 0, 0, 1, 0 ], [ 0, 0, 0, 1 ] ]
  left_arm:
    ee_sub_topic_name: left_target_ee_pose
    shoulder_frame_name: left_shoulder_base
    target_ee_frame_name: left_target_ee_frame
    curret_ee_frame_name: left_current_ee_frame
    joint_name_prefix: l_joint
    T_lee2pee: [ [ 0, 1, 0, 0 ], [ -1, 0, 0, 0 ], [ 0, 0, 1, 0 ], [ 0, 0, 0, 1 ] ]

  vel:
    fb_kp: [ 7, 7, 10, 10, 10, 8, 8 ]
    ff_alpha: 0.3
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import JointState
from rclpy.clock import Clock
from rclpy.executors import MultiThreadedExecutor
from std_msgs.msg import Float32

import numpy as np
import yaml

robot_config = yaml.safe_load(open("arm_config.yml"))

class JointInterpolator(Node):
    def __init__(self, namespace="right_arm", down=True, timer_on=False):
        super().__init__(f"{namespace}_joint_interpolator")
        self.namespace = namespace
        self.down = down

        self.subscription = self.create_subscription(
            JointState,
            f"/{namespace}/joint_states",
            self.joint_state_callback,
            10,
        )
        self.publisher = self.create_publisher(
            JointState, f"/api/{namespace}/joint_cmd", 10
        )
        
        # Enable signal
        if timer_on == True:
            self.enable_pub = self.create_publisher(Float32, '/api/fsm/enable', 10)
            self.create_timer(0.05, self.enable_callback)

        self.err_publisher = self.create_publisher(
            JointState, f"/{namespace}/traj_error", 10
        )
        self.current_pos = None
        self.start_time = None

        self.joint_names = []
        self.duration = 5.0  # seconds
        self.target_positions = []

        
        self.position_tolerance = 0.05  
        self.convergence_duration = 0.5  
        self.convergence_start_time = None
        self.is_converged = False

        num_activate = 7
        self.enable_flags = [False] * (7 - num_activate) + [True] * num_activate

        self.upper = np.array(robot_config["arms"][namespace]["upper"])
        self.lower = np.array(robot_config["arms"][namespace]["lower"])
        # Timer to run at 100 Hz for publishing commands
        self.timer = self.create_timer(0.01, self.timer_callback)  # 100 Hz

        if self.down:
            joint_2_down_pos = 1.5 if self.namespace == "left" else -1.5
            joint_4_down_pos = 0.0
        else:
            joint_2_down_pos, joint_4_down_pos = 0, 0
        
        # 根据namespace设置不同的目标位置
        if self.namespace == "right_arm":
            # 右臂目标位置
            self.target_positions = [
                -0.50,  # r_joint1
                -0.92,  # r_joint2
                0.52,  # r_joint3
                -1.28,    # r_joint4
                0.32,  # r_joint5
                0.55, # r_joint6
                -0.52   # r_joint7
            ]
        else:  # left
            # 左臂目标位置
            self.target_positions = [
                0.41,  # l_joint1
                1.16,  # l_joint2
                -0.47,  # l_joint3
                0.90,    # l_joint4
                0.23,  # l_joint5
                -0.15, # l_joint6
                0.60   # l_joint7
            ]

        self.get_logger().info(
            f"Joint interpolator node initialized for {namespace}. Waiting for initial joint states..."
        )
        self.get_logger().info(f"Position tolerance: {self.position_tolerance} rad")
        self.get_logger().info(f"Convergence duration: {self.convergence_duration} sec")

    def enable_callback(self):
        msg = Float32()
        msg.data = 1.0
        self.enable_pub.publish(msg)


    def joint_state_callback(self, msg):
        self.joint_names = msg.name
        self.current_pos = np.array(msg.position)
        self.current_vel = np.array(msg.velocity)

    def check_convergence(self, current_position, target_position):
        
        enabled_indices = [i for i, enabled in enumerate(self.enable_flags) if enabled]
        position_errors = np.abs(np.array(current_position) - np.array(target_position))

       
        enabled_errors = position_errors[enabled_indices]
        max_error = np.max(enabled_errors)

        return max_error < self.position_tolerance

    def timer_callback(self):
        
        if self.current_pos is None or self.start_time is None:
            self.get_logger().warn(
                "Current position not set yet, skipping timer callback."
            )
            self.start_time = self.get_clock().now()
            return

        current_time = self.get_clock().now()
        elapsed_time = (
            current_time - self.start_time
        ).nanoseconds / 1e9  # Convert to seconds

        cmd_msg = JointState()
        cmd_msg.header.stamp = current_time.to_msg()
        cmd_msg.name = self.joint_names

        if elapsed_time >= self.duration:
            # Publish target positions (all zeros) once duration is reached
            cmd_msg.position = self.target_positions
        else:
            # Linear interpolation between initial and target positions
            alpha = elapsed_time / self.duration
            interpolated_positions = [
                self.current_pos[i] * (1.0 - alpha) + self.target_positions[i] * alpha
                for i in range(len(self.current_pos))
            ]
            cmd_msg.position = interpolated_positions

        cmd_msg.velocity = [0.0] * 7
        des_position = np.array(cmd_msg.position)
        kp = np.array([ 7, 7, 10, 10, 10, 8, 8 ])
        vel_fb = kp * (des_position - self.current_pos)

        for i in range(len(self.enable_flags)):
            if self.enable_flags[i]:
                cmd_msg.position[i] = self.current_pos[i]
                cmd_msg.velocity[i] = vel_fb[i]
        cmd_msg.effort = [0.0] * 7
        self.publisher.publish(cmd_msg)

        
        msg = JointState()
        msg.header.stamp = current_time.to_msg()
        msg.name = self.joint_names
        msg.position = [
            self.current_pos[i] - cmd_msg.position[i]
            for i in range(len(self.current_pos))
        ]
        self.err_publisher.publish(msg)

        
        if elapsed_time >= self.duration: 
            target_pos = np.array(self.target_positions)
            if self.check_convergence(self.current_pos, target_pos):
                if self.convergence_start_time is None:
                    self.convergence_start_time = current_time
                    self.get_logger().info(
                        f"{self.namespace} arm: Position converged, starting convergence timer..."
                    )
                else:
                    convergence_elapsed = (
                        current_time - self.convergence_start_time
                    ).nanoseconds / 1e9
                    if convergence_elapsed >= self.convergence_duration:
                        if not self.is_converged:
                            self.get_logger().info(
                                f"{self.namespace} arm: Convergence achieved! Stopping control."
                            )
                            self.is_converged = True
                            
            else:
                
                if self.convergence_start_time is not None:
                    self.get_logger().info(
                        f"{self.namespace} arm: Position diverged, resetting convergence timer..."
                    )
                    self.convergence_start_time = None

def main(args=None):
    rclpy.init(args=args)
    joint_interpolator = JointInterpolator()
    rclpy.spin(joint_interpolator)
    joint_interpolator.destroy_node()
    rclpy.shutdown()

def main_multithreaded(args=None):
    rclpy.init(args=args)

    left_node = JointInterpolator(namespace="left_arm", down=True, timer_on=True)  # left arm
    right_node = JointInterpolator(namespace="right_arm", down=True, timer_on=True)  # right arm

    executor = MultiThreadedExecutor()
    executor.add_node(left_node)
    executor.add_node(right_node)

    try:
        print("Starting both arm nodes with MultiThreadedExecutor...")
        while rclpy.ok():
            executor.spin_once(timeout_sec=0.1)

            
            if left_node.is_converged and right_node.is_converged:
                print("Both arms converged! Shutting down...")
                break

    except KeyboardInterrupt:
        print("Shutting down...")
    finally:
        left_node.destroy_node()
        right_node.destroy_node()
        executor.shutdown()
        rclpy.shutdown()

if __name__ == "__main__":
    main_multithreaded()
    

6.6 升降电机demo(仅适用于升降版)

#!/usr/bin/env python3
  import time

  import rclpy
  from rclpy.node import Node
  from std_msgs.msg import Float32MultiArray


  class LiftServoApiDemo(Node):
      def __init__(self):
          super().__init__("lift_servo_api_demo")
          self.servo_command_pub = self.create_publisher(
              Float32MultiArray,
              "/api/servo/cmd",
              10,
          )

      def publish_servo_command(self, normalized_velocity: float, servo_enabled: bool):
          normalized_velocity = max(-1.0, min(1.0, float(normalized_velocity)))

          if not servo_enabled:
              normalized_velocity = 0.0

          msg = Float32MultiArray()
          msg.data = [
              normalized_velocity,           # 归一化速度指令 [-1, 1]
              1.0 if servo_enabled else 0.0, # 使能标志
          ]
          self.servo_command_pub.publish(msg)

      def publish_for_duration(
          self,
          normalized_velocity: float,
          servo_enabled: bool,
          duration_sec: float,
          rate_hz: float = 20.0,
      ):
          dt = 1.0 / rate_hz
          end_time = time.time() + duration_sec

          while rclpy.ok() and time.time() < end_time:
              self.publish_servo_command(normalized_velocity, servo_enabled)
              rclpy.spin_once(self, timeout_sec=0.0)
              time.sleep(dt)

      def stop_and_disable(self):
          self.publish_servo_command(0.0, False)

      def run_demo(self):
          self.get_logger().info("Lift up")
          self.publish_for_duration(
              normalized_velocity=0.30,
              servo_enabled=True,
              duration_sec=1.5,
          )

          self.get_logger().info("Pause")
          self.publish_for_duration(
              normalized_velocity=0.0,
              servo_enabled=True,
              duration_sec=0.4,
          )

          self.get_logger().info("Lift down")
          self.publish_for_duration(
              normalized_velocity=-0.30,
              servo_enabled=True,
              duration_sec=1.5,
          )

          self.get_logger().info("Stop")
          self.stop_and_disable()


  def main(args=None):
      rclpy.init(args=args)
      node = LiftServoApiDemo()

      try:
          node.run_demo()
      except KeyboardInterrupt:
          pass
      finally:
          node.stop_and_disable()


  if __name__ == "__main__":
      main()

结构图

关节限位信息

支持