简介:本资源为基于ROS2框架的智能轮椅自主导航系统完整工程包,面向机器人工程、自动化等专业的毕业设计、课程设计与期末大作业场景,也适合希望入门ROS2自主导航开发的学习者。项目整合传感器、执行器与导航算法,涵盖URDF建模、Gazebo仿真、AMCL定位、move_base路径规划及A*插件配置等核心模块,可帮助读者理解从建模到导航的完整链路。压缩包共42个文件,约1.34MB,以STL网格模型、YAML参数配置、launch启动脚本为主,另含world仿真环境、rviz可视化配置、URDF机器人描述及README说明文档,目录按data、map、meshes、launch、world、config等模块划分,结构清晰便于检索。目前已有80人学习下载。读者可据此快速搭建仿真环境、复现导航流程,并参考持续集成配置与参数组织方式,为自身项目开发与算法验证提供可复用的工程模板。
1. 智能轮椅自主导航:从 ROS2 话题到轮速闭环,这套系统到底怎么落地
一台普通电动轮椅加上激光雷达和 Jetson 之后,最常翻车的地方不是算法,而是「轮速指令发下去了,车不动」或者「建完图第二天开机定位漂到隔壁房间」。基于 ROS2 的智能轮椅自主导航系统,本质是把一套移动机器人栈塞进轮椅这个特殊载体:用 ROS2 做节点通信,用激光雷达做 SLAM 建图与定位,用 Nav2 做路径规划与动态避障,最后通过串口或 CAN 把速度指令下发给轮椅控制器。它解决的是行动不便人群在室内外固定场景下的点到点自主移动问题,适合有 ROS2 基础、手上有可改装电动轮椅或底盘、想跑通完整导航链路的开发者。热词里 ros2 humble、ros2激光雷达slam建图、ros2动态避障、micro-ros agent 这些,恰好就是这条链路上的关键节点。下面按「先立住理论、再动手复现、最后避坑」的顺序拆开讲。
2. 轮椅底盘与 ROS2 的通信层:先让轮子听懂话
2.1 为什么通信层是第一个要啃的硬骨头
智能轮椅和普通差速小车最大的区别在于底盘控制器。多数电动轮椅原厂控制器是封闭的,要么用摇杆模拟量输入,要么有厂商私有 CAN 协议。你要做的第一件事,是找到「速度指令入口」和「轮速反馈出口」。常见做法有三种:一是保留原控制器,用继电器或模拟量板卡模拟摇杆信号;二是绕过原控制器,直接换用支持 ROS2 的电机驱动器;三是通过 CAN 总线读取原控制器状态并注入指令。第一种成本最低但精度差,第二种最干净但改动大,第三种最考验逆向能力。
从 ROS2 角度看,这一层要产出一个标准接口:订阅/cmd_vel(geometry_msgs/Twist),发布/odom(nav_msgs/Odometry)和/tf。只要这两个话题通了,上层 Nav2 就能工作。热词里提到的 ros2 humble 串口桥接 esp32 小车,思路完全一样,只是把 ESP32 换成轮椅控制器。我一般会先写一个独立的wheel_bridge节点,把通信层和导航层解耦,这样调试时不用每次都启动整个导航栈。
2.2 用 Python 写一个最小可用的串口桥接节点
下面这个节点订阅/cmd_vel,把线速度和角速度换算成左右轮速,通过串口发给下位机,同时以固定频率读取编码器反馈并发布/odom。这是整个系统里最该先跑通的一段代码。
import rclpy from rclpy.node import Node from geometry_msgs.msg import Twist from nav_msgs.msg import Odometry import serial import struct import math class WheelBridge(Node): def __init__(self): super().__init__('wheel_bridge') # 参数:串口设备、波特率、轮距、轮径、编码器线数 self.declare_parameter('port', '/dev/ttyUSB0') self.declare_parameter('baudrate', 115200) self.declare_parameter('wheel_base', 0.52) # 左右轮中心距,单位米 self.declare_parameter('wheel_radius', 0.10) # 轮半径,单位米 self.declare_parameter('ticks_per_rev', 1024) self.wheel_base = self.get_parameter('wheel_base').value self.wheel_radius = self.get_parameter('wheel_radius').value self.ticks_per_rev = self.get_parameter('ticks_per_rev').value self.ser = serial.Serial( self.get_parameter('port').value, self.get_parameter('baudrate').value, timeout=0.05 ) self.sub = self.create_subscription(Twist, '/cmd_vel', self.cmd_cb, 10) self.pub = self.create_publisher(Odometry, '/odom', 10) self.timer = self.create_timer(0.02, self.read_odom) # 50Hz 读取 self.x = 0.0 self.y = 0.0 self.th = 0.0 self.last_ticks = [0, 0] def cmd_cb(self, msg): v = msg.linear.x w = msg.angular.z # 差速运动学逆解 v_left = v - w * self.wheel_base / 2.0 v_right = v + w * self.wheel_base / 2.0 # 转成下位机可识别的整数指令,这里假设单位是 mm/s left_cmd = int(v_left * 1000) right_cmd = int(v_right * 1000) packet = struct.pack('<Bhh', 0xAA, left_cmd, right_cmd) self.ser.write(packet) def read_odom(self): # 假设下位机每帧返回 0xBB + 左ticks(int32) + 右ticks(int32) if self.ser.in_waiting >= 9: data = self.ser.read(9) if data[0] == 0xBB: left_ticks, right_ticks = struct.unpack('<ii', data[1:9]) d_left = (left_ticks - self.last_ticks[0]) / self.ticks_per_rev * 2 * math.pi * self.wheel_radius d_right = (right_ticks - self.last_ticks[1]) / self.ticks_per_rev * 2 * math.pi * self.wheel_radius self.last_ticks = [left_ticks, right_ticks] d_center = (d_left + d_right) / 2.0 d_theta = (d_right - d_left) / self.wheel_base self.x += d_center * math.cos(self.th) self.y += d_center * math.sin(self.th) self.th += d_theta odom = Odometry() odom.header.stamp = self.get_clock().now().to_msg() odom.header.frame_id = 'odom' odom.child_frame_id = 'base_link' odom.pose.pose.position.x = self.x odom.pose.pose.position.y = self.y odom.pose.pose.orientation.z = math.sin(self.th / 2) odom.pose.pose.orientation.w = math.cos(self.th / 2) odom.twist.twist.linear.x = d_center / 0.02 odom.twist.twist.angular.z = d_theta / 0.02 self.pub.publish(odom) def main(): rclpy.init() node = WheelBridge() rclpy.spin(node) rclpy.shutdown() if __name__ == '__main__': main()逻辑说明:cmd_cb做的是差速运动学逆解,把整车线速度/角速度拆成左右轮线速度,再乘 1000 转成毫米每秒的整数指令。read_odom做的是正解,把编码器增量还原成位移和角度,用航迹推算更新位姿。参数里wheel_base和wheel_radius必须实测,量错 1 厘米,转一圈后角度误差就能累积到十几度。ticks_per_rev要查驱动器手册或自己转一圈数脉冲。串口协议里的0xAA、0xBB是自定义帧头,下位机固件必须对应实现,否则就是「发出去没反应」的经典翻车现场。
2.3 参数标定与验证方法
标定分两步。第一步静态验证:用ros2 topic pub手动发一个linear.x=0.1,看轮椅是否直线前进 1 米;发angular.z=0.5,看是否原地转约 28 度(0.5 rad)。第二步动态验证:推着轮椅走一段已知直线,对比/odom累积位移和实际卷尺测量值,误差应小于 5%。如果角速度方向反了,把wheel_base前的符号或左右轮定义对调即可。热词里的 ros2小乌龟 其实就是这套逻辑的简化版,turtlesim 里cmd_vel怎么驱动乌龟,这里就怎么驱动轮椅,只是多了真实硬件的不确定性。
3. 激光雷达 SLAM 建图:让轮椅先认识环境
3.1 选 slam_toolbox 还是 cartographer
ROS2 Humble 下做 2D 激光 SLAM,主流就两个选择:slam_toolbox 和 cartographer_ros。slam_toolbox 是 ROS2 原生维护、配置简单、在线建图和定位模式切换方便,适合室内轮椅场景;cartographer 建图质量在回环处理上略强,但配置复杂、对时间同步敏感。我一般先用 slam_toolbox 跑通,因为它的online_async模式对 CPU 占用低,Jetson 上也能实时。热词里 ros2激光雷达slam建图 的诉求,用 slam_toolbox 是最短路径。
雷达选型上,轮椅场景推荐 2D 单线雷达(如 RPLIDAR 系列或思岚 A 系列),安装高度建议在 30 到 50 厘米之间,避开轮椅扶手和腿部遮挡。如果要做 3D 建图或八叉树地图导航,那就得上 3D 雷达加 octomap,但轮椅室内导航 2D 足够,别过度设计。
3.2 建图启动文件与关键参数
下面是一个最小 launch 文件,同时拉起雷达驱动、slam_toolbox 和 RViz2。
from launch import LaunchDescription from launch_ros.actions import Node from launch.actions import IncludeLaunchDescription from launch.launch_description_sources import PythonLaunchDescriptionSource from ament_index_python.packages import get_package_share_directory import os def generate_launch_description(): slam_dir = get_package_share_directory('slam_toolbox') rplidar_dir = get_package_share_directory('rplidar_ros') return LaunchDescription([ IncludeLaunchDescription( PythonLaunchDescriptionSource( os.path.join(rplidar_dir, 'launch', 'rplidar_a1_launch.py') ) ), IncludeLaunchDescription( PythonLaunchDescriptionSource( os.path.join(slam_dir, 'launch', 'online_async_launch.py') ), launch_arguments={ 'use_sim_time': 'false', 'slam_params_file': os.path.join( get_package_share_directory('wheelchair_nav'), 'config', 'mapper_params_online_async.yaml' ) }.items() ), Node( package='rviz2', executable='rviz2', arguments=['-d', os.path.join( get_package_share_directory('wheelchair_nav'), 'config', 'slam.rviz' )] ) ])逻辑说明:这个 launch 把雷达、SLAM、可视化三件事串起来。online_async_launch.py是 slam_toolbox 自带的,通过slam_params_file传入自定义参数。参数文件里最关键的几项:max_laser_range设成雷达实际量程(比如 12 米),resolution设 0.05 米,map_update_interval设 1.0 秒,minimum_travel_distance设 0.3 米。建图时推着轮椅慢速走,转弯处多停两秒让雷达扫全,回环闭合靠的是loop_search_maximum_distance,默认 3 米,走廊长的话可以调到 5 米。
3.3 建图质量检查与地图保存
建完图别急着导航,先看三件事:墙壁是否双层(说明位姿漂移)、直角是否歪斜(说明雷达安装角或轮距标定有误)、回环处是否闭合(说明回环检测没生效)。检查方法是在 RViz2 里叠加/map和/scan,推着轮椅回到起点,看激光点是否和地图墙体重合。保存地图用:
ros2 run nav2_map_server map_saver_cli -f ~/wheelchair_map这会生成wheelchair_map.pgm和wheelchair_map.yaml。yaml 里的resolution、origin要和建图时一致,negate和occupied_thresh一般不用改。地图保存后,下次导航就用map_server加载它,配合 AMCL 做定位。热词里 ros2 humble gazebo panda 那套仿真流程,建图部分逻辑相通,只是仿真里没有真实雷达噪声,建出来的图过于完美,反而不能反映真实调参难度。
4. Nav2 导航栈配置:从全局规划到动态避障
4.1 Nav2 的插件架构与轮椅适配点
Nav2 在 Humble 里已经是成熟栈,核心是行为树(BT)驱动一堆插件服务器:planner_server管全局路径,controller_server管局部跟踪,behavior_server管恢复行为,bt_navigator管调度。轮椅场景要改的点有三个:一是controller_server的max_vel_x要压低,轮椅安全速度建议 0.3 到 0.6 米每秒;二是inflation_radius要加大,因为轮椅比小车宽,膨胀层至少覆盖半宽加 10 厘米;三是progress_checker的超时时间要放宽,轮椅加速慢,容易误判卡住。
热词里 ros2动态避障 的落地,靠的是controller_server里的 DWB 或 MPPI 局部规划器。DWB 在 Humble 里稳定,MPPI 更平滑但对算力要求高。Jetson Orin 以下建议 DWB,参数好调,社区案例多。
4.2 nav2_params.yaml 里必须改的六个参数
下面这张表列出轮椅场景和默认值的差异,照着改能省很多调试时间。
| 参数 | 默认值 | 轮椅建议值 | 原因 |
|---|---|---|---|
| max_vel_x | 0.26 | 0.4 | 轮椅电机响应慢,太快容易失控 |
| max_vel_theta | 1.0 | 0.6 | 原地转向对轮椅乘客不适 |
| acc_lim_x | 2.5 | 0.8 | 限制加速度,防止急冲 |
| inflation_radius | 0.55 | 0.75 | 轮椅宽度通常 0.6 米以上 |
| xy_goal_tolerance | 0.25 | 0.15 | 轮椅需要更精准停靠 |
| transform_tolerance | 0.1 | 0.3 | 串口通信有延迟,放宽 TF 容忍 |
改完参数后,启动导航:
ros2 launch nav2_bringup bringup_launch.py \ map:=/home/user/wheelchair_map.yaml \ params_file:=/home/user/wheelchair_nav/config/nav2_params.yaml \ use_sim_time:=false然后在 RViz2 里用2D Pose Estimate给初始位姿,用2D Goal Pose发目标点。如果轮椅不动,先看/cmd_vel有没有输出,再看controller_server的日志有没有报No valid control,通常是膨胀层把路堵死了。
4.3 动态避障的调试技巧
动态避障效果取决于局部代价地图的更新频率和障碍物膨胀策略。local_costmap的update_frequency建议设 10Hz,obstacle_layer的scan话题要确认是雷达实际输出。如果轮椅遇到行人后原地打转,多半是RotateToGoal行为被触发但角速度限制太死,把max_vel_theta临时调回 0.8 试试。热词里 ros2话题服务动作 的区分在这里很实用:/cmd_vel是话题,/compute_path_to_pose是动作,调试时用ros2 action list能看到 Nav2 暴露的所有动作接口,比盲猜日志快得多。
5. 避坑与排查:轮椅导航里最容易翻车的五件事
5.1 现象:建图时墙壁重影,导航时定位跳变
原因:轮式里程计标定不准,或者雷达和底盘之间的 TF 变换有静态偏差。轮椅的轮径和轮距如果靠估算,走 10 米就能差出半米。解决:用卷尺实测轮径(压上负载后的有效半径)和轮距,重新标定ticks_per_rev;雷达安装位置用static_transform_publisher精确到厘米,别用目测。
5.2 现象:Nav2 规划出路径但轮椅不动,/cmd_vel有数据
原因:串口桥接节点的速度指令单位或符号和下位机不匹配。常见的是 ROS 里线速度单位是 m/s,下位机期望 mm/s,忘了乘 1000;或者左右轮定义反了,直行指令变成原地转。解决:先用ros2 topic echo /cmd_vel确认数值合理,再用串口调试助手直接发指令看轮子响应,逐段隔离。
5.3 现象:轮椅在走廊尽头反复倒车,无法通过窄门
原因:膨胀半径过大,把可通行宽度压没了;或者全局规划器GridBased的tolerance太小,找不到可行路径。解决:实测门宽,膨胀半径设为「轮椅半宽 + 5 厘米」而不是拍脑袋;窄门场景可以把inflation_radius临时降到 0.5,同时把cost_scaling_factor调大,让代价衰减更快。
5.4 现象:AMCL 定位在长走廊里漂移,粒子发散
原因:长走廊激光特征单一,AMCL 的laser_model_type用likelihood_field时容易退化。解决:改用likelihood_field_prob并开启do_beamskip;同时把laser_max_range设成雷达实际量程的 80%,避免远处噪声干扰;走廊里可以贴一些反光标记或改变墙面材质增加特征。
5.5 现象:Jetson 上导航延迟大,RViz2 卡顿
原因:所有节点跑在同一台机器上,SLAM、Nav2、RViz2 抢 CPU;或者雷达驱动发布频率过高。解决:RViz2 放到笔记本上通过 ROS2 网络查看,Jetson 只跑导航和桥接;雷达降频到 10Hz 足够导航用;用ros2 run topic_tools throttle对/scan限流。热词里 ros2 零拷贝 在 Jetson 上对图像话题有效,但激光雷达数据量小,收益有限,别为了零拷贝折腾半天。
6. 进阶:用行为树定制轮椅的停靠与让行逻辑
Nav2 默认行为树能跑通点到点导航,但轮椅场景有些特殊需求:靠近目标点时减速更平缓、遇到人时主动靠边让行、到达后自动切换驻车模式。这些都可以通过改行为树 XML 实现,不用动 C++ 插件。下面是一个简化示例,在NavigateToPose的FollowPath之后插入一个自定义的SlowDownNearGoal条件节点。
<BehaviorTree ID="MainTree"> <RecoveryNode number_of_retries="6" name="NavigateRecovery"> <PipelineSequence name="NavigateWithReplanning"> <ControllerSelector selected_controller="{selected_controller}" default_controller="FollowPath"/> <PlannerSelector selected_planner="{selected_planner}" default_planner="GridBased"/> <RateController hz="1.0"> <ComputePathToPose goal="{goal}" path="{path}" planner_id="GridBased"/> </RateController> <FollowPath path="{path}" controller_id="FollowPath"/> <SlowDownNearGoal distance_threshold="0.5" speed_limit="0.15"/> </PipelineSequence> <SequenceStar name="RecoveryActions"> <ClearEntireCostmap name="ClearLocalCostmap" service_name="local_costmap/clear_entirely_local_costmap"/> <Spin spin_dist="1.57"/> <Wait wait_duration="3"/> </SequenceStar> </RecoveryNode> </BehaviorTree>逻辑说明:SlowDownNearGoal是自定义 BT 节点,在距离目标 0.5 米内把速度上限压到 0.15 m/s,避免轮椅急停。实现方式是在你的 ROS2 包里继承BT::SyncActionNode,注册到bt_navigator的插件列表里。参数distance_threshold和speed_limit按实际乘坐体验调,一般 0.3 到 0.5 米、0.1 到 0.2 m/s 比较舒适。
验证方法:在 RViz2 里发一个近距离目标点,观察/cmd_vel的linear.x是否在接近时平滑下降。如果没生效,检查bt_navigator的plugin_lib_names里有没有加你的节点库,以及行为树 XML 是否被正确加载(ros2 param get /bt_navigator default_bt_xml_filename)。
我自己踩过的最大坑是:行为树改完忘了重新编译,colcon build只编了包但没 source,结果跑的还是旧树,排查了两小时才发现是环境没刷新。现在的习惯是每次改 BT 或参数,先source install/setup.bash再启动,并且在 launch 里打印当前加载的 XML 路径。轮椅导航这方向值得做,但硬件标定和通信层的功夫占七成,算法只占三成,别本末倒置。希望帮到你。
本文还有配套的精品资源,点击获取