升降接口说明

一、概述

升降是机器人的垂直高度调节机构(整机 Z 向升降立柱)。它接口很少,但有一个必须先分清的前提:手册里的升降有两条完全不同的实现路径,走错一条,一个接口都调不通。

路径 配置条件 接口位置 本文是否覆盖
Modbus 升降 lift_model = yako / leesn / mdx 手册第六章 /fd_robot/lifting_motor/* 是,全文
YD100 机型 lift_model = yd100 手册 5.3 节 YD100 腰部接口
(无独立升降节点)
否,仅在第 2 章给出分流指引

本文覆盖手册第六章的全部二次开发接口 6 条(5 个命令发布话题 + 1 个状态订阅话题),以及第二章列出的 2 条系统内部接口,用于排查问题。

二次开发接口 6 条
5 发布(命令)+ 1 订阅(反馈)
没有 Action
手册 6.3 明写「无」
单位是米
Float32.data 为 m,行程 0–0.7 m
每接口配示例
命令行 / Python / C++ 各一段

对接升降真正要小心的是三件事:① 单位是米,不是毫米(发 100 会被限幅到 0.7);② 位置运动前必须先设速度,否则位置命令没有速度依据;③ 到位判定靠读 current_pose,不是靠固定延时。

接口挂在 /fd_robot/lifting_motor/ 下,由 fd_lifting_motor_pkg(Modbus 升降节点)提供。节点未启动时,相关接口不会有任何响应。

1.1 与 fd_api 升降接口的关系

工作区已有 《fd_api 升降机构说明》(C++ 封装层)。两者是两个层级,不要混用:

对比项 fd_api 封装层 本文(ROS 2 原生接口)
调用方式 fd_robot::Control::lift_set_pos() ros2 topic pub / topic echo
位置单位 毫米(lift_set_pos(spd, pos_mm)) 米(Float32.data,0–0.7 m)
速度单位 随封装层定义 m/s(yako 上限约 ±0.2)
接口粒度 按动作命名:上升 / 下降 / 位置 / 停止 / 设原点 按话题拆分:位置 / 速度预设 / 点动 / 回原点 / 字符串命令
可用范围 封装层已包装的能力 可直接使用 status 的全部字符串命令
手册原文特别提示:position_cmd 的说明里写着「fd_api.lift_set_pos 的毫米值会先换算成米」。也就是说 lift_set_pos(50.0f, 400.0f) 里的 400 是 400 mm,封装层会换算成 0.4 m 再走本章话题。直接发 ROS 话题时不要照抄毫米值。
不要同时用两套接口控制升降。手册 1.2 节接入步骤第 6 条明确:同一执行器同一时间只能有一个控制来源。fd_api 与 ROS 2 接口同时下发会互相抢控制权。

1.2 升降在整机中的位置

二次开发程序 / 遥操作 / 上层应用
▼
ROS 2 接口层 /fd_robot/lifting_motor/*
▼
fd_lifting_motor_pkg(Modbus 升降节点)
▼
升降电机 / 驱动器(Modbus,yako / leesn)

升降没有应用层服务,也没有动作接口——它是最"薄"的一层:程序直接向节点发布话题,节点转发给 Modbus 驱动。因此本章的全部接口都是话题。

安全须知(手册 1.1 节原文):触发急停、关机或重启服务时,升降、腰部等执行器会进入安全状态。二次开发程序应订阅急停和关机信号(/fd_robot/emergency_signal、/fd_robot/shutdown_signal)并做相应处理,不要把关机、重启相关接口当作常规控制手段。

二、运行前提与约定

2.1 环境与节点前提

项目 要求 说明
ROS 版本 ROS 2 Humble 手册适用软件版本 FDRS-V04-02-00
环境加载 source install/setup.bash 调用接口前先执行
域 ID ROS_DOMAIN_ID=25 手册 1.1 节明确要求确认
驱动节点 Modbus 升降节点已启动 对应 lift_model = yako / leesn / mdx
机型分流 lift_model = yd100 没有独立升降节点,请改用 5.3 节 YD100 腰部接口
先确认自己属于哪套。手册 6.4 节末尾原话:「lift_model=yd100 的机型不要使用本章接口。」YD100 机型的升降由 /fd_robot/waist_joints/* 那组接口控制(详见 《天链机器人腰部接口说明》)。

2.2 如何确认接口可用

# 1) 确认能列出升降话题
ros2 topic list | grep lifting_motor

# 期望看到(全部 6 条)
# /fd_robot/lifting_motor/current_pose
# /fd_robot/lifting_motor/home_cmd
# /fd_robot/lifting_motor/position_cmd
# /fd_robot/lifting_motor/position_set_speed_cmd
# /fd_robot/lifting_motor/status
# /fd_robot/lifting_motor/velocity_cmd

# 2) 确认状态反馈在发布(20 Hz)
ros2 topic hz /fd_robot/lifting_motor/current_pose

# 3) 确认消息类型
ros2 topic info /fd_robot/lifting_motor/position_cmd -v
只有反馈、没有命令话题说明升降节点只起了一部分或机型是 YD100;一条都看不到说明节点未启动,此时发命令不会有任何响应。

2.3 单位与取值约定

接口 字段 单位 取值范围
position_cmd data 米(m) 0–0.7 m(超出限幅)
position_set_speed_cmd data 米/秒(m/s) yako 常用 ≤ 0.2
velocity_cmd data 米/秒(m/s) yako 约 ±0.2,正负表示方向
home_cmd (Empty,无字段) — —
status data 字符串 5 个固定命令词,见 4.5 节
current_pose data 米(m) 当前高度;20 Hz
单位陷阱:ROS 用米,fd_api 用毫米。手册附录「已知限制」专门列了一条:
「升降 ROS position_cmd — Float32 单位为米,限幅范围 0–0.7 m — 不要按毫米发送(如 100);只有 fd_api.lift_set_pos 使用毫米」。
误发 100 想表达 100 mm,实际会被限幅成 0.7(顶到上限),且不会报错。

2.4 行程与限幅

目标高度会被限幅到 [0, 0.7] m。注意这是限幅(静默截断)而不是拒绝:超出范围的值被截断到边界后继续执行,调用方通常不会收到错误提示。

速度方面,手册 6.4 节给出:yako 的速度上限约为 ±0.2 m/s;执行位置运动前,请先通过 position_set_speed_cmd 设置速度(单位 m/s,yako 同样以 0.2 m/s 为上限)。

2.5 如何判断「到位」

手册 6.4 节末尾只有一句:「请通过 current_pose 判断是否到位。」升降没有 Action,也没有运动状态话题,所以没有 busy、result_code、REACHED 这类标志可用。

推荐写法是「读位置 → 与目标比较容差」:

# 目标 0.10 m,当前高度读回来接近 0.10 就认为到位
ros2 topic echo --once /fd_robot/lifting_motor/current_pose
关于容差:手册未给出到位判定的数值容差。本节示例里用的 0.005 m(5 mm)是工程建议值,不是手册数据,请按自己机型的重复定位精度调整。不要用固定 sleep 等待——行程和负载不同,时间不可预测。

2.6 安全前提

程序运行期间始终订阅急停。手册 1.2 节接入步骤第 2 条原文:「程序运行期间始终订阅 /fd_robot/emergency_signal。data 为 true 时停止发送任何运动命令;急停触发后,升降、腰部等执行器会进入安全状态。」
同一执行器同一时间只能有一个控制来源。手册 1.2 节第 6 条:Pico 遥操作、/teleop/joint_targets_* 或底层 /tl_control/* 正在控制时,不要再通过本手册接口下发命令。手册 11.3 节另说明:Pico 输入会由身体控制节点转换为头颈、腰部、升降等控制命令——即遥操作会话开启期间,升降可能已被别人控着。

三、接口总览

3.1 二次开发接口(6 条)

方向从二次开发程序的角度理解:「发布」表示由你的程序发送命令,「订阅」表示由你的程序接收反馈。

接口 类型 方向 用途 配置条件
/fd_robot/lifting_motor/position_cmd std_msgs/Float32 发布 目标高度(绝对值,单位 m) Modbus 升降节点
/fd_robot/lifting_motor/position_set_speed_cmd std_msgs/Float32 发布 设置后续位置运动的速度 Modbus 升降节点
/fd_robot/lifting_motor/velocity_cmd std_msgs/Float32 发布 速度控制(点动) Modbus 升降节点
/fd_robot/lifting_motor/home_cmd std_msgs/Empty 发布 回原点 Modbus 升降节点
/fd_robot/lifting_motor/status std_msgs/String 发布 字符串控制命令 Modbus 升降节点
/fd_robot/lifting_motor/current_pose std_msgs/Float32 订阅 当前高度;20 Hz Modbus 升降节点

3.2 系统内部接口(2 条)

下列接口由驱动和诊断底层使用,手册在 6.1 / 6.2 节把它们标为「系统内部接口」。列出仅供排查问题时参考,请勿与二次开发接口同时调用。

接口 类型 方向 用途 配置条件
/fd_robot/lifting_motor/motor_info fd_msgs/MotorInfoMsg 订阅 电机信息;20 Hz Modbus 升降节点
/fd_robot/self_check/lifting_motor fd_msgs/SelfCheckSrv 服务 升降自检 Modbus 升降节点

3.3 关于动作接口

手册 6.3 节原文:「动作:无。」升降没有 Action(YD100 机型对应的 move_to_location 属于 5.3 节腰部接口)。
因此:没有 goal / result_code / feedback 可用,运动完成只能靠 current_pose 判断(见 2.5 节)。

四、话题接口详解

本章按手册 6.1 节的顺序,逐个说明 6 个二次开发话题:先 5 个命令发布,再 1 个状态订阅。每个接口固定给出「语义卡片 → 字段表 → 三段示例(命令行 / Python / C++)」。

关于 QoS:手册第六章没有对升降这组话题标注 QoS 要求,使用默认 QoS(RELIABLE / VOLATILE,深度 10)即可。这与腰部 YD100 那三个 BEST_EFFORT 状态话题不同,不要照搬那边的写法。
示例均为示意代码,字段名与类型请以 ros2 interface show 的实际输出为准。三套 *_cmd 话题之间存在顺序依赖(先设速度、再发位置),示例里已按正确顺序组织。
/fd_robot/lifting_motor/position_cmd 话题 · 发布
目标高度(绝对值)。类型 std_msgs/Float32,来源包 fd_lifting_motor_pkg,配置条件「Modbus 升降节点」。
发的是绝对高度,不是增量。执行前请先用 position_set_speed_cmd 设置速度(手册 6.4 节原文要求)。
字段 侧 类型 取值 说明
data 消息 float32 0–0.7 m(超出时限幅) 绝对高度。fd_api.lift_set_pos 的毫米值会先换算成米
单位是米,不是毫米。要 100 mm 就发 0.10。发 100 会被限幅到 0.7(顶到上限)。

示例

① 命令行(ros2 CLI)

# ① 先设速度(顺序不能反)
ros2 topic pub -1 /fd_robot/lifting_motor/position_set_speed_cmd \
  std_msgs/msg/Float32 "{data: 0.05}"

# ② 再发目标高度:0.10 m(= 100 mm)
ros2 topic pub -1 /fd_robot/lifting_motor/position_cmd \
  std_msgs/msg/Float32 "{data: 0.10}"

# ③ 回读当前高度确认
ros2 topic echo --once /fd_robot/lifting_motor/current_pose

# 查看消息定义
ros2 interface show std_msgs/msg/Float32

② Python(rclpy)

# 示意代码:先设速度、再发位置、最后读 current_pose 判到位
import rclpy
from rclpy.node import Node
from std_msgs.msg import Float32

TOL_M = 0.005          # 工程建议容差 5 mm,手册未给该值,请按机型调整

class LiftPosition(Node):
    def __init__(self):
        super().__init__('lift_position')
        self.pub_speed = self.create_publisher(
            Float32, '/fd_robot/lifting_motor/position_set_speed_cmd', 10)
        self.pub_pos = self.create_publisher(
            Float32, '/fd_robot/lifting_motor/position_cmd', 10)
        self.sub_pos = self.create_subscription(
            Float32, '/fd_robot/lifting_motor/current_pose', self.on_pose, 10)
        self.cur = None

    def on_pose(self, msg):
        self.cur = msg.data

    def _pub_n(self, pub, value, n=3):
        # 重复发布几次,避免订阅端尚未建立连接时丢消息(工程建议)
        msg = Float32()
        msg.data = float(value)
        for _ in range(n):
            pub.publish(msg)
            rclpy.spin_once(self, timeout_sec=0.05)

    def move_to(self, height_m, speed_mps=0.05, timeout_sec=20.0):
        if not 0.0 <= height_m <= 0.7:
            self.get_logger().warn(f'目标 {height_m} m 超出行程 0–0.7,将被限幅')
        self._pub_n(self.pub_speed, speed_mps)   # ① 设速度
        self._pub_n(self.pub_pos, height_m)      # ② 发位置

        deadline = self.get_clock().now().nanoseconds + int(timeout_sec * 1e9)
        while rclpy.ok():
            rclpy.spin_once(self, timeout_sec=0.1)
            if self.cur is not None:
                self.get_logger().info(f'当前高度 {self.cur:.4f} m')
                if abs(self.cur - height_m) < TOL_M:
                    self.get_logger().info('到位')
                    return True
            if self.get_clock().now().nanoseconds > deadline:
                self.get_logger().error('超时未到位')
                return False

def main():
    rclpy.init()
    node = LiftPosition()
    node.move_to(0.10)        # 升到 0.10 m = 100 mm
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

③ C++(rclcpp)

#include <chrono>
#include <cmath>
#include <memory>
#include <rclcpp/rclcpp.hpp>
#include "std_msgs/msg/float32.hpp"

using namespace std::chrono_literals;
static constexpr double TOL_M = 0.005;   // 工程建议容差,手册未给该值

int main(int argc, char** argv) {
  rclcpp::init(argc, argv);
  auto node = rclcpp::Node::make_shared("lift_position");

  auto pub_speed = node->create_publisher<std_msgs::msg::Float32>(
      "/fd_robot/lifting_motor/position_set_speed_cmd", 10);
  auto pub_pos = node->create_publisher<std_msgs::msg::Float32>(
      "/fd_robot/lifting_motor/position_cmd", 10);

  double cur = std::nan("");
  auto sub_pos = node->create_subscription<std_msgs::msg::Float32>(
      "/fd_robot/lifting_motor/current_pose", 10,
      [&cur](std_msgs::msg::Float32::SharedPtr msg) { cur = msg->data; });

  // ① 先设速度(0.05 m/s)
  std_msgs::msg::Float32 s;
  s.data = 0.05f;
  for (int i = 0; i < 3; ++i) { pub_speed->publish(s); rclcpp::sleep_for(50ms); }

  // ② 再发目标高度 0.10 m(= 100 mm)
  std_msgs::msg::Float32 p;
  p.data = 0.10f;
  for (int i = 0; i < 3; ++i) { pub_pos->publish(p); rclcpp::sleep_for(50ms); }

  // ③ 读 current_pose 判断到位,不要用固定延时
  auto deadline = node->now() + 20s;
  while (rclcpp::ok() && node->now() < deadline) {
    rclcpp::spin_some(node);
    if (!std::isnan(cur)) {
      RCLCPP_INFO(node->get_logger(), "当前高度 %.4f m", cur);
      if (std::fabs(cur - 0.10) < TOL_M) {
        RCLCPP_INFO(node->get_logger(), "到位");
        break;
      }
    }
    rclcpp::sleep_for(100ms);
  }

  rclcpp::shutdown();
  return 0;
}
/fd_robot/lifting_motor/position_set_speed_cmd 话题 · 发布
设置后续位置运动的速度。类型 std_msgs/Float32,来源包 fd_lifting_motor_pkg。
手册 6.4 节原文:「执行位置运动前,请先通过 position_set_speed_cmd 设置速度(单位 m/s,yako 同样以 0.2 m/s 为上限)。」
字段 侧 类型 取值 说明
data 消息 float32 m/s,yako 常用 ≤ 0.2 后续 position_cmd 使用的速度
这是「预设」不是「本次运动」。它设置的是后续 position_cmd 使用的速度。每次想改变速度,都要在发位置命令之前重新设置一次。

示例

① 命令行(ros2 CLI)

# 设为较慢的 0.03 m/s(适合首次调试、行程较小时)
ros2 topic pub -1 /fd_robot/lifting_motor/position_set_speed_cmd \
  std_msgs/msg/Float32 "{data: 0.03}"

# 之后再发位置命令即可沿用该速度
ros2 topic pub -1 /fd_robot/lifting_motor/position_cmd \
  std_msgs/msg/Float32 "{data: 0.15}"

② Python(rclpy)

# 示意代码:把"设速度"单独封装成一个方法,便于复用到每次位置运动
import rclpy
from rclpy.node import Node
from std_msgs.msg import Float32

class LiftSpeed(Node):
    def __init__(self):
        super().__init__('lift_speed')
        self.pub = self.create_publisher(
            Float32, '/fd_robot/lifting_motor/position_set_speed_cmd', 10)

    def set_speed(self, speed_mps):
        if speed_mps > 0.2:
            self.get_logger().warn(f'{speed_mps} m/s 超过 yako 约 0.2 m/s 的上限')
        msg = Float32()
        msg.data = float(speed_mps)
        for _ in range(3):
            self.pub.publish(msg)
            rclpy.spin_once(self, timeout_sec=0.05)
        self.get_logger().info(f'已设置后续位置运动速度 {speed_mps} m/s')

def main():
    rclpy.init()
    node = LiftSpeed()
    node.set_speed(0.03)      # 先用低速验证
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

③ C++(rclcpp)

#include <chrono>
#include <rclcpp/rclcpp.hpp>
#include "std_msgs/msg/float32.hpp"

using namespace std::chrono_literals;

int main(int argc, char** argv) {
  rclcpp::init(argc, argv);
  auto node = rclcpp::Node::make_shared("lift_speed");
  auto pub = node->create_publisher<std_msgs::msg::Float32>(
      "/fd_robot/lifting_motor/position_set_speed_cmd", 10);

  const double speed_mps = 0.03;      // yako 上限约 0.2 m/s
  if (speed_mps > 0.2) {
    RCLCPP_WARN(node->get_logger(), "速度超过 yako 约 0.2 m/s 的上限");
  }

  std_msgs::msg::Float32 msg;
  msg.data = static_cast<float>(speed_mps);
  for (int i = 0; i < 3; ++i) {
    pub->publish(msg);
    rclcpp::sleep_for(50ms);
  }
  RCLCPP_INFO(node->get_logger(), "已设置后续位置运动速度 %.2f m/s", speed_mps);

  rclcpp::shutdown();
  return 0;
}
/fd_robot/lifting_motor/velocity_cmd 话题 · 发布
速度控制(点动)。类型 std_msgs/Float32,来源包 fd_lifting_motor_pkg。
与 position_cmd 的「到某个高度」不同,这是按速度持续运动的点动模式,正负号表示方向。
字段 侧 类型 取值 说明
data 消息 float32 m/s,yako 约 ±0.2 点动速度,正负表示方向
点动没有「自动停」。手册没有说明该速度指令是否需要持续发布维持,也没有说明松开后是否自动停止。稳妥做法:先用很小的速度做短时点动验证方向,停止时显式发送 0.0 或 status='motor_stop',不要依赖"发一次就跑一下"的直觉。

示例

① 命令行(ros2 CLI)

# 正向点动 0.05 m/s(务必先确认方向再提速)
ros2 topic pub -1 /fd_robot/lifting_motor/velocity_cmd \
  std_msgs/msg/Float32 "{data: 0.05}"

# 反方向
ros2 topic pub -1 /fd_robot/lifting_motor/velocity_cmd \
  std_msgs/msg/Float32 "{data: -0.05}"

# 停止:速度归零
ros2 topic pub -1 /fd_robot/lifting_motor/velocity_cmd \
  std_msgs/msg/Float32 "{data: 0.0}"

# 或直接用字符串命令急停(见 4.5 节)
ros2 topic pub -1 /fd_robot/lifting_motor/status \
  std_msgs/msg/String "{data: 'motor_stop'}"

② Python(rclpy)

# 示意代码:定长点动 —— 按速度走指定时长后自动停止
import time
import rclpy
from rclpy.node import Node
from std_msgs.msg import Float32

class LiftJog(Node):
    def __init__(self):
        super().__init__('lift_jog')
        self.pub = self.create_publisher(
            Float32, '/fd_robot/lifting_motor/velocity_cmd', 10)

    def _pub(self, v):
        msg = Float32()
        msg.data = float(v)
        self.pub.publish(msg)

    def jog(self, speed_mps, duration_sec):
        self.get_logger().info(f'点动 {speed_mps} m/s,持续 {duration_sec} s')
        end = time.time() + duration_sec
        while time.time() < end:
            self._pub(speed_mps)      # 持续下发,避免指令丢失后停住
            rclpy.spin_once(self, timeout_sec=0.05)
        self._pub(0.0)                # 显式停止
        self.get_logger().info('已停止')

def main():
    rclpy.init()
    node = LiftJog()
    node.jog(0.05, 1.0)               # 缓升 1 秒
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

③ C++(rclcpp)

#include <chrono>
#include <rclcpp/rclcpp.hpp>
#include "std_msgs/msg/float32.hpp"

using namespace std::chrono_literals;

int main(int argc, char** argv) {
  rclcpp::init(argc, argv);
  auto node = rclcpp::Node::make_shared("lift_jog");
  auto pub = node->create_publisher<std_msgs::msg::Float32>(
      "/fd_robot/lifting_motor/velocity_cmd", 10);

  std_msgs::msg::Float32 cmd;
  cmd.data = 0.05f;                       // 正向 0.05 m/s
  auto stop_at = node->now() + 1s;        // 点动 1 秒
  while (rclcpp::ok() && node->now() < stop_at) {
    pub->publish(cmd);
    rclcpp::sleep_for(50ms);
  }

  cmd.data = 0.0f;                        // 显式停止
  for (int i = 0; i < 3; ++i) { pub->publish(cmd); rclcpp::sleep_for(50ms); }
  RCLCPP_INFO(node->get_logger(), "点动结束,已停止");

  rclcpp::shutdown();
  return 0;
}
/fd_robot/lifting_motor/home_cmd 话题 · 发布
回原点。类型 std_msgs/Empty,来源包 fd_lifting_motor_pkg。
空消息(无字段),发布一次即触发回原点动作。没有响应、没有进度反馈,是否到位仍需读 current_pose。
字段 侧 类型 取值 说明
(Empty,无字段) 消息 — — 发布即触发回原点
回原点与「设原点」是两件事。本接口是执行回原点动作;把当前位置标定为原点属于 status 话题的 set_oring 命令(见 4.5 节)。手册 6.4 节的参数表里只写了「回原点」三个字,未说明回原点的速度与到位判据,建议先用低速、空载验证。

示例

① 命令行(ros2 CLI)

# 触发回原点
ros2 topic pub -1 /fd_robot/lifting_motor/home_cmd std_msgs/msg/Empty "{}"

# 回原点期间持续观察高度变化
ros2 topic echo /fd_robot/lifting_motor/current_pose

② Python(rclpy)

# 示意代码:触发回原点,并等待高度稳定在原点附近
import rclpy
from rclpy.node import Node
from std_msgs.msg import Empty, Float32

ORIGIN_TOL_M = 0.005      # 工程建议值,手册未给原点判定容差

class LiftHome(Node):
    def __init__(self):
        super().__init__('lift_home')
        self.pub = self.create_publisher(
            Empty, '/fd_robot/lifting_motor/home_cmd', 10)
        self.sub = self.create_subscription(
            Float32, '/fd_robot/lifting_motor/current_pose', self.on_pose, 10)
        self.cur = None

    def on_pose(self, msg):
        self.cur = msg.data

    def go_home(self, timeout_sec=30.0):
        self.pub.publish(Empty())        # 空消息,发布即触发
        self.get_logger().info('已触发回原点')
        deadline = self.get_clock().now().nanoseconds + int(timeout_sec * 1e9)
        while rclpy.ok():
            rclpy.spin_once(self, timeout_sec=0.1)
            if self.cur is not None and abs(self.cur) < ORIGIN_TOL_M:
                self.get_logger().info(f'已回到原点附近:{self.cur:.4f} m')
                return True
            if self.get_clock().now().nanoseconds > deadline:
                self.get_logger().error('回原点超时')
                return False

def main():
    rclpy.init()
    node = LiftHome()
    node.go_home()
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

③ C++(rclcpp)

#include <chrono>
#include <cmath>
#include <rclcpp/rclcpp.hpp>
#include "std_msgs/msg/empty.hpp"
#include "std_msgs/msg/float32.hpp"

using namespace std::chrono_literals;
static constexpr double ORIGIN_TOL_M = 0.005;   // 工程建议值

int main(int argc, char** argv) {
  rclcpp::init(argc, argv);
  auto node = rclcpp::Node::make_shared("lift_home");
  auto pub = node->create_publisher<std_msgs::msg::Empty>(
      "/fd_robot/lifting_motor/home_cmd", 10);

  double cur = -1.0;
  auto sub = node->create_subscription<std_msgs::msg::Float32>(
      "/fd_robot/lifting_motor/current_pose", 10,
      [&cur](std_msgs::msg::Float32::SharedPtr msg) { cur = msg->data; });

  pub->publish(std_msgs::msg::Empty());
  RCLCPP_INFO(node->get_logger(), "已触发回原点");

  auto deadline = node->now() + 30s;
  while (rclcpp::ok() && node->now() < deadline) {
    rclcpp::spin_some(node);
    if (cur >= 0.0 && std::fabs(cur) < ORIGIN_TOL_M) {
      RCLCPP_INFO(node->get_logger(), "已回到原点附近:%.4f m", cur);
      break;
    }
    rclcpp::sleep_for(100ms);
  }

  rclcpp::shutdown();
  return 0;
}
/fd_robot/lifting_motor/status 话题 · 发布
字符串控制命令。类型 std_msgs/String,来源包 fd_lifting_motor_pkg。
手册 6.1 节把它归类为发布(命令输入),不是状态反馈。可用的命令词共 5 个。
字段 侧 类型 取值 说明
data 消息 string motor_stop 字符串控制命令
data 消息 string set_oring 字符串控制命令
data 消息 string reset 字符串控制命令
data 消息 string servo_disable 字符串控制命令
data 消息 string encoder_exception_clear 字符串控制命令
手册只列出了命令词,没有逐条给出语义说明。下表「字面含义」是按名称理解的推断,不是手册原文,使用前请向厂家确认或实测验证。注意命令词拼写是 set_oring(沿用旧拼写,不是 set_origin),照抄即可。
命令词 字面含义(推断,非手册原文) 典型使用场景
motor_stop 电机停止 需要立即停住时的第一选择(比把速度发成 0 更直接)
set_oring 设置原点(把当前位置标定为原点) 安装、机械调整后重新标定。与 home_cmd 的「回原点」不同
reset 复位 状态异常后的软件复位
servo_disable 伺服断使能 断使能后升降可能失去保持力,注意负载掉落风险
encoder_exception_clear 编码器异常清除 编码器报异常后的清除操作
servo_disable 会让伺服失去保持力。升降带负载时断使能,机械结构可能因重力下滑。断使能前请先把升降降到安全高度,或确认机械自锁能力。

示例

① 命令行(ros2 CLI)

# 立即停止
ros2 topic pub -1 /fd_robot/lifting_motor/status std_msgs/msg/String "{data: 'motor_stop'}"

# 把当前位置标定为原点
ros2 topic pub -1 /fd_robot/lifting_motor/status std_msgs/msg/String "{data: 'set_oring'}"

# 复位
ros2 topic pub -1 /fd_robot/lifting_motor/status std_msgs/msg/String "{data: 'reset'}"

# 清除编码器异常
ros2 topic pub -1 /fd_robot/lifting_motor/status \
  std_msgs/msg/String "{data: 'encoder_exception_clear'}"

② Python(rclpy)

# 示意代码:把 5 个命令词封装成常量,避免拼写错误
import rclpy
from rclpy.node import Node
from std_msgs.msg import String

CMD_MOTOR_STOP = 'motor_stop'
CMD_SET_ORIGIN = 'set_oring'          # 注意是 set_oring,不是 set_origin
CMD_RESET = 'reset'
CMD_SERVO_DISABLE = 'servo_disable'
CMD_ENCODER_CLEAR = 'encoder_exception_clear'
VALID_CMDS = {CMD_MOTOR_STOP, CMD_SET_ORIGIN, CMD_RESET,
              CMD_SERVO_DISABLE, CMD_ENCODER_CLEAR}

class LiftStatusCmd(Node):
    def __init__(self):
        super().__init__('lift_status_cmd')
        self.pub = self.create_publisher(
            String, '/fd_robot/lifting_motor/status', 10)

    def send(self, cmd):
        if cmd not in VALID_CMDS:
            self.get_logger().error(f'未知命令词 {cmd},已拒绝发送')
            return False
        msg = String()
        msg.data = cmd
        for _ in range(3):
            self.pub.publish(msg)
            rclpy.spin_once(self, timeout_sec=0.05)
        self.get_logger().info(f'已发送 {cmd}')
        return True

def main():
    rclpy.init()
    node = LiftStatusCmd()
    node.send(CMD_MOTOR_STOP)          # 立即停止
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

③ C++(rclcpp)

#include <chrono>
#include <string>
#include <rclcpp/rclcpp.hpp>
#include "std_msgs/msg/string.hpp"

using namespace std::chrono_literals;

int main(int argc, char** argv) {
  rclcpp::init(argc, argv);
  auto node = rclcpp::Node::make_shared("lift_status_cmd");
  auto pub = node->create_publisher<std_msgs::msg::String>(
      "/fd_robot/lifting_motor/status", 10);

  const std::string cmd = "motor_stop";   // 另有 set_oring / reset /
                                          // servo_disable / encoder_exception_clear
  std_msgs::msg::String msg;
  msg.data = cmd;
  for (int i = 0; i < 3; ++i) {
    pub->publish(msg);
    rclcpp::sleep_for(50ms);
  }
  RCLCPP_INFO(node->get_logger(), "已发送 %s", cmd.c_str());

  rclcpp::shutdown();
  return 0;
}
/fd_robot/lifting_motor/current_pose 话题 · 订阅
当前高度。类型 std_msgs/Float32,来源包 fd_lifting_motor_pkg。
周期发布,20 Hz。这是升降唯一的对外状态反馈,也是判断到位的唯一依据。
字段 侧 类型 取值 说明
data 消息 float32 m 当前高度;20 Hz
这个接口的方向是「订阅」。指由你的程序创建订阅去接收数据。ros2 topic echo 就是这个动作的命令行形式。

示例

① 命令行(ros2 CLI)

# 持续观察
ros2 topic echo /fd_robot/lifting_motor/current_pose

# 只取一次当前值
ros2 topic echo --once /fd_robot/lifting_motor/current_pose

# 确认发布频率(应为 20 Hz 左右)
ros2 topic hz /fd_robot/lifting_motor/current_pose

② Python(rclpy)

# 示意代码:订阅并做"到位判定 + 超时保护"
import rclpy
from rclpy.node import Node
from std_msgs.msg import Float32

class LiftMonitor(Node):
    def __init__(self):
        super().__init__('lift_monitor')
        self.sub = self.create_subscription(
            Float32, '/fd_robot/lifting_motor/current_pose', self.on_pose, 10)
        self.cur = None

    def on_pose(self, msg):
        self.cur = msg.data

    def wait_until(self, target_m, tol_m=0.005, timeout_sec=20.0):
        # tol_m 为工程建议值,手册未给出到位容差
        deadline = self.get_clock().now().nanoseconds + int(timeout_sec * 1e9)
        while rclpy.ok():
            rclpy.spin_once(self, timeout_sec=0.05)
            if self.cur is None:
                continue
            if abs(self.cur - target_m) < tol_m:
                self.get_logger().info(f'到位:{self.cur:.4f} m')
                return True
            if self.get_clock().now().nanoseconds > deadline:
                self.get_logger().error(f'超时,当前 {self.cur:.4f} m')
                return False

def main():
    rclpy.init()
    node = LiftMonitor()
    node.wait_until(0.10)
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

③ C++(rclcpp)

#include <chrono>
#include <cmath>
#include <rclcpp/rclcpp.hpp>
#include "std_msgs/msg/float32.hpp"

using namespace std::chrono_literals;

int main(int argc, char** argv) {
  rclcpp::init(argc, argv);
  auto node = rclcpp::Node::make_shared("lift_monitor");

  double cur = std::nan("");
  auto sub = node->create_subscription<std_msgs::msg::Float32>(
      "/fd_robot/lifting_motor/current_pose", 10,
      [&cur](std_msgs::msg::Float32::SharedPtr msg) {
        cur = msg->data;
        RCLCPP_INFO_THROTTLE(rclcpp::get_logger("lift_monitor"),
                             *rclcpp::Clock::make_shared(), 500,
                             "当前高度 %.4f m", cur);
      });

  // 阻塞等待:把 spin 放在循环里,避免订阅回调不执行
  rclcpp::spin(node);
  rclcpp::shutdown();
  return 0;
}
C++ 里最常犯的错:只在循环里 sleep 而不 spin。不 spin 就不会执行订阅回调,cur 永远是 NaN。要么像上面这样直接 rclcpp::spin(node) 阻塞等待,要么在循环里调用 rclcpp::spin_some(node)(见 4.1 节的完整闭环示例)。

五、系统内部接口

手册 6.1 / 6.2 节把下面 2 条标为系统内部接口:由驱动与诊断底层使用,仅供排查问题时参考,请勿与二次开发接口同时调用。手册 2.2 节也说明:系统内部接口不逐一展开字段,如需查看可对类型名执行 ros2 interface show。

/fd_robot/lifting_motor/motor_info 话题 · 系统内部
电机信息。类型 fd_msgs/MotorInfoMsg,来源包 fd_lifting_motor_pkg,20 Hz。
属于诊断/监控类数据,用于排查电机层问题(温度、电流、状态字等,字段以实际类型定义为准)。
项目 内容
类型 fd_msgs/MotorInfoMsg
方向(从程序角度) 订阅
频率 20 Hz
字段 手册未展开,用 ros2 interface show fd_msgs/msg/MotorInfoMsg 查看
用途 排查电机层异常时参考
# 查看字段定义
ros2 interface show fd_msgs/msg/MotorInfoMsg

# 观察电机信息
ros2 topic echo /fd_robot/lifting_motor/motor_info
/fd_robot/self_check/lifting_motor 服务 · 系统内部
升降自检。类型 fd_msgs/SelfCheckSrv,来源包 fd_lifting_motor_pkg。
手册 6.2 节原文:「本节服务均为系统内部接口。急停状态请订阅 /fd_robot/emergency_signal(见第十一章)。」

SelfCheckSrv 是各子系统自检的通用类型(手册 11.3 节给出字段):

字段 侧 类型 取值 说明
(无请求字段) 请求 — — 直接调用即可
name 响应 string 子系统名称 —
check_abnormal_info 响应 string[] 为空表示该项正常 非空时为异常说明
# 单独调用升降自检
ros2 service call /fd_robot/self_check/lifting_motor fd_msgs/srv/SelfCheckSrv "{}"

# 查看服务定义
ros2 interface show fd_msgs/srv/SelfCheckSrv
整机汇总自检可能超时,不代表升降有问题。手册附录明确:/fd_robot/self_check 汇总自检可能因调用现场不存在的子系统服务(/fd_robot/self_check/arm、/chassis)而超时。对失败项请调用对应子系统的自检服务,即本节的 /fd_robot/self_check/lifting_motor。

六、典型流程与完整示例

6.1 首次对接步骤

步骤 操作 目的 / 判据
1 source install/setup.bash;确认 ROS_DOMAIN_ID=25 能与机器人节点通信
2 ros2 topic list | grep lifting_motor 确认 6 条话题都在;一条都没有说明节点未启动或机型为 YD100
3 ros2 topic hz /fd_robot/lifting_motor/current_pose 确认反馈在发,约 20 Hz
4 订阅 /fd_robot/emergency_signal 急停保护先接上,再谈运动
5 确认无遥操作 / 其他控制源占用 手册 1.2 节第 6 条:同一执行器只能有一个控制来源
6 记录当前 current_pose 作为基线 后续判断运动方向与幅度
7 先 小速度 + 小位移 试一次 验证方向、速度、到位判定是否符合预期
8 确认无误后再投入实际使用 ——

6.2 位置运动闭环

升降没有 Action,闭环要自己搭。标准顺序只有三步:

顺序 动作 接口 要点
① 设置速度 position_set_speed_cmd 必须先于位置命令;单位 m/s,yako ≤ 0.2
② 发送目标高度 position_cmd 绝对高度,单位 m,0–0.7(超出限幅)
③ 读高度判断到位 current_pose 20 Hz;与目标比较容差,不要用固定 sleep
① position_set_speed_cmd(0.05 m/s)
▼
② position_cmd(0.10 m)
▼
升降按设定速度运动
▼
③ current_pose(20 Hz)→ |当前值 − 目标| < 容差 即到位

6.3 点动控制(velocity_cmd)

点动适合微调和示教前的粗定位:按住走、松开停。因为没有位置目标,程序需要自己做「走多久 / 走到哪停」。

写法 做法 适用
定长点动 按速度持续下发固定时长,到时发 0.0 已知大致速度,想要"走一小段"
阈值点动 持续下发速度,同时监听 current_pose,接近目标就停 更准确,推荐
纯点动 用户界面按住发速度、松开发 0.0 人工微调
点动要养成「显式停止」的习惯。停止方式推荐 velocity_cmd = 0.0,需要立即停住时用 status = 'motor_stop'。不要依赖"发一次速度就自动跑完停下"。

6.4 回原点

# ① 触发回原点(Empty 消息)
ros2 topic pub -1 /fd_robot/lifting_motor/home_cmd std_msgs/msg/Empty "{}"

# ② 观察高度变化,确认在动、方向正确
ros2 topic echo /fd_robot/lifting_motor/current_pose

# ③ 高度稳定在原点附近即完成(容差需自行确定,手册未给)
「回原点」(home_cmd)与「设原点」(status='set_oring')不要混淆。前者是去原点,后者是把当前位置标定为原点。装调后重新标定用后者,日常归位用前者。

6.5 停止 / 复位类命令对照

需求 推荐命令 说明
位置运动中停住 status = 'motor_stop' 最直接;比发速度 0 更明确
点动停住 velocity_cmd = 0.0 也可用 motor_stop
让运动失效 / 断保持力 status = 'servo_disable' 带负载时注意掉落风险
重新标定原点 status = 'set_oring' 拼写照抄,不是 set_origin
状态异常复位 status = 'reset' ——
编码器异常 status = 'encoder_exception_clear' ——
回到初始位置 home_cmd(Empty) 回原点动作
整机急停 订阅 /fd_robot/emergency_signal 不是本章接口;触发后升降进入安全状态

6.6 完整示例:Python 端到端

下面这份脚本把「前置检查 → 急停监听 → 设速度 → 发位置 → 等到位 → 停止」串成了一条完整链路,可直接作为对接起点(示意代码,请按实际机型调整容差与速度)。

# lift_demo.py —— 升降对接端到端示例(示意代码)
import rclpy
from rclpy.node import Node
from std_msgs.msg import Bool, Empty, Float32, String

TOL_M = 0.005           # 工程建议容差,手册未给出该值
TOPIC_POSE = '/fd_robot/lifting_motor/current_pose'
TOPIC_SET_SPEED = '/fd_robot/lifting_motor/position_set_speed_cmd'
TOPIC_POS = '/fd_robot/lifting_motor/position_cmd'
TOPIC_STATUS = '/fd_robot/lifting_motor/status'
TOPIC_ESTOP = '/fd_robot/emergency_signal'

class LiftClient(Node):
    def __init__(self):
        super().__init__('lift_demo')
        self.pub_speed = self.create_publisher(Float32, TOPIC_SET_SPEED, 10)
        self.pub_pos = self.create_publisher(Float32, TOPIC_POS, 10)
        self.pub_status = self.create_publisher(String, TOPIC_STATUS, 10)
        self.create_subscription(Float32, TOPIC_POSE, self.on_pose, 10)
        self.create_subscription(Bool, TOPIC_ESTOP, self.on_estop, 10)
        self.cur = None
        self.estop = False

    def on_pose(self, msg):
        self.cur = msg.data

    def on_estop(self, msg):
        self.estop = bool(msg.data)

    def wait_if_estop(self):
        while rclpy.ok() and self.estop:
            self.get_logger().warn('急停已触发,等待解除…')
            rclpy.spin_once(self, timeout_sec=0.2)

    def _pub_n(self, pub, value, n=3):
        msg = pub.msg_type()            # 按发布者类型构造消息
        msg.data = value
        for _ in range(n):
            pub.publish(msg)
            rclpy.spin_once(self, timeout_sec=0.05)

    def preflight(self):
        # 等一次 current_pose,确认节点在发数据
        for _ in range(50):
            rclpy.spin_once(self, timeout_sec=0.1)
            if self.cur is not None:
                self.get_logger().info(f'升降节点在线,当前高度 {self.cur:.4f} m')
                return True
        self.get_logger().error('收不到 current_pose,请检查 Modbus 升降节点 / lift_model')
        return False

    def move_to(self, height_m, speed_mps=0.05, timeout_sec=20.0):
        assert 0.0 <= height_m <= 0.7, '目标超出 0–0.7 m,会被限幅'
        assert 0.0 < speed_mps <= 0.2, 'yako 速度上限约 0.2 m/s'

        self.wait_if_estop()
        self._pub_n(self.pub_speed, speed_mps)     # ① 先设速度
        self._pub_n(self.pub_pos, height_m)        # ② 再发位置
        self.get_logger().info(f'目标 {height_m} m,速度 {speed_mps} m/s')

        deadline = self.get_clock().now().nanoseconds + int(timeout_sec * 1e9)
        while rclpy.ok():
            rclpy.spin_once(self, timeout_sec=0.05)
            if self.estop:
                self.get_logger().error('急停触发,停止等待')
                return False
            if self.cur is not None and abs(self.cur - height_m) < TOL_M:
                self.get_logger().info(f'到位:{self.cur:.4f} m')
                return True
            if self.get_clock().now().nanoseconds > deadline:
                self.get_logger().error(f'超时未到位,当前 {self.cur:.4f} m')
                return False

    def stop(self):
        self._pub_n(self.pub_status, 'motor_stop')  # 立即停止

def main():
    rclpy.init()
    node = LiftClient()
    try:
        if node.preflight():
            node.move_to(0.10, speed_mps=0.05)      # 升到 100 mm
            node.move_to(0.00, speed_mps=0.05)      # 再降回 0
    finally:
        node.stop()
        node.destroy_node()
        rclpy.shutdown()

if __name__ == '__main__':
    main()

6.7 完整示例:C++ 端到端

// lift_demo.cpp —— 升降对接端到端示例(示意代码)
#include <chrono>
#include <cmath>
#include <memory>
#include <rclcpp/rclcpp.hpp>
#include "std_msgs/msg/bool.hpp"
#include "std_msgs/msg/float32.hpp"
#include "std_msgs/msg/string.hpp"

using namespace std::chrono_literals;
static constexpr double TOL_M = 0.005;      // 工程建议容差,手册未给

class LiftClient : public rclcpp::Node {
 public:
  LiftClient() : Node("lift_demo") {
    pub_speed_ = create_publisher<std_msgs::msg::Float32>(
        "/fd_robot/lifting_motor/position_set_speed_cmd", 10);
    pub_pos_ = create_publisher<std_msgs::msg::Float32>(
        "/fd_robot/lifting_motor/position_cmd", 10);
    pub_status_ = create_publisher<std_msgs::msg::String>(
        "/fd_robot/lifting_motor/status", 10);

    sub_pose_ = create_subscription<std_msgs::msg::Float32>(
        "/fd_robot/lifting_motor/current_pose", 10,
        [this](std_msgs::msg::Float32::SharedPtr m) { cur_ = m->data; });
    sub_estop_ = create_subscription<std_msgs::msg::Bool>(
        "/fd_robot/emergency_signal", 10,
        [this](std_msgs::msg::Bool::SharedPtr m) { estop_ = m->data; });
  }

  bool preflight() {
    for (int i = 0; i < 50; ++i) {
      rclcpp::spin_some(this);
      if (!std::isnan(cur_)) {
        RCLCPP_INFO(get_logger(), "升降节点在线,当前高度 %.4f m", cur_);
        return true;
      }
      rclcpp::sleep_for(100ms);
    }
    RCLCPP_ERROR(get_logger(), "收不到 current_pose,请检查升降节点");
    return false;
  }

  bool move_to(double height_m, double speed_mps = 0.05,
               double timeout_sec = 20.0) {
    if (height_m < 0.0 || height_m > 0.7) {
      RCLCPP_WARN(get_logger(), "目标超出行程 0–0.7 m,会被限幅");
    }
    if (estop_) { RCLCPP_ERROR(get_logger(), "急停已触发,拒绝发送命令"); return false; }

    std_msgs::msg::Float32 s;
    s.data = static_cast<float>(speed_mps);
    for (int i = 0; i < 3; ++i) { pub_speed_->publish(s); rclcpp::sleep_for(50ms); }

    std_msgs::msg::Float32 p;
    p.data = static_cast<float>(height_m);
    for (int i = 0; i < 3; ++i) { pub_pos_->publish(p); rclcpp::sleep_for(50ms); }

    auto deadline = now() + rclcpp::Duration::from_seconds(timeout_sec);
    while (rclcpp::ok() && now() < deadline) {
      rclcpp::spin_some(this);
      if (estop_) { RCLCPP_ERROR(get_logger(), "急停触发,停止等待"); return false; }
      if (!std::isnan(cur_) && std::fabs(cur_ - height_m) < TOL_M) {
        RCLCPP_INFO(get_logger(), "到位:%.4f m", cur_);
        return true;
      }
      rclcpp::sleep_for(50ms);
    }
    RCLCPP_ERROR(get_logger(), "超时未到位,当前 %.4f m", cur_);
    return false;
  }

  void stop() {
    std_msgs::msg::String m;
    m.data = "motor_stop";
    for (int i = 0; i < 3; ++i) { pub_status_->publish(m); rclcpp::sleep_for(50ms); }
  }

 private:
  rclcpp::Publisher<std_msgs::msg::Float32>::SharedPtr pub_speed_, pub_pos_;
  rclcpp::Publisher<std_msgs::msg::String>::SharedPtr pub_status_;
  rclcpp::Subscription<std_msgs::msg::Float32>::SharedPtr sub_pose_;
  rclcpp::Subscription<std_msgs::msg::Bool>::SharedPtr sub_estop_;
  double cur_{std::nan("")};
  bool estop_{false};
};

int main(int argc, char** argv) {
  rclcpp::init(argc, argv);
  auto node = std::make_shared<LiftClient>();
  if (node->preflight()) {
    node->move_to(0.10, 0.05);      // 升到 100 mm
    node->move_to(0.00, 0.05);      // 降回 0
  }
  node->stop();
  rclcpp::shutdown();
  return 0;
}

6.8 急停与关机场景处理

信号 话题 处理建议
急停 /fd_robot/emergency_signal data=true 时立即停止发送任何运动命令;恢复后不要自动续跑,需人工确认
关机请求 /fd_robot/shutdown_signal 停止运动、保存状态;不要把发布关机信号当常规控制手段
急停后不要自动重发命令。手册 1.1 节说明:急停触发后升降等执行器会进入安全状态。"安全状态"下位置与使能情况由控制器决定,程序不应假设升降仍停在原位或仍处于使能状态,恢复后请重新读取 current_pose 再决定下一步。

七、注意事项与已知限制

7.1 硬性约束

① 单位是米,不是毫米。要 100 mm 发 0.10。0–0.7 m 之外的值会被静默限幅,调用方不会收到错误提示。只有 fd_api 的 lift_set_pos 用毫米。
② 位置运动前必须先设速度。手册 6.4 节:执行位置运动前,请先通过 position_set_speed_cmd 设置速度。顺序反了位置命令就缺少速度依据。
③ lift_model=yd100 不要用本章接口。手册 6.4 节原文。YD100 机型没有独立升降节点,升降由 5.3 节腰部接口控制。
④ 同一执行器只能有一个控制来源。遥操作(Pico)会话开启期间,升降可能正被身体控制节点驱动,此时程序下发命令会抢控制权。
⑤ 始终订阅急停。/fd_robot/emergency_signal;data=true 时停止一切运动命令。

7.2 已知限制表

下表前 5 条来自手册附录「已知限制」与第六章正文;第 6–9 条是本文在写示例过程中发现的手册未明确项,已逐条标注。

限制 说明 影响
position_cmd 单位 Float32 单位为米,限幅范围 0–0.7 m。不要按毫米发送(如 100);只有 fd_api.lift_set_pos 使用毫米 高
lift_model=mdx 配置中虽允许填写 mdx,但当前实现中除 leesn 外均按 yako 处理;请勿按独立的 mdx 协议对接 高
lift_model=yd100 无独立升降节点,本章 6 条话题均不适用;请改用 5.3 节 YD100 腰部接口 高
无 Action / 无运动状态 手册 6.3 节「动作:无」。没有 result_code、busy、REACHED,到位只能靠 current_pose 判断 中
限幅不报错 手册 2.2 节:限幅是「超出范围的值会被截断到边界值后继续执行,调用方通常不会收到错误提示」 中
status 命令词语义 手册未明确:6.1/6.4 节只列出了 5 个命令词,没有逐条语义说明(本文 4.5 节的解释为按名称推断) 中
velocity_cmd 停止行为 手册未明确:点动速度是否需要持续发布维持、松手是否自动停止,均未说明。建议显式停止 中
到位判定容差 手册未给出数值容差(原话只有"请通过 current_pose 判断是否到位")。示例中的 5 mm 为工程建议值 低
整机汇总自检 手册附录:/fd_robot/self_check 可能因调用不存在的子系统服务而超时;对失败项调用 /fd_robot/self_check/lifting_motor 低

7.3 排查速查表

现象 可能原因 处理
发位置命令不动 没先设速度;节点未启动;被遥操作占用 先补发 position_set_speed_cmd;查 ros2 topic list;确认无其他控制源
想要 100 mm,结果顶到最高 单位错,发了 100 而不是 0.10 改用米制:100 mm = 0.10
发命令没有任何反应、也不报错 话题没有订阅者,或目标值被限幅 ros2 topic info <话题> -v 看订阅者数量;核对取值范围
current_pose 收不到数据 节点未启动,或机型是 YD100 确认 lift_model;查节点是否运行
到位判定总是超时 容差设得太小;负载下存在静态偏差 适当放宽容差,并按机型实测重复定位精度
点动停不下来 只发了速度没发停止 发 velocity_cmd = 0.0 或 status = 'motor_stop'
找不到 lifting_motor 相关话题 节点未启动;或 lift_model=yd100 查配置与节点状态;YD100 转用腰部接口

7.4 通用提醒

先确认接口可见再发命令。用 ros2 topic list、ros2 topic hz 确认接口存在且有反馈;驱动节点未启动时接口不会有任何响应。
内部接口只用于排查。5 章标注的「系统内部接口」(motor_info、self_check/lifting_motor)仅供排查问题参考,请勿与二次开发接口同时调用。
与 fd_api 不要混用。两套接口层级不同、单位也不同(fd_api 升降用毫米)。同时使用会抢控制权。相关对照见 1.1 节,另一层级的完整说明见 《fd_api 升降机构说明》。
YD100 机型请转另一份文档。采用 YD100 的机型升降由腰部那组接口控制,完整说明见 《天链机器人腰部接口说明》。
升降 ROS 接口说明 — 天链机器人 © 2026