底盘接口说明
底盘接口说明
chassis_model = slamtec / woosh / yunji · 三种底盘同名话题 · 逐接口说明 + 独立示例
软件版本 FDRS-V04-02-00 · 核对日期 2026-09-17一、概述
底盘是机器人的移动平台。它和腰部那种"一外设多套互斥驱动栈"的情况不同:手册第七章第一句就写明——slamtec、woosh、yunji 三种底盘使用相同的 ROS 话题名。
也就是说:接口不用按机型分栈,一套代码三种底盘通用。真正需要分机型的是能力,其中最关键的一条是:
| 导航方式 | nav_mode | slamtec | woosh | yunji |
|---|---|---|---|---|
| 按位姿导航(POSE) | 1 | ✔ 支持 | ✔ 支持 | ✔ 支持 |
| 按点位导航(MARK) | 0 | ✗ 不支持,nav_status 会立即报告 failed | ✔ 支持 | ✔ 支持 |
本文覆盖手册第七章的全部二次开发接口 4 条(2 个命令发布 + 2 个状态订阅),以及 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 |
chassis_wait_arrived() 在 ROS 层没有对应接口。它是封装层在 nav_status 之上包出来的阻塞等待。直接用 ROS 话题时,需要自己实现"订阅 + 判定跳变 + 超时"这套逻辑——本文 6.7 / 6.8 节的端到端示例给的就是这套实现。1.2 底盘在整机中的位置
底盘和升降一样,没有应用层服务、没有动作接口——程序直接发布话题,节点转发给底盘厂家接口。因此本章全部接口都是话题。
/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('已到达')
chassis_wait_arrived 默认 600 秒仅供参考,属另一套封装)。自己的程序里应设一个上限,超时后发 request_type=1 取消导航。2.6 安全前提与已知交互
/teleop/joint_targets_* 或底层 /tl_control/* 正在控制时,不要再通过本手册接口下发命令。底盘尤其容易被人工遥控同时操作。/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 关于服务与动作
因此:没有
result_code、没有 goal / feedback,导航完成只能靠订阅 nav_status 判定(见 2.5 节);取消导航也不是服务调用,而是往同一个 goto_control 话题发 request_type=1。四、话题接口详解
本章按手册 7.1 节的顺序,逐个说明 4 个二次开发话题:先 2 个命令发布,再 2 个状态订阅。每个接口固定给出「语义卡片 → 字段表 → 三段示例(命令行 / Python / C++)」。
BEST_EFFORT 状态话题不同,不要照搬那边的写法。ros2 interface show 的实际输出为准。三种底盘共用同名话题,示例代码通用,但能力差异(尤其 nav_mode=0)需要自行按机型规避。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_msgs/ChassisGotoMsg,来源包 fd_chassis_http_client。| 字段 | 侧 | 类型 | 取值 | 说明 |
|---|---|---|---|---|
| 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_msgs/ChassisNavStatusMsg,来源包 fd_chassis_http_client。| 字段 | 侧 | 类型 | 取值 | 说明 |
|---|---|---|---|---|
| active | 消息 | — | true / false | 导航进行中 |
| arrived | 消息 | — | true / false | 已到达 |
| failed | 消息 | — | true / false | 导航失败 |
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;
}
geometry_msgs/Pose,来源包 fd_chassis_http_client。| 字段 | 侧 | 类型 | 取值 | 说明 |
|---|---|---|---|---|
| position.x / position.y | 消息 | float64 | 米(m) | 地图坐标系下的平面位置 |
| position.z | 消息 | float64 | — | 手册称「平面位姿」,z 是否有效手册未说明 |
| orientation | 消息 | geometry_msgs/Quaternion | 四元数 | 朝向;平面场景下主要用 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;
}
sleep 而不 spin。不 spin 就不会执行订阅回调,位姿永远是空的。持续监听直接用 rclcpp::spin(node);需要与其它逻辑并行时,在循环里调用 rclcpp::spin_some(node)(见 4.1 节与 6.8 节的写法)。五、系统内部接口
手册 7.1 节把下面 4 条标为系统内部接口:由驱动与诊断底层使用,仅供排查问题时参考,请勿与二次开发接口同时调用。手册 2.2 节也说明:系统内部接口不逐一展开字段,如需查看可对类型名执行 ros2 interface show。
fd_msgs/ChassisStatusMsg,1 Hz。| 项目 | 内容 |
|---|---|
| 类型 / 频率 | 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_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。std_msgs/Bool,1 Hz。| 项目 | 内容 |
|---|---|
| 类型 / 频率 | std_msgs/Bool · 1 Hz |
| 取值含义 | data=true 表示在运动;判定阈值为速度约 0.02 |
| 典型用途 | 排查"命令发了但车没动";辅助整机空闲判断(见 6.9 节) |
ros2 topic echo /fd_robot/slamtec/run_status
std_msgs/Bool,配置条件 chassis_model=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 的跳变 |
| 依赖 | 只需底盘驱动 | 需底盘已完成建图与定位 |
| 适用 | 微调、脱困、示教、简单进出 | 点到点运输、巡场 |
current_pose 做闭环。6.3 定时速度运动的标准姿势
| 顺序 | 动作 | 要点 |
|---|---|---|
| ① | 读取当前 current_pose |
记录起点,运动后可对比实际位移 |
| ② | 发布 move_control |
direction 用 0/1/2/3;duration 单位毫秒;速度 ≤0 用节点默认值 |
| ③ | 等待 duration 结束 | 到时自动停止,无需再发停止命令 |
| ④ | 再次读取 current_pose(可选叠加 run_status) |
确认是否真的动了、动了多少 |
6.4 导航完整流程
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
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 硬性约束
nav_mode=0 时,nav_status 会立即报告 failed。跨机型代码统一用 nav_mode=1。1000 是 1 秒。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)仅供排查问题参考,请勿与二次开发接口同时调用。chassis_wait_arrived 这类封装,ROS 层没有)。同时使用会抢控制权。相关对照见 1.1 节,另一层级的完整说明见 《fd_api 底盘说明》。