尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于ROS2的无人船集群控制系统:从DDS配置到实船部署避坑指南

基于ROS2的无人船集群控制系统:从DDS配置到实船部署避坑指南 简介基于ROS2的无人船集群控制系统完整项目包面向需要完成毕业设计、课程设计或期末大作业的ROS2学习者。项目依托ROS2分布式通信架构实现多无人船协同控制与自主导航覆盖集群启动、标准消息接口、控制算法、通信管理及无人船物理模型等关键环节。压缩包共74个文件整体约25.99MB包含22个C源文件、10个Python脚本、7个URDF模型文件、5个YAML参数配置以及msg/srv接口定义和RVIZ可视化配置目录结构清晰便于检索。资源按asv_bringup、asv_interfaces、asv_control、asv_comunication、yf_description五个模块组织从启动脚本到控制算法均有示例便于快速理解模块划分与消息流可直接用于仿真验证或二次开发。目前已有52人学习下载适合作为ROS2集群控制、无人船系统设计等课题的参考模板。1. 把无人船集群控制系统拆开看为什么说 ROS2 是当下的最优解三艘无人船要在同一条内河航段里做覆盖搜索领航船按规划路径走另外两艘保持三角队形并自动避障岸基电脑只发任务不接管每一条船的舵和油门。这个场景不是把遥控航模拼在一起而是一个典型的分布式实时机器人系统需要通信中间件把传感器、控制器和岸基监控串成一张网。ROS2 就是目前最适合做这件事的软件骨架这也是“基于ROS2的无人船集群控制系统”这类工程包能成立的根本原因。这类工程包里通常放着三块东西仿真环境、船载节点、岸基监控。拿到手之后新手能从零启动三条船的仿真熟手会接着改 QoS 参数、换 DDS 实现、把仿真里的编队控制器搬到实船。真正决定集群成败的往往不是队形算法而是通信架构命名空间怎么隔离、话题怎么定 QoS、每个节点该放哪台机器。这篇笔记不打算给你讲一遍 ROS2 菜鸟教程而是直接从“三艘船要协同”这个目标出发把节点拆分、DDS 选型、仿真复现、实船排错这几个环节讲透适合做水域巡检、环境采样、无人船比赛和毕业设计的学生与工程师。2. 集群控制系统的骨架基于 ROS2 的节点划分、DDS 与 QoS 怎么定2.1 把单船拆成节点、话题、服务、动作为什么编队要三层通信ROS2 给无人船集群提供的最小通信原语就是节点、话题、服务、动作这四样正好分别对应船上不同性质的交互。话题适合流水数据GPS 定位、IMU 姿态、速度指令都是高频、单向、不关心谁在收。服务适合低频配置修改 PID 参数、查询电池电压、切换控制模式。动作适合长时间任务跟航点执行、覆盖搜索、自主返航因为动作服务端能上报进度客户端也能在中途取消。我一般会把每条船拆成这样的节点gps_driver、imu_driver、thruster_control、local_planner、swarm_controller和safety_watchdog。其中swarm_controller负责把领航船或目标点转成这条船的期望速度local_planner负责避障并把期望速度转成左右推进器的 PWM。这样拆分的好处是单船调试时只启动 gps、imu、thruster 三个节点编队调试时才拉高整个集群哪一层出问题能快速定位。# launch/single_boat.launch.py —— 一条船内的最小节点集合 from launch import LaunchDescription from launch_ros.actions import Node def generate_launch_description(): return LaunchDescription([ Node(packageboat_bringup, executablegps_driver, namegps_driver, namespaceboat0, parameters[{port: /dev/ttyGPS0}]), Node(packageboat_bringup, executableimu_driver, nameimu_driver, namespaceboat0, parameters[{port: /dev/ttyIMU0}]), Node(packageboat_bringup, executablethruster_control, namethruster_control, namespaceboat0, parameters[{channel_left: 0, channel_right: 1}]), Node(packageboat_control, executablelocal_planner, namelocal_planner, namespaceboat0), Node(packageboat_control, executablesafety_watchdog, namesafety_watchdog, namespaceboat0), ])这段 launch 文件的关键在namespaceboat0。加上之后GPS 节点发布的话题实际名是/boat0/gps/fix速度指令是/boat0/cmd_vel不同船之间天然隔离。gps_driver和imu_driver的port参数是串口设备路径要根据工控机上实际挂载的设备号来填不要照抄别人工程里的/dev/ttyUSB0。参数用parameters[{port: ...}]传节点内用self.declare_parameter(port, /dev/ttyUSB0)接收后续换船、换串口都不用改代码。航点跟随这种长任务应该用 action 而不是 service。常见做法是让local_planner起一个 action server话题形如/boat0/follow_waypoint岸基或者swarm_controller作为 action client 发目标。action 的好处是反馈中能带上“当前航点、剩余距离、是否被障碍物阻塞”当任务被取消时服务器会清理内部状态不会出现 service 那种“调用卡死导致任务悬空”的局面。集群控制里每条船各自维护一套 action server靠命名空间区分这就是 ROS2 里动作机制比单纯用话题标志位稳得多的原因。2.2 水面无线场景下的 DDS 与 QoS 参数别照搬陆地机器人配置ROS2 的底层传输是 DDSDDS 的优劣直接决定水面集群的稳定性。在 ROS2 Humble 里默认是 Fast DDS也可以通过环境变量切到 Cyclone DDS。我实际对比过Wi-Fi 网络不稳定时 Cyclone DDS 对丢包和拓扑变化的容忍度更好代价是单拷贝吞吐稍低。切换到 Cyclone DDS 只需要在每个船载终端和岸基电脑的 bashrc 里加一句export RMW_IMPLEMENTATIONrmw_cyclonedds_cpp三条船必须在同一个网段里能互相访问。比较稳的组网方案是每条船上一台工控机加一个工业路由器路由器之间用 5.8GHz 网桥串联岸基电脑也接到这个网络里给每台设备指定固定 IP。千万不要依赖 DHCP 自动分配的地址DDS 发现机制在 IP 变化频繁的网络上很容易互相找不到。如果船的作业范围超过了几百米无线网桥撑不住就要换 4G 工业数传模块但这时候延迟和抖动会明显变大编队控制频率必须降下来。QoS 参数是水面场景里最容易踩坑的地方。很多人在陆地上做 ROS2 机器人习惯了 RELIABLE KEEP_LAST(10)上船之后照搬结果无线丢包时 DDS 疯狂重传旧数据控制指令的实时性反而被拖垮。我的经验是控制链路和传感器链路用不同的 QoS 策略见下表。用途ReliabilityHistory / Depth说明速度指令 cmd_velBEST_EFFORTKEEP_LAST / 5新指令优先旧指令可丢GPS/IMU 高频状态BEST_EFFORTKEEP_LAST / 20掉一帧不影响控制航点结果/任务状态RELIABLEKEEP_LAST / 1终态必须到达日志和调试数据RELIABLEKEEP_ALL用于事后回放不能丢# qos_profiles.py —— 水面控制常用的两套 QoS 配置 from rclpy.qos import QoSProfile, QoSReliabilityPolicy, QoSHistoryPolicy # 控制指令永远关心最新值丢了就丢不要重传旧包 cmd_qos QoSProfile( depth5, reliabilityQoSReliabilityPolicy.BEST_EFFORT, historyQoSHistoryPolicy.KEEP_LAST, ) # 日志类数据要完整回放宁可延迟也不能丢 log_qos QoSProfile( depth1000, reliabilityQoSReliabilityPolicy.RELIABLE, historyQoSHistoryPolicy.KEEP_LAST, )depth1在 Wi-Fi 环境里不够稳。原因是无线链路一个瞬时拥塞可能连续丢 3-4 帧depth 为 1 时接收端拿到的是拥塞后的第一帧控制效果跳动明显depth 为 5 时至少能缓冲一小段再由控制器做时间插值船的姿态会更平滑。注意 RELIABLE 不代表“保数据”它在网络状况差时会无限重传把带宽占满最终控制频率掉到 1Hz 以下。所以控制指令必须 BEST_EFFORT。2.3 单船最小数据链从串口 GPS 到 NavSatFix 话题的搭建顺序单船调试是集群调试的前提。我习惯先把 GPS 和 IMU 的 ROS 话题打通再接推进器最后才跑编队控制器。最小系统只需要一个串口 GPS、一块带 IMU 的飞控板或独立的 AHRS以及一块能输出 PWM 的电调控制板。GPS 的串口协议大多是 NMEA 0183ROS2 标准消息是sensor_msgs/NavSatFix所以第一步就是写一个串口驱动节点做格式转换。#!/usr/bin/env python3 import serial import rclpy from rclpy.node import Node from sensor_msgs.msg import NavSatFix class GpsDriver(Node): def __init__(self): super().__init__(gps_driver) self.pub self.create_publisher(NavSatFix, gps/fix, 20) self.ser serial.Serial(/dev/ttyGPS0, 9600, timeout0.1) def parse_nmea(self, line): # 只处理 GGA 语句它包含经纬度和高程 if not line.startswith($GPGGA): return None parts line.split(,) if len(parts) 10 or parts[2] or parts[4] : return None lat_raw float(parts[2]) lon_raw float(parts[4]) msg NavSatFix() # NMEA 格式是 度分.分要转成纯十进制 msg.latitude int(lat_raw / 100) (lat_raw % 100) / 60.0 msg.longitude int(lon_raw / 100) (lon_raw % 100) / 60.0 if parts[3] S: msg.latitude -msg.latitude if parts[5] W: msg.longitude -msg.longitude msg.altitude float(parts[9]) return msg def spin(self): while rclpy.ok(): line self.ser.readline().decode(ascii, errorsignore) msg self.parse_nmea(line) if msg: self.pub.publish(msg) def main(): rclpy.init() node GpsDriver() try: node.spin() except KeyboardInterrupt: pass if __name__ __main__: main()这段代码里最容易出错的是 NMEA 经纬度转换。4807.038表示 48 度 07.038 分转成十进制度公式是48 07.038/60 48.1173不是直接除以 100。我见过不少工程把纬度直接除以 100编队控制在小范围水域漂了上百米还找不到原因。另一个细节是串口波特率很多 GPS 模块出厂是 9600也有 115200 的先在电脑上用串口助手确认再写进参数不要在代码里硬编码。串口读取超时timeout0.1保证循环不会卡死GPS 信号丢失时parse_nmea会返回 None话题就自然停止更新不会发脏数据。这层数据链打通之后用ros2 topic echo /boat0/gps/fix --once能读到定位坐标再启动 IMU 驱动和推进器驱动单船就算具备闭环控制的条件了。仿真阶段还需要为每条船准备一个模拟的/boatX/odom这个放到下一章展开。3. 先仿真后实船用 Gazebo 把三船编队控制跑通的最小复现3.1 用 URDF 与 robot_state_publisher 按命名空间拉起三艘船水面集群算法不能在实船上试错第一版必须在 Gazebo 里跑。Gazebo 默认物理场景是地面环境做无人船仿真至少要解决浮力和流体阻力两个问题。常见做法是借用 VRX 这类开源无人船仿真项目的船体模型和推进器插件如果只是验证集群控制逻辑用一个带阻尼插件的简易船体模型也够用重点是让三艘船的运动学响应接近真实而不是把水动力细节做得多精确。我推荐的启动顺序是先启动 Gazebo 空世界再为每条船单独起一个robot_state_publisher并发布各自的robot_description最后用spawn_entity.py把模型实例化成boat0、boat1、boat2三个实体。关键点在于每条船的robot_state_publisher都要放在独立命名空间里这样 Gazebo 里的模型对应的话题才不会互相覆盖。# multi_boat_sim.launch.py —— 批量拉起三条船的机器人描述 from launch import LaunchDescription from launch_ros.actions import Node import xacro def generate_launch_description(): launch_actions [] xacro_file src/boat_description/urdf/boat.xacro robot_desc xacro.process_file(xacro_file).toxml() for boat_name in [boat0, boat1, boat2]: launch_actions.append(Node( packagerobot_state_publisher, executablerobot_state_publisher, namerobot_state_publisher, namespaceboat_name, parameters[{robot_description: robot_desc}], )) # spawn_entity 放到另一个 ExecuteProcess 里执行 launch_actions.append( ExecuteProcess( cmd[ros2, run, gazebo_ros, spawn_entity.py, -topic, f/{boat_name}/robot_description, -entity, boat_name, -namespace, boat_name, -x, str(BOAT_POS[boat_name][0]), -y, str(BOAT_POS[boat_name][1])], outputscreen, ) ) return LaunchDescription(launch_actions)上面代码里我用BOAT_POS字典存三艘船的初始坐标实际写 launch 文件时直接定义成常量即可例如boat0在(0, 0)、boat1在(5, 0)、boat2在(0, 5)。xacro.process_file(...).toxml()会把 xacro 文件解析成 URDF 字符串作为robot_description参数传给robot_state_publisher。这一步不要在命令行里用$(xacro boat.xacro)直接传因为 URDF 文本很长命令行容易触发系统参数长度上限而且调试时格式问题也难看出来。启动之后用下面两条命令检查三条船是否都进入了 Gazebo 世界# 查看 Gazebo 里的实体话题是否生成 ros2 topic list | grep boat # 分别查看三条船是否有里程计输出 ros2 topic echo /boat0/odom --once ros2 topic echo /boat2/odom --once如果topic echo一直阻塞优先检查spawn_entity.py是否执行成功以及robot_state_publisher是否真的在/boat0/命名空间下发布了描述。另一个常见问题是三艘船共用了同一个 URDF 中的插件命名空间导致 Gazebo 差分驱动或 IMU 插件互相干扰这时候要在 URDF 里给每个插件加上唯一的name前缀否则仿真启动后话题和数据都是乱的。水面的 URDF 不需要建得和真实船体一样精细一个box的 hull link 加两个表示推进器位置的 link 就足够跑通集群逻辑。3.2 领航-跟随法控制器代码偏移、误差与参数怎么给三艘船的编队控制最常用的是领航-跟随法领航船按自主规划路径走跟随船通过跟踪领航船位姿加一个偏移量来计算自己的期望位置。这个方法实现简单、便于调试适合三到五艘船的规模船数量增多或者队形需要平行移动时虚拟结构法更合适因为所有船同时向各自虚拟锚点收敛误差不会像跟随法那样向后传播。我第一次做三船编队用的是虚拟结构法代码量会大一些但它能把队形变换做得更整齐。领航-跟随法控制器里最重要的参数是期望偏移量offset。以领航船为参考把领航船的 yaw 角旋转应用到偏移量上得到跟随船在地系下的目标点再让跟随船朝目标点收敛。这个旋转必须做否则领航船转弯时跟随船会切内线或者外线。控制器输出是线速度和角速度可以用两个独立的 P 控制器分别处理距离误差和航向误差。# swarm_controller.py —— 领航-跟随法boat1/boat2 跟踪 boat0 import math import rclpy from rclpy.node import Node from nav_msgs.msg import Odometry from geometry_msgs.msg import Twist def yaw_from_quat(q): 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)) class SwarmController(Node): def __init__(self, boat_name, leader_name): super().__init__(boat_name _swarm_controller) self.declare_parameter(offset, [0.0, -3.0]) self.declare_parameter(kp_distance, 0.8) self.declare_parameter(kp_angle, 1.2) self.declare_parameter(max_linear, 1.5) self.leader_name leader_name self.this_pose None self.leader_pose None self.create_subscription( Odometry, / self.leader_name /odom, self.on_leader, 10) self.create_subscription( Odometry, / boat_name /odom, self.on_self, 10) self.pub self.create_publisher( Twist, / boat_name /cmd_vel, 5) def on_leader(self, msg): self.leader_pose msg.pose.pose def on_self(self, msg): self.this_pose msg.pose.pose if self.leader_pose is None: return offset self.get_parameter(offset).value lx self.leader_pose.position.x ly self.leader_pose.position.y lyaw yaw_from_quat(self.leader_pose.orientation) # 把编队偏移从领航船坐标系转到世界坐标系 tx lx offset[0] * math.cos(lyaw) - offset[1] * math.sin(lyaw) ty ly offset[0] * math.sin(lyaw) offset[1] * math.cos(lyaw) # 本船到目标点的距离和航向差 dx tx - self.this_pose.position.x dy ty - self.this_pose.position.y dist math.hypot(dx, dy) self_yaw yaw_from_quat(self.this_pose.orientation) heading_err math.atan2(dy, dx) - self_yaw heading_err math.atan2(math.sin(heading_err), math.cos(heading_err)) kp_d self.get_parameter(kp_distance).value kp_a self.get_parameter(kp_angle).value max_v self.get_parameter(max_linear).value cmd Twist() # 距离接近 0.2m 时停船避免在水面反复震荡 cmd.linear.x min(kp_d * dist, max_v) if dist 0.2 else 0.0 cmd.angular.z kp_a * heading_err self.pub.publish(cmd) def main(): rclpy.init() node SwarmController(boat1, boat0) rclpy.spin(node) if __name__ __main__: main()启动这个控制器时船1的命令是ros2 run boat_control swarm_controller --ros-args -p offset:[0.0,-3.0]船2用offset:[0.0,3.0]这样三艘船形成一条横队。kp_distance和kp_angle的初始值分别为 0.8 和 1.2船体约三米长、最高航速 1.5m/s 时这个组合能保持稳定如果起漂先把kp_angle降到 0.6再把kp_distance降到 0.5。max_linear必须限制否则跟随船追上领航船时会过冲然后来回震荡。代码里heading_err用atan2(sin, cos)把角度差折叠到 [-pi, pi]这是所有航向控制器的通用防坑细节不做这一步角度差等于 350 度时控制器会朝另一侧打满舵。3.3 用 rqt_graph 和 ros2 topic hz 验证编队通信仿真跑起来以后先用可视化工具确认通信拓扑是否符合预期再开始调队形。rqt_graph是最直接的检查方式它能显示当前所有节点和话题的连线。我一般重点看三件事每条船的swarm_controller是否同时订阅了领航船和本船的 odom三艘船的cmd_vel发布者是否有正确命名空间是否有节点把话题发到了全局名字/cmd_vel而不是/boatX/cmd_vel。这些在图纸上看着没问题一旦跑起来就会发现某些节点因为use_sim_time不一致或者命名空间没设置对数据流和预期完全不同。# 验证三条船之间数据通路是否正常 ros2 topic list | grep odom ros2 topic hz /boat0/cmd_vel ros2 topic hz /boat1/cmd_vel # 在 RQt 里可视化整个集群的通信拓扑 rqt_graphros2 topic hz的输出能直接反映控制频率。如果cmd_vel频率远低于控制循环配置的 10Hz说明上游 odom 更新不够或者订阅端的 QoS 队列深度太小如果频率忽高忽低说明 multicore 环境下某个节点把 CPU 占满需要用top或ros2 topic delay进一步排查。仿真阶段把三艘船的编队误差控制在 0.5 米以内再上实船才有意义Gazebo 里本来就包含理想化因素实船的水流和风浪至少会让误差翻倍。仿真通过之后录制一份 ros2 bag 留作后续对比然后就可以进入实船部署阶段。4. 实船部署的六大避坑记录从时钟同步到无线断连4.1 时间不同步导致回放轨迹前后错位现象三艘船各自记录轨迹回放时发现船的位置和岸基日志对不上编队误差计算出来莫名其妙一直偏大。原因每条船用独立工控机系统时间各自走各的有的快几秒有的慢几秒。ROS2 消息自带时间戳但很多自定义节点里用self.get_clock().now()记录而不是采用 GPS 时间源导致集群控制器在比较“我什么时候收到领航船的位置”时拿到的领航船数据其实是好几秒之前的。解决每条船装上支持 PPS 秒脉冲的 GPS 模块用 chrony 把系统时间锁定到 GPS 时间。chrony 配置里加一行refclock PPS 0 poll 3并把local stratum设为 10 作为备用时间源。岸基电脑则用 NTP 指向任意一条船的 IP所有设备统一时间基准。仿真阶段则要让每条船的所有节点都开use_sim_time:true否则仿真时钟和真实时钟混用回放时会看到船在跳着走。4.2 无线丢包让队形从可控变成发散现象两船距离拉开到一百米以上或者中间有桥墩遮挡时跟随船突然偏离队形误差从 3 米扩大到 10 米以上而且很难自动拉回来。原因控制指令走的是 RELIABLE QoS无线链路丢包后 DDS 不断重传旧包把有效带宽占满跟随船实际收到的指令频率降低到 1-2Hz控制器来不及修正航向。这是水面集群最容易翻车的地方因为陆地 WiFi 环境丢包率低RELIABLE 的缺点表现不出来。解决把/boatX/cmd_vel、/boatX/odom这类高频链路全部改成 BEST_EFFORT depth 5。无线链路无遮挡时把控制频率降到 5Hz有遮挡时再降一档不要追求 20Hz 的指令更新水面船体的惯性比轮式机器人大多了5Hz 足够。如果仍然丢包在岸基做一个航迹预测用最近三帧位置外推跟随船的期望目标点把副作用平均到预测里。前面 2.2 小节那张 QoS 表是实船验证过之后定下来的直接照抄即可。4.3 控制频率与 DDS 队列深度不匹配现象GPS 明明是 20Hz订阅端处理得也不慢但ros2 topic hz /boat1/gps/fix只有 3Hz。原因发布端节点的循环频率是 50Hz但 DDS writer 的队列深度是 1GPS 消息到达时如果上一帧还没被取走就直接覆盖统计下来的实际有效到达率自然低。还有一种情况是发布端和订阅端在同一个进程里串行执行发布端在等待订阅端处理完成整个链路被拖慢。解决传感器节点用单线程加高优先级队列不和其他控制逻辑写进同一个可执行文件。GPS 驱动发布 QoS 的 depth 设为 20控制指令深度按 2.2 的表来导航状态和原始日志拆成两个话题一个 BEST_EFFORT 给控制器实时用一个 RELIABLE 给录制回放用不要让日志的可靠性拖累控制链路。排查时用ros2 topic hz分别看发布端和订阅端的消息频率如果发布端正常、订阅端偏低问题通常就出在 QoS 队列和节点线程模型上。4.4 多船同名话题串扰追上来的不是队友现象船1启动后一直接收到一个奇怪的速度指令船头不停摆动rqt_graph里发现/cmd_vel这个全局话题同时连着三条船。原因为了图省事有些节点代码里直接把话题名写成了绝对路径/cmd_vel、/odom没有加命名空间。在单船程序里这种写法没问题三条船一跑起来所有船都在发布和订阅同一个全局话题形成消息串扰。解决所有话题名一律使用相对名称cmd_vel、odom、gps/fix靠 launch 文件里的namespace隔离。在实际工程里还要检查代码里有没有残留的硬编码绝对路径。加一个检查习惯每次启动集群后先跑ros2 topic list | grep boat确认所有话题都有/boatX/前缀如果看到裸的/odom或/cmd_vel直接定位是哪艘船的哪个节点没写对。4.5 GPS 跳变被当成真实位移现象船在岸边停着集群控制器却认为它往航道外移动了 1 米然后自动给一个反向修正船身轻微抖动编队误差在小数点上波动。原因单点 GPS 在桥墩、建筑物和水面反射严重的区域会出现多路径效应位置输出瞬间跳变。如果这个跳变直接输入 PID 控制器它会被当作真实位移进行积分船自然就被拉偏。解决在 GPS 驱动节点里加一个速率门限滤波器当前帧与上一帧位置距离超过 1.5m/s 对应的阈值时丢弃当前帧并继续用上一帧。更稳的做法是用robot_localization把 IMU 和 GPS 融合输出/odomGPS 的协方差按实际精度设置比如单点定位 1.0、RTK 定位 0.2这样融合后的位姿不会跟着单帧跳变走。如果船需要近距离编队间距小于 2 米单点 GPS 根本不够用必须上 RTK这是我在两船间距 1.5 米测试时踩出的硬教训。4.6 电池电压下降后同一套 PID 不再适用现象刚下水时编队控制很正常跑二十分钟后船开始浑身发抖转弯半径也变大看起来像是 PID 参数漂了。原因电池从满电 25.2V 掉到 22V 时相同 PWM 占空比下推进器输出的推力明显下降相当于控制器执行增益变小。PID 参数本身没变但系统被控对象的增益变了原有的kp_distance和kp_angle就不再匹配。解决把电池电压采集进控制系统作为前馈补偿。最简单的做法是在推进器驱动节点里按电压修正 PWMpwm_out pwm_base k_v * (v_nominal - v_battery)电压越高补偿越小电压越低补偿越大。同时把swarm_controller里的kp乘以一个电压系数v_battery / v_nominal让控制器增益随电源衰减自适应。如果不做这一步每块电池老化程度不一样同一套代码在不同船上的表现也会有差异排查时还会被误判成 PID 参数问题。5. 让集群方案真正可用行为树任务、岸基监管与编队质量验证5.1 行为树接管任务切换低电量返航与断连保位集群控制满足了编队还不够任务层必须能处理异常。如果用状态机写任务切换状态多了以后逻辑会非常乱尤其在“正在覆盖搜索时突然收到低电量告警”“跟随船失联 3 秒后自动转入原地保位”这类组合场景下状态机加标志位很容易漏掉边界。行为树的好处是每个叶子节点只做一个判断或一个动作组合靠序列和回退优先级表达新增任务不需要改动原有逻辑。常见做法是用py_trees_ros或 BehaviorTree.CPP 写一个岸基任务节点把“执行任务、低电量返航、通信中断保位”三个行为挂到一个回退节点下低电量和通信异常优先级更高。每条船继续保持自己的swarm_controller任务切换只改变目标点来源不重新启动控制器这样切换过程不会产生控制抖动。5.2 岸基监管与人工接管最后一道安全网编队控制器再稳水面上也不能做到百分之百不出意外。我会在岸基加一个监管节点它订阅所有船的 odom 和 cmd_vel实时判断两两间距是否低于安全半径、是否越出作业边界。一旦触发监管节点直接向对应船发布一个高优先级的cmd_vel_override话题local_planner收到 override 后暂停编队指令执行原地停车或朝安全点回退。人工接管也是一样岸基遥控手柄或遥控 App 发来的指令以 override 形式注入不需要去关闭船上的任何节点。这个设计把“编队控制”和“安全保障”解耦开任何时候人工都有一票否决权这对无人船是必须的因为水上没有刹车撞一次代价不小。5.3 编队质量验证录 ros2 bag 后算编队 RMSE最后讲一个我每次测试都会做的验证手段把三艘船的 odom 话题录制成 ros2 bag测试结束后用离线脚本计算编队 RMSE。这个指标能客观回答“编队到底稳不稳”而不是靠肉眼盯着 rviz 猜。# 录制一次 10 分钟的编队测试数据 ros2 bag record -o formation_test \ /boat0/odom /boat1/odom /boat2/odom \ /boat0/cmd_vel /boat1/cmd_vel /boat2/cmd_vel # 回放录制的数据同时启动离线评估节点 ros2 bag play formation_test ros2 run boat_control formation_auditor评估节点的逻辑不复杂订阅每条船的 odom按照第一二章的编队偏移关系计算船1、船2与领航船的实时距离再和期望编队尺寸对比每秒钟统计一次 RMSE 和最大瞬时误差。这个脚本跑完你会得到一份“编队质量”报告RMSE 小于 0.5 米说明控制参数基本合格0.5 到 1.0 米说明能维持队形但波动偏大超过 1.0 米就要回头查无线丢包率、GPS 精度和控制频率。这个习惯帮我避免了很多“看起来还行”的回归没有客观记录代码改一个参数后编队是否真的变好单靠现场感觉会骗人。我自己的习惯是每次实船测试前先在仿真里录一版相同路径的 bag实船 test 后拿两组 RMSE 对比差距在合理范围内再继续调差距异常大优先怀疑定位和通信而不是急着改 PID。希望这个从仿真到实船的闭环思路能帮你在无人船集群控制方向上少走弯路更多的时间花在队形算法和任务决策上而不是被底层通信问题反复折腾。本文还有配套的精品资源点击获取
返回列表