底盘接口说明

一、概述

底盘是机器人的移动平台。它和腰部那种"一外设多套互斥驱动栈"的情况不同:手册第七章第一句就写明——slamtec、woosh、yunji 三种底盘使用相同的 ROS 话题名。

也就是说:接口不用按机型分栈,一套代码三种底盘通用。真正需要分机型的是能力,其中最关键的一条是:

导航方式 nav_mode slamtec woosh yunji
按位姿导航(POSE) 1 ✔ 支持 ✔ 支持 ✔ 支持
按点位导航(MARK) 0 ✗ 不支持,nav_status 会立即报告 failed ✔ 支持 ✔ 支持

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

二次开发接口 4 条
2 发布(控制)+ 2 订阅(反馈)
没有服务、没有动作
手册 7.2 节原文「无」
两种运动方式
定时速度运动 / 导航到目标
每接口配示例
命令行 / Python / C++ 各一段

对接底盘真正要小心的是三件事:① slamtec 不支持按点位导航,发 nav_mode=0 会立刻 failed;② nav_status 的三个标志可能短暂同时为 true,不能用"当前是否为 true"判断结果,要用 false→true 的跳变时刻;③ 三种底盘共用同名话题,所以"接口能调通"不代表"该功能这台机器支持"。

接口挂在 /fd_robot/chassis/* 下,全部由 fd_chassis_http_client 提供——从包名可见,该 ROS 节点是以 HTTP 客户端方式对接三种厂家底盘的。未启动底盘驱动时,本章接口不可用。

1.1 与 fd_api 底盘接口的关系

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

对比项 fd_api 封装层 本文(ROS 2 原生接口)
定时运动 chassis_move(direction, duration_ms, linear_speed, angular_speed) 发布 /fd_robot/chassis/move_control(ChassisMoveMsg,四个字段一一对应)
导航到点位 chassis_goto_mark(mark_no, task_type) goto_control + nav_mode=0 + mark_no
按位姿导航 chassis_goto_pose(x, y, yaw, task_type) goto_control + nav_mode=1 + x/y/yaw
取消导航 chassis_cancel_nav() goto_control + request_type=1
等待到达 chassis_wait_arrived(timeout_sec) 无此接口,需自己订阅 nav_status 做跳变判定
导航状态 chassis_nav_status() 订阅 /fd_robot/chassis/nav_status
fd_api 的 chassis_wait_arrived() 在 ROS 层没有对应接口。它是封装层在 nav_status 之上包出来的阻塞等待。直接用 ROS 话题时,需要自己实现"订阅 + 判定跳变 + 超时"这套逻辑——本文 6.7 / 6.8 节的端到端示例给的就是这套实现。
不要同时用两套接口控制底盘。手册 1.2 节接入步骤第 6 条明确:同一执行器同一时间只能有一个控制来源。fd_api 与 ROS 2 接口同时下发会互相抢控制权。

1.2 底盘在整机中的位置

二次开发程序 / 导航调度 / 上层应用
▼
ROS 2 接口层 /fd_robot/chassis/*
▼
fd_chassis_http_client(三种底盘统一适配节点)
▼
slamtec / woosh / yunji 底盘本体(HTTP 接口)

底盘和升降一样,没有应用层服务、没有动作接口——程序直接发布话题,节点转发给底盘厂家接口。因此本章全部接口都是话题。

底盘运动会干扰整机空闲判断。手册附录明确:/fd_robot/robot_idle_status「底盘运动可能未计入空闲判断」,处理方式是「请结合底盘 nav_status 或速度判断」。详见 6.9 节。

二、运行前提与约定

2.1 环境与节点前提

项目 要求 说明
ROS 版本 ROS 2 Humble 手册适用软件版本 FDRS-V04-02-00
环境加载 source install/setup.bash 调用接口前先执行
域 ID ROS_DOMAIN_ID=25 手册 1.1 节明确要求确认
驱动节点 底盘驱动(HTTP 客户端)已启动 对应 chassis_model = slamtec / woosh / yunji
未启动底盘驱动时,本章接口不可用。手册第七章原文。届时话题可能列不出来,或列出来但没有任何反馈。

2.2 如何确认接口可用

# 1) 确认能列出底盘话题
ros2 topic list | grep -E 'chassis|slamtec'

# 期望看到(二次开发 4 条)
# /fd_robot/chassis/current_pose
# /fd_robot/chassis/goto_control
# /fd_robot/chassis/move_control
# /fd_robot/chassis/nav_status

# 2) 确认位姿反馈在发布(应约 5 Hz)
ros2 topic hz /fd_robot/chassis/current_pose

# 3) 确认消息类型与订阅者数量
ros2 topic info /fd_robot/chassis/move_control -v

# 4) 查看两个消息类型的完整定义
ros2 interface show fd_msgs/msg/ChassisMoveMsg
ros2 interface show fd_msgs/msg/ChassisGotoMsg
nav_status 是按需发布的。手册 2.1 节说明:「未注明频率的话题为按需发布(由命令或事件触发)」。7.1 节的表里只有 current_pose 标了 5 Hz,nav_status 没有标频率——所以不发导航命令时它可能长时间没有数据,这是正常的,不要误判为节点挂了。

2.3 单位与取值约定

接口 字段 单位 取值范围
move_control direction — 0 前进 / 1 后退 / 2 右转 / 3 左转;其他值无效
duration 毫秒 到时自动停止
linear_speed m/s ≤ 0 时使用节点默认值(通常约 0.2)
angular_speed rad/s ≤ 0 时使用节点默认值(通常约 0.5)
goto_control x / y 米(m) 地图坐标系,仅 nav_mode=1 时使用
yaw 弧度(rad) 地图坐标系,仅 nav_mode=1 时使用
current_pose position / orientation m / 四元数 地图坐标系下平面位姿;5 Hz
注意 duration 是毫秒,而位姿单位是米。手册 1.1 节专门列了这条易混单位:「底盘:duration 单位为毫秒;linear_speed 单位为 m/s,angular_speed 单位为 rad/s。」duration: 1000 是 1 秒,不是 1000 秒。

2.4 方向与导航模式

枚举 取值 含义
direction 0 / 1 / 2 / 3 前进 / 后退 / 右转 / 左转(注意 2 是右、3 是左)
nav_mode 0 / 1 0 = MARK(按点位,slamtec 不支持);1 = POSE(按位姿,三种都支持)
request_type 0 / 1 0 = goto(开始导航);1 = cancel(取消导航)
direction 的转向定义:手册原文「2 右转,3 左转」。转向类命令(2 / 3)通常只用 angular_speed,linear_speed 置 0 即可。

2.5 如何判断「到位」

手册 1.2 节接入步骤第 4 条给出标准做法:发布 goto_control 后,订阅 nav_status,根据 arrived 或 failed 判断导航结果。

但 7.3 节紧接着给了一个非常关键的细节:

手册原文:ChassisNavStatusMsg——active 表示导航进行中,arrived 表示已到达,failed 表示导航失败。三个标志可能短暂同时为 true,请以 failed 或 arrived 由 false 变为 true 的时刻作为判定依据。

所以要判断的不是「arrived 现在是 true 吗」,而是「arrived 从 false 变成 true 的那一刻」。正确与错误写法对照:

# ✗ 错误:只看当前值,三个标志同时为 true 时会误判
if nav_status.arrived:
    print('到了')

# ✓ 正确:检测 false → true 的跳变,且优先判定 failed
if not prev_failed and cur_failed:
    print('导航失败')
elif not prev_arrived and cur_arrived:
    print('已到达')
另外建议加超时保护。手册没有给出导航的超时约定(fd_api 的 chassis_wait_arrived 默认 600 秒仅供参考,属另一套封装)。自己的程序里应设一个上限,超时后发 request_type=1 取消导航。

2.6 安全前提与已知交互

同一底盘同一时间只能有一个控制来源。手册 1.2 节第 6 条:Pico 遥操作、/teleop/joint_targets_* 或底层 /tl_control/* 正在控制时,不要再通过本手册接口下发命令。底盘尤其容易被人工遥控同时操作。
底盘会发布自己的关机信号。手册 7.1 节系统内部接口表列出 /fd_robot/shutdown_signal(chassis_model=woosh,由 woosh 电源键触发)。注意它与第十一章 fd_power_board_pkg 的同名话题是两回事——订阅时要留意发布者是谁。
整机空闲状态不可只信 idle 字段。手册附录:/fd_robot/robot_idle_status「底盘运动可能未计入空闲判断」,需结合 nav_status 或速度判断(见 6.9 节)。

三、接口总览

3.1 二次开发接口(4 条)

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

接口 类型 方向 用途 配置条件
/fd_robot/chassis/move_control fd_msgs/ChassisMoveMsg 发布 按方向运动指定时长,到时自动停止 底盘驱动已启动
/fd_robot/chassis/goto_control fd_msgs/ChassisGotoMsg 发布 开始或取消导航。按点位导航(nav_mode=0)仅 woosh、yunji 支持 底盘驱动已启动
/fd_robot/chassis/nav_status fd_msgs/ChassisNavStatusMsg 订阅 导航状态:active / arrived / failed 底盘驱动已启动
/fd_robot/chassis/current_pose geometry_msgs/Pose 订阅 地图坐标系下的平面位姿;5 Hz 底盘驱动已启动

3.2 系统内部接口(4 条)

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

接口 类型 方向 用途 配置条件
/fd_robot/chassis_status fd_msgs/ChassisStatusMsg 订阅 位姿、chassis_stat 和 error_msg;1 Hz。健康状态字段主要在 slamtec 上有效 底盘驱动已启动
/fd_robot/slamtec/battery_info fd_msgs/SlamTecBatteryMsg 订阅 电量、充电和对接状态;2 Hz。三种底盘都会发布 底盘驱动已启动
/fd_robot/slamtec/run_status std_msgs/Bool 订阅 底盘是否在运动(速度阈值约 0.02);1 Hz 底盘驱动已启动
/fd_robot/shutdown_signal std_msgs/Bool 订阅 底盘侧关机信号(woosh 电源键触发) chassis_model=woosh

3.3 关于服务与动作

手册 7.2 节原文:「服务 / 动作:无。」底盘既没有 Service 也没有 Action。
因此:没有 result_code、没有 goal / feedback,导航完成只能靠订阅 nav_status 判定(见 2.5 节);取消导航也不是服务调用,而是往同一个 goto_control 话题发 request_type=1。

四、话题接口详解

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

关于 QoS:手册第七章没有对底盘这组话题标注 QoS 要求,使用默认 QoS(RELIABLE / VOLATILE,深度 10)即可。这与腰部 YD100 那三个 BEST_EFFORT 状态话题不同,不要照搬那边的写法。
示例均为示意代码,字段名与类型请以 ros2 interface show 的实际输出为准。三种底盘共用同名话题,示例代码通用,但能力差异(尤其 nav_mode=0)需要自行按机型规避。
/fd_robot/chassis/move_control 话题 · 发布
按方向运动指定时长,到时自动停止。类型 fd_msgs/ChassisMoveMsg,来源包 fd_chassis_http_client,配置条件「底盘驱动已启动」。
这是开环定时运动:不指定目标点,只指定方向和持续时间。适合小幅调整位置/朝向,或作为导航失败后的脱困手段。
字段 侧 类型 取值 说明
direction 消息 uint32 0 前进 / 1 后退 / 2 右转 / 3 左转 其他值无效
duration 消息 uint32 毫秒 到时自动停止
linear_speed 消息 float32 m/s;≤ 0 时使用节点默认值(通常约 0.2) 前进 / 后退速度
angular_speed 消息 float32 rad/s;≤ 0 时使用节点默认值(通常约 0.5) 旋转速度
duration=0 且速度均接近 0 时,手册说「部分底盘会将其视为停止命令」。注意是部分底盘——不能当作跨机型的通用急停手段。需要可靠停止时,建议改为发送一个极短的相反方向运动,或直接使用底盘自身的急停/电源键。请勿依赖该行为做安全停机。

示例

① 命令行(ros2 CLI)

# 前进 1 秒,线速度 0.2 m/s
ros2 topic pub -1 /fd_robot/chassis/move_control fd_msgs/msg/ChassisMoveMsg \
  "{direction: 0, duration: 1000, linear_speed: 0.2, angular_speed: 0.0}"

# 原地右转 0.5 秒(direction=2 是右转、3 是左转)
ros2 topic pub -1 /fd_robot/chassis/move_control fd_msgs/msg/ChassisMoveMsg \
  "{direction: 2, duration: 500, linear_speed: 0.0, angular_speed: 0.5}"

# 速度填 0,使用节点默认值(约 0.2 m/s)
ros2 topic pub -1 /fd_robot/chassis/move_control fd_msgs/msg/ChassisMoveMsg \
  "{direction: 0, duration: 1000, linear_speed: 0.0, angular_speed: 0.0}"

# 查看消息定义
ros2 interface show fd_msgs/msg/ChassisMoveMsg

② Python(rclpy)

# 示意代码:定时速度运动,并在 duration 内等待、结束后读位姿
import time
import rclpy
from rclpy.node import Node
from fd_msgs.msg import ChassisMoveMsg
from geometry_msgs.msg import Pose

DIR_FORWARD, DIR_BACKWARD, DIR_RIGHT, DIR_LEFT = 0, 1, 2, 3

class ChassisMove(Node):
    def __init__(self):
        super().__init__('chassis_move')
        self.pub = self.create_publisher(
            ChassisMoveMsg, '/fd_robot/chassis/move_control', 10)
        self.sub = self.create_subscription(
            Pose, '/fd_robot/chassis/current_pose', self.on_pose, 10)
        self.pose = None

    def on_pose(self, msg):
        self.pose = msg

    def move(self, direction, duration_ms, linear_speed=0.0, angular_speed=0.0):
        msg = ChassisMoveMsg()
        msg.direction = int(direction)
        msg.duration = int(duration_ms)          # 毫秒
        msg.linear_speed = float(linear_speed)   # ≤0 用节点默认值
        msg.angular_speed = float(angular_speed)
        for _ in range(3):
            self.pub.publish(msg)
            rclpy.spin_once(self, timeout_sec=0.05)
        self.get_logger().info(
            f'已下发 direction={direction} duration={duration_ms} ms,到时自动停止')

        # 手册:到时自动停止。等待期内保持 spin,便于接收位姿
        end = time.time() + duration_ms / 1000.0
        while time.time() < end and rclpy.ok():
            rclpy.spin_once(self, timeout_sec=0.05)

        if self.pose is not None:
            p = self.pose.position
            self.get_logger().info(f'运动结束,位姿 x={p.x:.3f} y={p.y:.3f}')

def main():
    rclpy.init()
    node = ChassisMove()
    node.move(DIR_FORWARD, 1000, linear_speed=0.2)   # 前进 1 秒
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

③ C++(rclcpp)

#include <chrono>
#include <rclcpp/rclcpp.hpp>
#include "fd_msgs/msg/chassis_move_msg.hpp"
#include "geometry_msgs/msg/pose.hpp"

using namespace std::chrono_literals;

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

  auto pub = node->create_publisher<fd_msgs::msg::ChassisMoveMsg>(
      "/fd_robot/chassis/move_control", 10);

  auto sub = node->create_subscription<geometry_msgs::msg::Pose>(
      "/fd_robot/chassis/current_pose", 10,
      [](geometry_msgs::msg::Pose::SharedPtr m) {
        RCLCPP_INFO_THROTTLE(rclcpp::get_logger("chassis_move"),
                             *rclcpp::Clock::make_shared(), 1000,
                             "pose x=%.3f y=%.3f", m->position.x, m->position.y);
      });

  fd_msgs::msg::ChassisMoveMsg cmd;
  cmd.direction = 0;            // 0 前进 / 1 后退 / 2 右转 / 3 左转
  cmd.duration = 1000;          // 毫秒,到时自动停止
  cmd.linear_speed = 0.2f;      // m/s,≤0 用节点默认值
  cmd.angular_speed = 0.0f;     // rad/s

  for (int i = 0; i < 3; ++i) { pub->publish(cmd); rclcpp::sleep_for(50ms); }
  RCLCPP_INFO(node->get_logger(), "已下发前进 1000 ms");

  // duration 内保持 spin,便于接收位姿反馈
  auto end = node->now() + 1s;
  while (rclcpp::ok() && node->now() < end) {
    rclcpp::spin_some(node);
    rclcpp::sleep_for(50ms);
  }

  rclcpp::shutdown();
  return 0;
}
/fd_robot/chassis/goto_control 话题 · 发布
开始或取消导航。类型 fd_msgs/ChassisGotoMsg,来源包 fd_chassis_http_client。
同一个话题承担三种操作:按位姿导航(nav_mode=1)、按点位导航(nav_mode=0)、取消导航(request_type=1)。靠字段组合区分。
字段 侧 类型 取值 说明
nav_mode 消息 uint8 0=MARK(点位),1=POSE(位姿) slamtec 不支持 0,会立即返回 failed
request_type 消息 uint8 0=goto(开始),1=cancel(取消) —
mark_no 消息 string 点位号,如 '1' 仅在 nav_mode=0 时使用(woosh / yunji)
x / y / yaw 消息 float64 地图坐标系,m / rad 仅在 nav_mode=1 时使用
task_type / task_id / direction / task_type_no 消息 uint32 厂家扩展字段 二次开发时保持 0 即可
取消导航不是"停止运动"的万能键。手册只说明 request_type=1 用于取消导航。如果是用 move_control 发起的定时运动,它本来就会"到时自动停止",不需要也无法用 goto_control 取消。

示例

① 命令行(ros2 CLI)

# ① 按位姿导航(三种底盘都支持)
ros2 topic pub -1 /fd_robot/chassis/goto_control fd_msgs/msg/ChassisGotoMsg \
  "{nav_mode: 1, request_type: 0, x: 1.0, y: 0.0, yaw: 0.0}"

# ② 按点位导航(仅 woosh / yunji;slamtec 会立即 failed)
ros2 topic pub -1 /fd_robot/chassis/goto_control fd_msgs/msg/ChassisGotoMsg \
  "{nav_mode: 0, request_type: 0, mark_no: '1'}"

# ③ 取消导航
ros2 topic pub -1 /fd_robot/chassis/goto_control fd_msgs/msg/ChassisGotoMsg \
  "{nav_mode: 1, request_type: 1}"

# 查看消息定义
ros2 interface show fd_msgs/msg/ChassisGotoMsg

② Python(rclpy)

# 示意代码:三种操作各封装一个方法;同时订阅 nav_status 打印状态跳变
import rclpy
from rclpy.node import Node
from fd_msgs.msg import ChassisGotoMsg, ChassisNavStatusMsg

class ChassisGoto(Node):
    def __init__(self):
        super().__init__('chassis_goto')
        self.pub = self.create_publisher(
            ChassisGotoMsg, '/fd_robot/chassis/goto_control', 10)
        self.sub = self.create_subscription(
            ChassisNavStatusMsg, '/fd_robot/chassis/nav_status',
            self.on_status, 10)
        self.prev = (False, False, False)

    def on_status(self, msg):
        cur = (bool(msg.active), bool(msg.arrived), bool(msg.failed))
        # 手册:三个标志可能短暂同时为 true,以 false→true 的跳变时刻为准
        if cur[2] and not self.prev[2]:
            self.get_logger().warn('nav_status: failed 触发')
        elif cur[1] and not self.prev[1]:
            self.get_logger().info('nav_status: arrived 触发')
        self.prev = cur

    def _send(self, msg):
        for _ in range(3):
            self.pub.publish(msg)
            rclpy.spin_once(self, timeout_sec=0.05)

    def goto_pose(self, x, y, yaw, cancel=False):
        msg = ChassisGotoMsg()
        msg.nav_mode = 1                      # POSE,三种底盘都支持
        msg.request_type = 1 if cancel else 0
        msg.x = float(x); msg.y = float(y); msg.yaw = float(yaw)
        self._send(msg)
        self.get_logger().info(
            f"{'取消' if cancel else '开始'}位姿导航 x={x} y={y} yaw={yaw}")

    def goto_mark(self, mark_no):
        msg = ChassisGotoMsg()
        msg.nav_mode = 0                      # MARK,仅 woosh / yunji
        msg.request_type = 0
        msg.mark_no = str(mark_no)
        self._send(msg)
        self.get_logger().info(f'开始点位导航 mark_no={mark_no}')

def main():
    rclpy.init()
    node = ChassisGoto()
    node.goto_pose(1.0, 0.0, 0.0)             # 去 (1.0, 0.0) 朝向 0
    # 需要取消时:node.goto_pose(0, 0, 0, cancel=True)
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

③ C++(rclcpp)

#include <chrono>
#include <rclcpp/rclcpp.hpp>
#include "fd_msgs/msg/chassis_goto_msg.hpp"

using namespace std::chrono_literals;

int main(int argc, char** argv) {
  rclcpp::init(argc, argv);
  auto node = rclcpp::Node::make_shared("chassis_goto");
  auto pub = node->create_publisher<fd_msgs::msg::ChassisGotoMsg>(
      "/fd_robot/chassis/goto_control", 10);

  // 按位姿导航(nav_mode=1,三种底盘均支持)
  fd_msgs::msg::ChassisGotoMsg goal;
  goal.nav_mode = 1;
  goal.request_type = 0;        // 0 开始 / 1 取消
  goal.x = 1.0;                 // m,地图坐标系
  goal.y = 0.0;
  goal.yaw = 0.0;               // rad
  // task_type / task_id / direction / task_type_no 二次开发保持 0 即可

  for (int i = 0; i < 3; ++i) { pub->publish(goal); rclcpp::sleep_for(50ms); }
  RCLCPP_INFO(node->get_logger(), "已下发位姿导航 (1.0, 0.0, 0.0)");

  // 需要取消时:
  // fd_msgs::msg::ChassisGotoMsg cancel;
  // cancel.nav_mode = 1;
  // cancel.request_type = 1;
  // pub->publish(cancel);

  rclcpp::shutdown();
  return 0;
}
/fd_robot/chassis/nav_status 话题 · 订阅
导航状态。类型 fd_msgs/ChassisNavStatusMsg,来源包 fd_chassis_http_client。
手册 7.1 节说明:「导航状态:active / arrived / failed」。这是判断导航结果的唯一依据。
字段 侧 类型 取值 说明
active 消息 — true / false 导航进行中
arrived 消息 — true / false 已到达
failed 消息 — true / false 导航失败
手册 7.3 节只解释了这三个标志的含义,没有给出字段类型和完整字段表。上表「类型」列因此留空。消息里是否还有其他字段(例如错误码),请用 ros2 interface show fd_msgs/msg/ChassisNavStatusMsg 自行确认。另外,nav_status 手册未标注发布频率,按 2.1 节约定属于按需发布——没有导航事件时可能长时间无数据。

示例

① 命令行(ros2 CLI)

# 持续观察导航状态(发导航命令后才有数据)
ros2 topic echo /fd_robot/chassis/nav_status

# 只取一次
ros2 topic echo --once /fd_robot/chassis/nav_status

# 查看消息定义(确认是否有手册未列出的字段)
ros2 interface show fd_msgs/msg/ChassisNavStatusMsg

# 典型联合调试:先发导航,再看状态
ros2 topic pub -1 /fd_robot/chassis/goto_control fd_msgs/msg/ChassisGotoMsg \
  "{nav_mode: 1, request_type: 0, x: 1.0, y: 0.0, yaw: 0.0}"
ros2 topic echo /fd_robot/chassis/nav_status

② Python(rclpy)

# 示意代码:状态机 + 跳变判定 + 超时保护
import rclpy
from rclpy.node import Node
from fd_msgs.msg import ChassisNavStatusMsg

class NavWatcher(Node):
    def __init__(self):
        super().__init__('nav_watcher')
        self.sub = self.create_subscription(
            ChassisNavStatusMsg, '/fd_robot/chassis/nav_status',
            self.on_status, 10)
        self.prev = (False, False, False)
        self.result = None          # 'arrived' / 'failed' / None

    def on_status(self, msg):
        active, arrived, failed = bool(msg.active), bool(msg.arrived), bool(msg.failed)
        # 手册 7.3:三个标志可能短暂同时为 true,
        # 以 failed 或 arrived 由 false 变为 true 的时刻作为判定依据
        if failed and not self.prev[2]:
            self.get_logger().warn('判定:导航失败 (failed 由 false→true)')
            self.result = 'failed'
        elif arrived and not self.prev[1]:
            self.get_logger().info('判定:已到达 (arrived 由 false→true)')
            self.result = 'arrived'
        elif active:
            self.get_logger().debug('导航进行中')
        self.prev = (active, arrived, failed)

    def wait(self, timeout_sec=120.0):
        deadline = self.get_clock().now().nanoseconds + int(timeout_sec * 1e9)
        while rclpy.ok() and self.result is None:
            rclpy.spin_once(self, timeout_sec=0.1)
            if self.get_clock().now().nanoseconds > deadline:
                self.get_logger().error('等待导航结果超时')
                return None
        return self.result

def main():
    rclpy.init()
    node = NavWatcher()
    result = node.wait(120.0)
    node.get_logger().info(f'导航结果:{result}')
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

③ C++(rclcpp)

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

using namespace std::chrono_literals;

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

  bool prev_arrived = false, prev_failed = false;
  std::string result = "none";

  auto sub = node->create_subscription<fd_msgs::msg::ChassisNavStatusMsg>(
      "/fd_robot/chassis/nav_status", 10,
      [&](fd_msgs::msg::ChassisNavStatusMsg::SharedPtr m) {
        // 手册 7.3:三个标志可能短暂同时为 true,
        // 以 failed 或 arrived 由 false 变为 true 的时刻作为判定依据
        if (m->failed && !prev_failed) {
          RCLCPP_WARN(rclcpp::get_logger("nav_watcher"),
                      "判定:导航失败 (failed false->true)");
          result = "failed";
        } else if (m->arrived && !prev_arrived) {
          RCLCPP_INFO(rclcpp::get_logger("nav_watcher"),
                      "判定:已到达 (arrived false->true)");
          result = "arrived";
        }
        prev_arrived = m->arrived;
        prev_failed = m->failed;
      });

  auto deadline = node->now() + 120s;      // 自行设置超时,手册未给导航超时约定
  while (rclcpp::ok() && node->now() < deadline && result == "none") {
    rclcpp::spin_some(node);
    rclcpp::sleep_for(100ms);
  }
  RCLCPP_INFO(node->get_logger(), "导航结果:%s", result.c_str());

  rclcpp::shutdown();
  return 0;
}
/fd_robot/chassis/current_pose 话题 · 订阅
地图坐标系下的平面位姿。类型 geometry_msgs/Pose,来源包 fd_chassis_http_client。
周期发布,5 Hz。这是底盘唯一的持续位姿反馈,可用于画轨迹、判断是否真的在动、以及导航失败后的现场确认。
字段 侧 类型 取值 说明
position.x / position.y 消息 float64 米(m) 地图坐标系下的平面位置
position.z 消息 float64 — 手册称「平面位姿」,z 是否有效手册未说明
orientation 消息 geometry_msgs/Quaternion 四元数 朝向;平面场景下主要用 yaw 分量
手册未给出 z / roll / pitch 是否有效,也未给出 yaw 的提取方式。下面示例用标准四元数→yaw 公式换算(yaw = atan2(2(wz+xy), 1−2(y²+z²))),这是通用数学关系,不是手册内容。

示例

① 命令行(ros2 CLI)

# 持续观察位姿(应约 5 Hz)
ros2 topic echo /fd_robot/chassis/current_pose

# 只取一次当前位置
ros2 topic echo --once /fd_robot/chassis/current_pose

# 确认发布频率
ros2 topic hz /fd_robot/chassis/current_pose

② Python(rclpy)

# 示意代码:订阅位姿并换算 yaw(弧度)
import math
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Pose

class PoseMonitor(Node):
    def __init__(self):
        super().__init__('pose_monitor')
        self.sub = self.create_subscription(
            Pose, '/fd_robot/chassis/current_pose', self.on_pose, 10)
        self.pose = None

    @staticmethod
    def quat_to_yaw(q):
        # 标准四元数 → yaw 换算(通用数学关系,手册未给出)
        return math.atan2(2.0 * (q.w * q.z + q.x * q.y),
                          1.0 - 2.0 * (q.y * q.y + q.z * q.z))

    def on_pose(self, msg):
        self.pose = msg
        yaw = self.quat_to_yaw(msg.orientation)
        self.get_logger().info(
            f'x={msg.position.x:.3f} y={msg.position.y:.3f} '
            f'yaw={yaw:.3f} rad ({math.degrees(yaw):.1f}°)')

def main():
    rclpy.init()
    node = PoseMonitor()
    rclpy.spin(node)          # 持续接收
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

③ C++(rclcpp)

#include <cmath>
#include <rclcpp/rclcpp.hpp>
#include "geometry_msgs/msg/pose.hpp"

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

  auto sub = node->create_subscription<geometry_msgs::msg::Pose>(
      "/fd_robot/chassis/current_pose", 10,
      [](geometry_msgs::msg::Pose::SharedPtr m) {
        // 标准四元数 → yaw(通用数学关系,手册未给出)
        const auto& q = m->orientation;
        double yaw = std::atan2(2.0 * (q.w * q.z + q.x * q.y),
                                1.0 - 2.0 * (q.y * q.y + q.z * q.z));
        RCLCPP_INFO(rclcpp::get_logger("pose_monitor"),
                    "x=%.3f y=%.3f yaw=%.3f rad (%.1f deg)",
                    m->position.x, m->position.y, yaw, yaw * 180.0 / M_PI);
      });

  rclcpp::spin(node);
  rclcpp::shutdown();
  return 0;
}
C++ 里最常犯的错:只在循环里 sleep 而不 spin。不 spin 就不会执行订阅回调,位姿永远是空的。持续监听直接用 rclcpp::spin(node);需要与其它逻辑并行时,在循环里调用 rclcpp::spin_some(node)(见 4.1 节与 6.8 节的写法)。

五、系统内部接口

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

/fd_robot/chassis_status 话题 · 系统内部
底盘综合状态。类型 fd_msgs/ChassisStatusMsg,1 Hz。
手册说明其内容为「位姿、chassis_stat 和 error_msg」,并特别标注:健康状态字段主要在 slamtec 上有效。
项目 内容
类型 / 频率 fd_msgs/ChassisStatusMsg · 1 Hz
手册列出的内容 位姿、chassis_stat、error_msg
机型差异 健康状态字段主要在 slamtec 上有效;woosh / yunji 请以位姿和导航状态为准
ros2 topic echo /fd_robot/chassis_status
ros2 interface show fd_msgs/msg/ChassisStatusMsg
/fd_robot/slamtec/battery_info 话题 · 系统内部
底盘电量与充电状态。类型 fd_msgs/SlamTecBatteryMsg,2 Hz。
手册说明:内容为「电量、充电和对接状态」,话题名沿用旧命名,三种底盘都会发布。
项目 内容
类型 / 频率 fd_msgs/SlamTecBatteryMsg · 2 Hz
手册列出的内容 电量、充电和对接状态
机型差异 非 slamtec 底盘的部分字段可能为空(附录列为已知限制,仅作参考)
ros2 topic echo /fd_robot/slamtec/battery_info
ros2 interface show fd_msgs/msg/SlamTecBatteryMsg
别和第十二章的电池接口搞混。手册第十二章说明:/fd_robot/battary_manager/* 是独立电池管理节点(如铁塔储能电池)的接口,与本章由底盘上报的 battery_info 相互独立。注意路径里的 battary 是沿用的旧拼写,不是 battery。
/fd_robot/slamtec/run_status 话题 · 系统内部
底盘是否在运动。类型 std_msgs/Bool,1 Hz。
手册说明:速度阈值约 0.02(即速度低于该阈值视为静止)。这是判断"底盘是否真的停下来"最直接的信号。
项目 内容
类型 / 频率 std_msgs/Bool · 1 Hz
取值含义 data=true 表示在运动;判定阈值为速度约 0.02
典型用途 排查"命令发了但车没动";辅助整机空闲判断(见 6.9 节)
ros2 topic echo /fd_robot/slamtec/run_status
/fd_robot/shutdown_signal 话题 · 系统内部
底盘侧关机信号。类型 std_msgs/Bool,配置条件 chassis_model=woosh。
手册说明:由 woosh 电源键触发,并提示「整机关机信号见第十一章同名话题」。
项目 内容
类型 std_msgs/Bool
触发方式 woosh 底盘电源键
配置条件 chassis_model=woosh
ros2 topic info /fd_robot/shutdown_signal -v   # 看清发布者是谁
ros2 topic echo /fd_robot/shutdown_signal
shutdown_signal 有两个来源。本章的是 fd_chassis_http_client(woosh 底盘侧),第十一章的是 fd_power_board_pkg(整机)。话题名完全相同,订阅时务必用 ros2 topic info -v 确认发布者,不要想当然。

六、典型流程与完整示例

6.1 首次对接步骤

步骤 操作 目的 / 判据
1 source install/setup.bash;确认 ROS_DOMAIN_ID=25 能与机器人节点通信
2 确认 chassis_model 是 slamtec / woosh / yunji 中的哪一个 决定能否使用按点位导航
3 ros2 topic list | grep -E 'chassis|slamtec' 确认接口存在;一条都没有说明底盘驱动未启动
4 ros2 topic hz /fd_robot/chassis/current_pose 确认位姿反馈在发,约 5 Hz
5 确认底盘周围有安全空间、无人站在行进路线上 底盘会带动整机移动,风险等级高于关节
6 确认无人工遥控 / 其他控制源占用 手册 1.2 节第 6 条:同一执行器只能有一个控制来源
7 先用 move_control 做一次很短的运动(如 300 ms、低速) 验证方向定义(0/1/2/3)与实际一致
8 再用 goto_control 做一次近距离导航 验证 nav_status 判定逻辑与超时设置

6.2 两种运动方式怎么选

对比项 move_control(定时速度运动) goto_control(导航)
是否有目标点 没有,只给方向和时长 有,点位(mark)或位姿(x/y/yaw)
是否闭环 开环,走多久全靠 duration 闭环,底盘自己规划并判断到达
完成判定 duration 到时自动停止;可配合 run_status 确认 订阅 nav_status,看 arrived / failed 的跳变
依赖 只需底盘驱动 需底盘已完成建图与定位
适用 微调、脱困、示教、简单进出 点到点运输、巡场
定时运动是开环的。手册只保证「到时自动停止」,不保证走了多远。想要"走 0.5 m",请用导航(nav_mode=1 + 目标位姿),或自己订阅 current_pose 做闭环。

6.3 定时速度运动的标准姿势

顺序 动作 要点
① 读取当前 current_pose 记录起点,运动后可对比实际位移
② 发布 move_control direction 用 0/1/2/3;duration 单位毫秒;速度 ≤0 用节点默认值
③ 等待 duration 结束 到时自动停止,无需再发停止命令
④ 再次读取 current_pose(可选叠加 run_status) 确认是否真的动了、动了多少

6.4 导航完整流程

① 订阅 nav_status(先订阅,再发命令)
▼
② 发布 goto_control:nav_mode + request_type=0 + 目标
▼
③ 等待 nav_status 中 arrived / failed 的 false→true 跳变
▼
④ arrived → 成功;failed → 失败;超时 → 发 request_type=1 取消
顺序是"先订阅、后发命令"。nav_status 是按需发布的话题。如果先发导航命令再创建订阅,可能错过最早的几次状态变化,甚至错过从 false 到 true 的跳变。

6.5 取消导航

# 取消导航:同一个 goto_control 话题,request_type=1
ros2 topic pub -1 /fd_robot/chassis/goto_control fd_msgs/msg/ChassisGotoMsg \
  "{nav_mode: 1, request_type: 1}"

# 取消后观察状态,确认已停止
ros2 topic echo /fd_robot/chassis/nav_status
ros2 topic echo /fd_robot/chassis/current_pose
手册未说明取消后 nav_status 的具体表现(是否会置 failed,或只是 active 变 false),也未说明取消是否需要重复下发。建议实测确认,并保留 current_pose 是否停止变化作为二次验证。

6.6 三种底盘支持矩阵与降级策略

能力 slamtec woosh yunji 不支持时的替代做法
按位姿导航(nav_mode=1) ✔ ✔ ✔ —
按点位导航(nav_mode=0) ✗ ✔ ✔ 改用 nav_mode=1,自行维护"点位号 → (x, y, yaw)"映射表
chassis_status 健康字段 ✔ 部分 部分 以 current_pose + nav_status 为准
battery_info 完整字段 ✔ 部分为空 部分为空 字段为空时不要当作"电量为 0"
底盘侧 shutdown_signal ✗ ✔ ✗ 订阅第十一章整机关机信号
推荐的兼容写法:把「点位号 → 位姿」的映射放在自己的程序里,统一用 nav_mode=1 下发。这样三种底盘走同一条代码路径,不需要为 slamtec 单独分支,也避免了"发 nav_mode=0 就立即 failed"的坑。

6.7 完整示例:Python 端到端

下面这份脚本把「前置检查 → 先订阅 → 导航 → 跳变判定 → 超时取消」串成了完整链路(示意代码,请按实际机型调整超时与目标点)。

# chassis_nav_demo.py —— 底盘导航端到端示例(示意代码)
import rclpy
from rclpy.node import Node
from fd_msgs.msg import ChassisGotoMsg, ChassisNavStatusMsg
from geometry_msgs.msg import Pose

TOPIC_GOTO = '/fd_robot/chassis/goto_control'
TOPIC_NAV = '/fd_robot/chassis/nav_status'
TOPIC_POSE = '/fd_robot/chassis/current_pose'

class ChassisNav(Node):
    def __init__(self):
        super().__init__('chassis_nav_demo')
        self.pub = self.create_publisher(ChassisGotoMsg, TOPIC_GOTO, 10)
        self.sub_nav = self.create_subscription(
            ChassisNavStatusMsg, TOPIC_NAV, self.on_nav, 10)
        self.sub_pose = self.create_subscription(
            Pose, TOPIC_POSE, self.on_pose, 10)
        self.prev = (False, False, False)
        self.result = None
        self.pose = None

    def on_pose(self, msg):
        self.pose = msg

    def on_nav(self, msg):
        active, arrived, failed = bool(msg.active), bool(msg.arrived), bool(msg.failed)
        # 手册 7.3:三个标志可能短暂同时为 true,
        # 以 failed 或 arrived 由 false 变为 true 的时刻作为判定依据
        if failed and not self.prev[2]:
            self.get_logger().warn('导航失败(failed false→true)')
            self.result = 'failed'
        elif arrived and not self.prev[1]:
            self.get_logger().info('已到达(arrived false→true)')
            self.result = 'arrived'
        self.prev = (active, arrived, failed)

    def preflight(self):
        for _ in range(50):
            rclpy.spin_once(self, timeout_sec=0.1)
            if self.pose is not None:
                self.get_logger().info(
                    f'底盘在线,起点 x={self.pose.position.x:.3f} '
                    f'y={self.pose.position.y:.3f}')
                return True
        self.get_logger().error('收不到 current_pose,请检查底盘驱动是否启动')
        return False

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

    def goto_pose(self, x, y, yaw, timeout_sec=120.0):
        self.result = None
        msg = ChassisGotoMsg()
        msg.nav_mode = 1            # POSE:三种底盘均支持
        msg.request_type = 0        # 0 开始
        msg.x, msg.y, msg.yaw = float(x), float(y), float(yaw)
        self._send(msg)
        self.get_logger().info(f'开始导航到 x={x} y={y} yaw={yaw}')

        deadline = self.get_clock().now().nanoseconds + int(timeout_sec * 1e9)
        while rclpy.ok() and self.result is None:
            rclpy.spin_once(self, timeout_sec=0.1)
            if self.get_clock().now().nanoseconds > deadline:
                self.get_logger().error('导航超时,取消导航')
                self.cancel_nav()
                return 'timeout'
        return self.result

    def cancel_nav(self):
        msg = ChassisGotoMsg()
        msg.nav_mode = 1
        msg.request_type = 1        # 1 取消
        self._send(msg)
        self.get_logger().warn('已下发取消导航')

def main():
    rclpy.init()
    node = ChassisNav()
    try:
        if node.preflight():
            result = node.goto_pose(1.0, 0.0, 0.0, timeout_sec=120.0)
            node.get_logger().info(f'导航结果:{result}')
    finally:
        node.destroy_node()
        rclpy.shutdown()

if __name__ == '__main__':
    main()

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

// chassis_nav_demo.cpp —— 底盘导航端到端示例(示意代码)
#include <chrono>
#include <string>
#include <rclcpp/rclcpp.hpp>
#include "fd_msgs/msg/chassis_goto_msg.hpp"
#include "fd_msgs/msg/chassis_nav_status_msg.hpp"
#include "geometry_msgs/msg/pose.hpp"

using namespace std::chrono_literals;

class ChassisNav : public rclcpp::Node {
 public:
  ChassisNav() : Node("chassis_nav_demo") {
    pub_ = create_publisher<fd_msgs::msg::ChassisGotoMsg>(
        "/fd_robot/chassis/goto_control", 10);

    sub_nav_ = create_subscription<fd_msgs::msg::ChassisNavStatusMsg>(
        "/fd_robot/chassis/nav_status", 10,
        [this](fd_msgs::msg::ChassisNavStatusMsg::SharedPtr m) {
          // 手册 7.3:三个标志可能短暂同时为 true,以 false→true 的跳变时刻为准
          if (m->failed && !prev_failed_) {
            RCLCPP_WARN(get_logger(), "导航失败(failed false->true)");
            result_ = "failed";
          } else if (m->arrived && !prev_arrived_) {
            RCLCPP_INFO(get_logger(), "已到达(arrived false->true)");
            result_ = "arrived";
          }
          prev_arrived_ = m->arrived;
          prev_failed_ = m->failed;
        });

    sub_pose_ = create_subscription<geometry_msgs::msg::Pose>(
        "/fd_robot/chassis/current_pose", 10,
        [this](geometry_msgs::msg::Pose::SharedPtr m) {
          has_pose_ = true;
          RCLCPP_INFO_THROTTLE(get_logger(), *get_clock(), 2000,
                               "pose x=%.3f y=%.3f",
                               m->position.x, m->position.y);
        });
  }

  bool preflight() {
    for (int i = 0; i < 50 && rclcpp::ok(); ++i) {
      rclcpp::spin_some(this);
      if (has_pose_) { RCLCPP_INFO(get_logger(), "底盘在线"); return true; }
      rclcpp::sleep_for(100ms);
    }
    RCLCPP_ERROR(get_logger(), "收不到 current_pose,请检查底盘驱动是否启动");
    return false;
  }

  std::string goto_pose(double x, double y, double yaw, double timeout_sec) {
    result_ = "none";
    fd_msgs::msg::ChassisGotoMsg goal;
    goal.nav_mode = 1;          // POSE:三种底盘均支持
    goal.request_type = 0;      // 0 开始
    goal.x = x; goal.y = y; goal.yaw = yaw;
    for (int i = 0; i < 3; ++i) { pub_->publish(goal); rclcpp::sleep_for(50ms); }

    auto deadline = now() + rclcpp::Duration::from_seconds(timeout_sec);
    while (rclcpp::ok() && now() < deadline && result_ == "none") {
      rclcpp::spin_some(this);
      rclcpp::sleep_for(100ms);
    }
    if (result_ == "none") { cancel_nav(); return "timeout"; }
    return result_;
  }

  void cancel_nav() {
    fd_msgs::msg::ChassisGotoMsg cancel;
    cancel.nav_mode = 1;
    cancel.request_type = 1;    // 1 取消
    for (int i = 0; i < 3; ++i) { pub_->publish(cancel); rclcpp::sleep_for(50ms); }
    RCLCPP_WARN(get_logger(), "已下发取消导航");
  }

 private:
  rclcpp::Publisher<fd_msgs::msg::ChassisGotoMsg>::SharedPtr pub_;
  rclcpp::Subscription<fd_msgs::msg::ChassisNavStatusMsg>::SharedPtr sub_nav_;
  rclcpp::Subscription<geometry_msgs::msg::Pose>::SharedPtr sub_pose_;
  bool prev_arrived_{false}, prev_failed_{false}, has_pose_{false};
  std::string result_{"none"};
};

int main(int argc, char** argv) {
  rclcpp::init(argc, argv);
  auto node = std::make_shared<ChassisNav>();
  if (node->preflight()) {
    std::string r = node->goto_pose(1.0, 0.0, 0.0, 120.0);
    RCLCPP_INFO(node->get_logger(), "导航结果:%s", r.c_str());
  }
  rclcpp::shutdown();
  return 0;
}

6.9 与整机状态 / 安全的联动

手册附录给了一条容易被忽略的限制:/fd_robot/robot_idle_status 可能没有把底盘运动算进去。也就是说,「整机空闲」为 true 时,底盘可能正在移动。

# 示意代码:判断"真的都停了",不要只信 robot_idle_status
import rclpy
from rclpy.node import Node
from std_msgs.msg import Bool, String

class IdleChecker(Node):
    def __init__(self):
        super().__init__('idle_checker')
        self.create_subscription(String, '/fd_robot/robot_idle_status',
                                 self.on_idle, 10)
        self.create_subscription(Bool, '/fd_robot/slamtec/run_status',
                                 self.on_run, 10)
        self.idle_json = None
        self.chassis_moving = None

    def on_idle(self, msg):
        self.idle_json = msg.data

    def on_run(self, msg):
        self.chassis_moving = bool(msg.data)

    def is_really_idle(self):
        # 附录:底盘运动可能未计入空闲判断,需结合 run_status / nav_status
        if self.chassis_moving:
            return False
        if self.idle_json is None:
            return False
        return '"idle"' in self.idle_json and 'true' in self.idle_json

def main():
    rclpy.init()
    node = IdleChecker()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()
上面 is_really_idle() 里的 JSON 判断是简化写法。手册只说明 robot_status / robot_idle_status 的 data 是 JSON 文本,没有给出字段结构。实际使用时请先用 ros2 topic echo 看清 JSON 的字段名,再做解析,不要照抄这段字符串匹配。
信号 话题 处理建议
底盘是否在动 /fd_robot/slamtec/run_status 系统内部接口,但排查"命令发了没动"最直接;阈值约 0.02
导航状态 /fd_robot/chassis/nav_status 判定导航结果的唯一依据
整机空闲 /fd_robot/robot_idle_status 不能只看 idle 字段,需叠加底盘状态
整机关机 / 急停 /fd_robot/shutdown_signal
/fd_robot/emergency_signal
见第十一章;不要在底盘运动中途触发关机

七、注意事项与已知限制

7.1 硬性约束

① slamtec 不支持按点位导航。手册 7.1 / 7.3 节两处强调:slamtec 底盘收到 nav_mode=0 时,nav_status 会立即报告 failed。跨机型代码统一用 nav_mode=1。
② nav_status 必须按"跳变"判定。手册 7.3 节原文:三个标志可能短暂同时为 true,请以 failed 或 arrived 由 false 变为 true 的时刻作为判定依据。用"当前值是否为 true"会误判。
③ nav_status 是按需发布。手册未标注频率。要先订阅、再发导航命令,否则可能错过状态跳变。
④ duration 单位是毫秒。手册 1.1 节专门列出;1000 是 1 秒。
⑤ 没有服务、没有动作。手册 7.2 节「无」。取消导航也要发话题(request_type=1),不是服务调用。
⑥ 同一底盘只能有一个控制来源。人工遥控 / 遥操作会话期间不要同时下发命令。

7.2 已知限制表

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

限制 说明 影响
nav_status 三标志可能同时为 true 手册 7.3 节明确:须以 failed 或 arrived 由 false 变为 true 的时刻作为判定依据 高
slamtec + nav_mode=0 slamtec 不支持按点位导航,收到 nav_mode=0 会立即报告 failed 高
/fd_robot/chassis_status 健康状态字段主要在 slamtec 上有效;woosh / yunji 以位姿和导航状态为准 中
/fd_robot/robot_idle_status 底盘运动可能未计入空闲判断;须结合 nav_status 或速度判断 中
/fd_robot/slamtec/battery_info 非 slamtec 底盘的部分字段可能为空,仅作参考 低
ChassisNavStatusMsg 字段表 手册未明确:只说明了 active / arrived / failed 三个标志的含义,未给出类型与完整字段 中
导航超时约定 手册未给出。fd_api 的 chassis_wait_arrived 默认 600 s 属另一套封装,仅供参考。程序须自设超时 中
取消导航后的状态 手册未明确:取消后 nav_status 是否置 failed、是否需要重复下发,均未说明 中
move_control 停止行为 手册原文「部分底盘」会把 duration=0 且速度≈0 视为停止命令,不是三种通用 中
current_pose 的 z / roll / pitch 手册未说明这些分量是否有效,只称「平面位姿」 低

7.3 排查速查表

现象 可能原因 处理
发 nav_mode=0 立刻 failed 机型是 slamtec,不支持按点位导航 改用 nav_mode=1,自行维护点位→位姿映射
明明到了却判定成失败(或反之) 用"当前值"而不是"跳变"做判定 改为检测 arrived / failed 的 false→true 跳变
订阅了 nav_status 却一直没数据 它是按需发布,没有导航事件就不发 先发导航命令;确认订阅是在发命令之前建立的
命令发了车不动 被人工遥控占用;底盘未定位;速度被填成 0 又没默认值 确认控制源唯一;用 run_status 看是否在动;显式填速度
duration 填 1000 结果瞬间停 误以为单位是秒,实际是毫秒 1 秒要填 1000(毫秒)
一条底盘话题都看不到 底盘驱动未启动 检查 chassis_model 与驱动节点状态
整机报空闲但底盘在动 附录已知限制:底盘运动可能未计入 叠加 run_status 或 nav_status 判断
battery_info 字段是空的 非 slamtec 底盘部分字段为空 不要当作电量为 0;以底盘自身指示为准

7.4 通用提醒

先确认接口可见再发命令。用 ros2 topic list、ros2 topic hz 确认接口存在且有反馈;底盘驱动未启动时接口不会有任何响应。
内部接口只用于排查。第五章标注的「系统内部接口」(chassis_status、slamtec/battery_info、slamtec/run_status、shutdown_signal)仅供排查问题参考,请勿与二次开发接口同时调用。
与 fd_api 不要混用。两套接口是不同层级(fd_api 有 chassis_wait_arrived 这类封装,ROS 层没有)。同时使用会抢控制权。相关对照见 1.1 节,另一层级的完整说明见 《fd_api 底盘说明》。
底盘运动影响整机,风险等级高于单个关节。正式使用前请在空旷、有人监护的条件下验证方向与速度;不要在人员通行的路线上做首次调试。
底盘 ROS 接口说明 — 天链机器人 © 2026