公司动态
基于ROS的机器人感知-决策-执行系统构建实战
最近在技术圈里小米汽车工厂的实训经历和其新一代人形机器人的亮相引发了大量关于机器人技术、AI集成以及智能制造落地的讨论。对于开发者而言这背后涉及的是一个庞大而复杂的技术栈从机械控制、传感器融合到上层AI决策每一步都充满了挑战与机遇。本文将从一名软件开发者的视角切入探讨如何构建一个简化但完整的“机器人核心控制系统”原型。我们将聚焦于软件层面模拟机器人的感知、决策与执行流程使用Python和ROS机器人操作系统的基础概念搭建一个可在仿真环境中运行的示例项目。无论你是对机器人技术感兴趣的在校学生还是希望了解AI如何与实体硬件结合的软件工程师都能通过本文的实践掌握从零搭建一个机器人控制循环的核心思路。1. 背景与核心概念从工厂实训到软件定义机器人小米在汽车工厂进行为期数月的实训其目的远不止于造车。现代汽车工厂是机器人技术、自动化流水线和工业物联网的集大成者。这种环境为研发人形机器人提供了绝佳的试验场高精度的机械臂、复杂的移动底盘AGV、海量的传感器数据以及需要实时响应的生产节拍。对于软件开发者理解机器人系统关键在于理解其“感知-思考-行动”Sense-Think-Act的循环以及实现这一循环的软件框架。感知Sense机器人通过各类传感器如摄像头、激光雷达、IMU、力觉传感器获取环境信息。在软件层面这对应着数据采集、滤波、融合等模块。例如将摄像头图像和激光雷达点云数据进行对齐和融合得到更可靠的环境模型。思考Think基于感知信息机器人需要做出决策。这包括路径规划、任务调度、姿态控制、AI识别与推理。例如识别前方障碍物是人还是箱子并决定是绕行还是交互。行动Act将决策转化为具体的物理动作。这对应着运动控制、伺服驱动、通信协议。例如向机器人的关节电机发送一组精确的位置或力矩指令。为了实现这些复杂模块的高效协作与解耦ROSRobot Operating System成为了机器人领域事实上的标准中间件。它不是传统意义上的操作系统而是一个运行在Linux之上的分布式通信框架提供了节点Node、话题Topic、服务Service、动作Action等核心通信机制让感知、决策、执行等模块可以独立开发通过标准接口连接。本文的实战案例将围绕一个模拟的“抓取-放置”任务展开使用ROS的核心通信模式构建一个简化的软件系统。2. 环境准备与版本说明在开始编码前我们需要搭建开发环境。本文示例基于Ubuntu 20.04 LTS和ROS Noetic这是目前长期支持且社区资源丰富的组合。如果你使用其他Linux发行版或ROS版本如ROS2 Galactic/Humble核心概念相通但命令和API可能需要调整。2.1 基础环境安装首先确保你的系统是Ubuntu 20.04然后按照ROS官方文档安装ROS Noetic桌面完整版。# 1. 设置软件源 sudo sh -c echo deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main /etc/apt/sources.list.d/ros-latest.list sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv-key C1CF6E31E6BADE8868B172B4F42ED6FBAB17C654 # 2. 更新软件包索引并安装ROS sudo apt update sudo apt install ros-noetic-desktop-full # 3. 初始化rosdep sudo rosdep init rosdep update # 4. 设置环境变量每次打开新终端都需要执行或写入~/.bashrc echo source /opt/ros/noetic/setup.bash ~/.bashrc source ~/.bashrc # 5. 安装构建工具和依赖 sudo apt install python3-rosinstall python3-rosinstall-generator python3-wstool build-essential2.2 创建工作空间与项目结构ROS代码通常组织在工作空间Workspace中。我们创建一个名为robot_sim_ws的工作空间。# 创建并进入工作空间目录 mkdir -p ~/robot_sim_ws/src cd ~/robot_sim_ws/src # 初始化工作空间 catkin_init_workspace # 回到工作空间根目录并编译 cd ~/robot_sim_ws catkin_make # 激活工作空间环境同样可写入~/.bashrc source ~/robot_sim_ws/devel/setup.bash现在我们的项目基础环境就准备好了。接下来将在src目录下创建我们自己的功能包Package。3. 核心原理与ROS通信模型拆解在编写具体代码前必须理解ROS的几种核心通信机制这决定了我们如何架构机器人软件。3.1 节点Node节点是ROS中可执行的进程是完成具体功能的软件模块。例如一个节点负责读取摄像头数据另一个节点负责识别物体第三个节点负责控制机械臂。我们的系统将由多个节点组成。3.2 话题Topic与消息Message话题是节点间进行异步数据交换的通道采用发布/订阅Publisher/Subscriber模型。数据以消息的形式在话题上传递。消息有严格的数据结构定义.msg文件。示例一个camera_node发布Image消息到/camera/rgb话题一个object_detection_node订阅该话题以获取图像进行处理。3.3 服务Service服务实现了请求/响应Request/Response模型的同步通信。客户端节点发送请求服务端节点处理并返回响应。示例一个navigation_node提供GetPath服务task_manager_node调用该服务请求从A点到B点的路径规划结果。3.4 动作Action动作是对服务的扩展用于处理长时间运行、可抢占、有反馈的任务。它包含目标Goal、反馈Feedback和结果Result。示例控制机械臂抓取物体。arm_controller_node提供一个GraspObject动作。brain_node发送抓取目标并在抓取过程中持续接收“正在移动”、“已接触物体”等反馈最终获得“抓取成功”或“失败”的结果。我们的“抓取-放置”demo将综合运用这些模型。4. 完整实战案例构建抓取-放置机器人软件系统我们将创建四个ROS节点模拟一个完整的控制流程感知节点(perception_node): 模拟发布虚拟的物体检测结果。决策节点(brain_node): 接收感知信息决策任务流程调用服务和动作。导航服务节点(navigation_service_node): 提供路径规划服务。机械臂动作节点(arm_action_node): 提供抓取和放置的动作服务器。4.1 创建功能包首先在工作空间的src目录下创建我们的功能包它依赖于roscpp,rospy,std_msgs,actionlib,actionlib_msgs。cd ~/robot_sim_ws/src catkin_create_pkg robot_demo rospy roscpp std_msgs actionlib actionlib_msgs message_generation message_runtime cd robot_demo4.2 定义自定义消息、服务和动作我们需要定义通信所用的数据结构。创建msg目录和物体位置消息:mkdir msg echo -e string object_id\nfloat32 x\nfloat32 y\nfloat32 z msg/ObjectPosition.msg创建srv目录和导航服务:mkdir srv echo -e float32 start_x\nfloat32 start_y\nfloat32 start_z\nfloat32 goal_x\nfloat32 goal_y\nfloat32 goal_z\n---\nbool success\nstring message\ngeometry_msgs/Pose[] path srv/PlanPath.srv注意这里为了简化直接使用了geometry_msgs/Pose数组。实际需要确保geometry_msgs依赖已添加。更规范的做法是自定义一个Pose消息。创建action目录和抓取动作:mkdir action echo -e # Goal Definition\nstring object_id\nfloat32 pick_x\nfloat32 pick_y\nfloat32 pick_z\nfloat32 place_x\nfloat32 place_y\nfloat32 place_z\n---\n# Result Definition\nbool success\nstring status_message\n---\n# Feedback Definition\nstring current_state\nfloat32 completion_percentage action/GraspAndPlace.action4.3 修改package.xml和CMakeLists.txt为了让ROS能编译我们自定义的接口需要修改两个配置文件。package.xml: 确保包含以下行通常catkin_create_pkg已添加大部分build_dependmessage_generation/build_depend exec_dependmessage_runtime/exec_depend exec_dependactionlib_msgs/exec_dependCMakeLists.txt: 找到相应部分并修改/添加find_package(catkin REQUIRED COMPONENTS roscpp rospy std_msgs actionlib actionlib_msgs message_generation # 添加 ) add_message_files( FILES ObjectPosition.msg ) add_service_files( FILES PlanPath.srv ) add_action_files( FILES GraspAndPlace.action ) generate_messages( DEPENDENCIES std_msgs actionlib_msgs # 如果使用了geometry_msgs也需要添加在这里 ) catkin_package( CATKIN_DEPENDS roscpp rospy std_msgs actionlib actionlib_msgs message_runtime )4.4 编写节点代码Python示例我们使用Python编写节点因其原型开发速度快。在robot_demo/scripts目录下创建Python文件先创建scripts目录。脚本 1:perception_node.py(感知节点)#!/usr/bin/env python3 import rospy from robot_demo.msg import ObjectPosition import random def perception_node(): rospy.init_node(perception_node, anonymousTrue) # 发布到 /detected_objects 话题 pub rospy.Publisher(/detected_objects, ObjectPosition, queue_size10) rate rospy.Rate(1) # 1Hz rospy.loginfo(感知节点启动开始模拟发布物体位置...) object_id 0 while not rospy.is_shutdown(): # 模拟检测到一个随机位置的物体 obj_pos ObjectPosition() obj_pos.object_id fobj_{object_id} obj_pos.x random.uniform(0.5, 1.5) obj_pos.y random.uniform(-0.5, 0.5) obj_pos.z 0.8 # 假设桌子高度 pub.publish(obj_pos) rospy.loginfo(f发布物体: {obj_pos.object_id} 在位置 ({obj_pos.x:.2f}, {obj_pos.y:.2f}, {obj_pos.z:.2f})) object_id 1 rate.sleep() if __name__ __main__: try: perception_node() except rospy.ROSInterruptException: pass脚本 2:navigation_service_node.py(导航服务节点)#!/usr/bin/env python3 import rospy from robot_demo.srv import PlanPath, PlanPathResponse from geometry_msgs.msg import Pose, Point, Quaternion import math def handle_plan_path(req): rospy.loginfo(f收到路径规划请求: 从({req.start_x},{req.start_y},{req.start_z}) 到 ({req.goal_x},{req.goal_y},{req.goal_z})) # 模拟一个简单的路径规划算法这里直接返回一条直线上的几个点 resp PlanPathResponse() resp.success True resp.message Path planned successfully. num_points 5 for i in range(num_points): pose Pose() t i / (num_points - 1) if num_points 1 else 0 pose.position.x req.start_x (req.goal_x - req.start_x) * t pose.position.y req.start_y (req.goal_y - req.start_y) * t pose.position.z req.start_z (req.goal_z - req.start_z) * t # 简单朝向目标点 pose.orientation.w 1.0 resp.path.append(pose) rospy.loginfo(f规划路径完成包含 {len(resp.path)} 个路径点。) return resp def navigation_service_node(): rospy.init_node(navigation_service_node) s rospy.Service(/plan_path, PlanPath, handle_plan_path) rospy.loginfo(导航服务已启动等待请求...) rospy.spin() if __name__ __main__: navigation_service_node()脚本 3:arm_action_node.py(机械臂动作节点)#!/usr/bin/env python3 import rospy import time import actionlib from robot_demo.msg import GraspAndPlaceAction, GraspAndPlaceFeedback, GraspAndPlaceResult class GraspAndPlaceServer: def __init__(self): self.server actionlib.SimpleActionServer(grasp_and_place, GraspAndPlaceAction, self.execute_callback, False) self.server.start() rospy.loginfo(机械臂动作服务器已启动。) def execute_callback(self, goal): rospy.loginfo(f收到抓取放置动作目标: 抓取 {goal.object_id} 从 ({goal.pick_x},{goal.pick_y},{goal.pick_z}) 放置到 ({goal.place_x},{goal.place_y},{goal.place_z})) result GraspAndPlaceResult() feedback GraspAndPlaceFeedback() # 模拟执行过程并发送反馈 stages [移动至抓取点, 执行抓取, 抬起物体, 移动至放置点, 执行放置, 返回待命位置] for i, stage in enumerate(stages): if self.server.is_preempt_requested(): rospy.logwarn(动作被抢占) result.success False result.status_message Preempted self.server.set_preempted(result) return # 模拟该阶段耗时 time.sleep(1.0) feedback.current_state stage feedback.completion_percentage (i 1) / len(stages) * 100 self.server.publish_feedback(feedback) rospy.loginfo(f状态: {stage} - 完成度 {feedback.completion_percentage:.1f}%) # 动作完成 result.success True result.status_message Grasp and place completed successfully. self.server.set_succeeded(result) rospy.loginfo(抓取放置动作执行成功) def arm_action_node(): rospy.init_node(arm_action_node) server GraspAndPlaceServer() rospy.spin() if __name__ __main__: arm_action_node()脚本 4:brain_node.py(决策节点)#!/usr/bin/env python3 import rospy from robot_demo.msg import ObjectPosition from robot_demo.srv import PlanPath, PlanPathRequest import actionlib from robot_demo.msg import GraspAndPlaceAction, GraspAndPlaceGoal class BrainNode: def __init__(self): rospy.init_node(brain_node) # 订阅感知话题 rospy.Subscriber(/detected_objects, ObjectPosition, self.object_detected_callback) # 等待导航服务可用 rospy.loginfo(等待导航服务 /plan_path ...) rospy.wait_for_service(/plan_path) self.plan_path_client rospy.ServiceProxy(/plan_path, PlanPath) # 连接机械臂动作服务器 rospy.loginfo(连接机械臂动作服务器 grasp_and_place ...) self.arm_client actionlib.SimpleActionClient(grasp_and_place, GraspAndPlaceAction) if not self.arm_client.wait_for_server(rospy.Duration(5.0)): rospy.logerr(动作服务器未响应) return rospy.loginfo(决策节点初始化完成等待物体检测...) def object_detected_callback(self, msg): rospy.loginfo(f决策节点: 检测到新物体 {msg.object_id} 在 ({msg.x}, {msg.y}, {msg.z})) # 决策逻辑执行一次抓取放置任务 self.execute_grasp_and_place_task(msg) def execute_grasp_and_place_task(self, object_msg): try: # 1. 规划到抓取点的路径 (假设机器人起始在原点(0,0,0)) rospy.loginfo(步骤1: 规划到抓取点的路径...) nav_req PlanPathRequest() nav_req.start_x, nav_req.start_y, nav_req.start_z 0.0, 0.0, 0.0 nav_req.goal_x, nav_req.goal_y, nav_req.goal_z object_msg.x, object_msg.y, object_msg.z nav_resp self.plan_path_client(nav_req) if not nav_resp.success: rospy.logerr(f路径规划失败: {nav_resp.message}) return rospy.loginfo(f路径规划成功获得 {len(nav_resp.path)} 个路径点。) # 2. 发送抓取放置动作目标 rospy.loginfo(步骤2: 发送抓取放置指令给机械臂...) goal GraspAndPlaceGoal() goal.object_id object_msg.object_id goal.pick_x, goal.pick_y, goal.pick_z object_msg.x, object_msg.y, object_msg.z # 假设放置点在另一个固定位置 goal.place_x, goal.place_y, goal.place_z 2.0, 0.0, 0.8 self.arm_client.send_goal(goal) # 等待动作完成并可以在这里处理反馈 self.arm_client.wait_for_result() result self.arm_client.get_result() if result.success: rospy.loginfo(f任务成功: {result.status_message}) else: rospy.logwarn(f任务失败: {result.status_message}) except rospy.ServiceException as e: rospy.logerr(f服务调用失败: {e}) except Exception as e: rospy.logerr(f任务执行异常: {e}) if __name__ __main__: try: node BrainNode() rospy.spin() except rospy.ROSInterruptException: pass4.5 赋予脚本执行权限并编译cd ~/robot_sim_ws/src/robot_demo chmod x scripts/*.py cd ~/robot_sim_ws catkin_make source devel/setup.bash4.6 运行与验证打开四个终端分别运行以下命令# 终端1: 启动ROS核心 roscore # 终端2: 启动感知节点 rosrun robot_demo perception_node.py # 终端3: 启动导航服务节点 rosrun robot_demo navigation_service_node.py # 终端4: 启动机械臂动作节点 rosrun robot_demo arm_action_node.py # 终端5: 启动决策节点 rosrun robot_demo brain_node.py4.7 结果说明观察各个终端的日志输出你将看到类似以下的信息流perception_node每秒发布一个虚拟物体位置。brain_node订阅到物体位置后会先调用/plan_path服务。navigation_service_node响应服务请求返回模拟的路径。brain_node接着向grasp_and_place动作服务器发送目标。arm_action_node开始执行并持续向brain_node发送反馈当前状态和完成百分比。最终动作执行完成返回成功结果。这个过程完整模拟了一个机器人从感知环境、做出决策、规划路径到执行复杂动作的闭环。你可以使用rostopic list,rostopic echo,rosservice list,rosservice call等命令来查看和调试系统中的话题、服务与消息。5. 常见问题与排查思路在开发和运行ROS机器人程序时经常会遇到一些问题。下表列出了一些典型问题及其解决方法问题现象可能原因排查思路与解决方案roscore启动失败或ROS_MASTER_URI错误环境变量未设置或设置错误端口被占用。1. 检查echo $ROS_MASTER_URI应为http://localhost:11311。2. 检查echo $ROS_HOSTNAME或echo $ROS_IP应为本机可访问地址。3. 确认11311端口未被占用netstat -tulpn | grep 11311。catkin_make编译失败提示找不到消息/服务/动作1.package.xml或CMakeLists.txt依赖配置错误。2. 自定义接口文件语法错误。3. 未先source devel/setup.bash。1. 仔细核对CMakeLists.txt中的find_package,add_message_files,generate_messages,catkin_package部分。2. 检查.msg,.srv,.action文件格式是否正确。3. 删除build和devel文件夹重新catkin_make并source。节点启动后订阅/发布或服务调用失败1. 节点名称、话题名称、服务名称拼写错误。2. 消息类型不匹配。3. 服务/动作服务器未启动。1. 使用rosnode list,rostopic list,rosservice list确认名称。2. 使用rostopic info topic_name和rosservice info service_name查看类型。3. 确保服务器节点先于客户端节点启动或客户端有重连机制。Python脚本运行时提示“ImportError: No module named ...”1. Python路径问题未找到自定义的ROS消息模块。2. 脚本没有执行权限。1. 确保已执行source ~/robot_sim_ws/devel/setup.bash。2. 检查脚本第一行是否为#!/usr/bin/env python3。3. 使用chmod x your_script.py添加执行权限。动作执行过程中无反馈或结果1. 动作服务器未正确发送反馈或设置结果。2. 客户端未处理反馈或未等待结果。1. 在服务器端execute_callback中检查publish_feedback和set_succeeded/set_aborted的调用。2. 在客户端检查send_goal后是否调用了wait_for_result并处理了返回值。6. 最佳实践与工程建议将demo代码转化为健壮、可维护的机器人软件系统需要遵循以下工程实践6.1 代码组织与架构功能包职责单一每个功能包应只负责一个明确的功能模块如perception_pkg,navigation_pkg,manipulation_pkg。使用Launch文件对于需要启动多个节点的系统务必编写.launch文件。这能统一管理参数、节点和命名空间极大简化启动流程。!-- robot_demo/launch/demo.launch -- launch node pkgrobot_demo typeperception_node.py nameperception outputscreen/ node pkgrobot_demo typenavigation_service_node.py namenavigation outputscreen/ node pkgrobot_demo typearm_action_node.py namearm_controller outputscreen/ node pkgrobot_demo typebrain_node.py namebrain outputscreen/ /launch启动命令简化为roslaunch robot_demo demo.launch6.2 通信与接口设计定义清晰的接口消息、服务、动作的定义要稳定、语义明确。一旦发布尽量避免修改以免破坏依赖它的其他节点。使用命名空间对于大型系统或多机器人系统使用命名空间如/robot1/perception,/robot2/navigation来隔离资源避免冲突。合理选择通信模型实时性要求高的流数据如传感器数据用话题需要确认结果的单次请求用服务长时间、可监控的任务用动作。6.3 鲁棒性与错误处理超时与重试服务调用、动作目标发送、等待服务器等操作都必须设置合理的超时并实现重试或降级逻辑。参数服务器将硬编码的配置如机器人尺寸、速度限制、目标位置移至ROS参数服务器支持动态配置和加载。异常捕获与日志像示例中一样使用try-except捕获异常并使用rospy.loginfo/warn/err分级记录日志便于调试和监控系统状态。6.4 仿真与测试善用Gazebo等仿真工具在投入真机前务必在Gazebo、RViz等仿真环境中充分测试算法和逻辑。可以订阅仿真环境发布的传感器话题并向仿真关节控制器发布控制指令。单元测试与集成测试对核心算法函数编写单元测试。使用rostest框架进行节点级的集成测试模拟输入话题消息并验证输出。6.5 向真实系统迁移硬件抽象层将控制硬件的代码如通过串口、CAN总线发送指令封装成独立的驱动节点。上层规划节点通过标准的ROS话题/服务与驱动节点交互实现软件与硬件的解耦。实时性考虑ROS1本身不是实时系统。对于高实时性要求的关节控制可能需要结合ros_control框架或直接使用实时操作系统RTOS与ROS桥接。通过以上步骤你不仅完成了一个机器人软件系统的原型搭建更掌握了构建此类系统的基本方法论。从汽车工厂的自动化到人形机器人的复杂操作其软件内核都遵循着类似的感知-决策-执行范式与模块化通信架构。理解并熟练运用ROS这套工具链是进入机器人软件开发领域的坚实一步。