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

资讯详情

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

Jetson Nano上ROS服务通信实战:从概念到调试全解析

Jetson Nano上ROS服务通信实战:从概念到调试全解析 1. 项目缘起与核心目标最近在Jetson Nano上折腾ROS跟着赵虚左老师的《ROS理论与实践》课程一路学下来到了第三章服务通信这块。说实话之前话题通信和订阅发布模型玩得挺溜但服务通信Service这个“一问一答”的模式刚开始接触时总觉得有点别扭不像话题那样“滔滔不绝”。尤其是在资源受限的Jetson NanoUbuntu 18.04 ROS Melodic上从零开始搭建服务通信的客户端Client和服务端Server每一步都可能遇到在x86电脑上不太会出现的“坑”。比如Python节点的依赖管理、ROS Master的通信稳定性还有在ARM架构下一些库的细微差别。这篇文章就是我在Nano上“啃”下服务通信这一章的实战记录。我不会照本宣科地复述教程而是聚焦于如何在真实的、资源有限的嵌入式开发板上把一个ROS服务从概念变成可稳定运行的代码。你会看到我如何创建功能包、编写Python服务端和客户端、处理参数传递以及最关键的部分——在Nano上调试服务通信时遇到的典型问题及其解决方案。无论你是刚拿到Jetson Nano的ROS新手还是在其他嵌入式平台学习ROS希望这些踩坑经验能帮你少走弯路。2. 服务通信模型深度解析为什么是“一问一答”在开始敲代码之前我们必须先彻底搞懂服务通信Service在ROS架构里到底扮演什么角色它和之前学的发布订阅Topic本质区别在哪里。很多人一开始会混淆觉得不就是发送接收消息吗但它们的应用场景和通信模式截然不同。2.1 服务 vs. 话题同步与异步的本质区别你可以把话题通信想象成广播电台。一个节点发布者不断地向某个频道话题发送消息它并不关心有没有听众订阅者也不关心听众收到后会不会回复。这是一种单向、异步、多对多的通信。比如你的传感器节点持续发布摄像头图像数据到/camera/image话题导航节点订阅它来进行路径规划。发布者只管发订阅者只管收两者在时间上是解耦的。服务通信则更像是一次电话呼叫。客户端Client发起一个具体的请求Request然后主动等待直到服务端Server处理完这个请求并返回一个响应Response。这是一个双向、同步、一对一的请求-响应模型。客户端在收到响应之前其调用线程通常是阻塞的当然也可以实现异步调用。典型的应用场景包括让机器人移动到某个指定位置客户端发送目标坐标服务端执行移动并返回是否成功、查询传感器状态、或者执行一次计算任务如求解逆运动学。在赵虚左老师的课程中这个模型被用于实现两个整数求和的服务客户端发送两个整数a和b服务端计算ab并返回结果。这个例子虽小但完美诠释了服务的核心客户端需要得到一个明确的、即时的答案。2.2 服务通信在资源受限设备上的特殊考量在Jetson Nano这类设备上理解这个模型尤为重要。因为同步调用意味着客户端在等待响应时会占用一个线程资源。如果服务端处理缓慢比如涉及大量计算或者网络有延迟客户端就可能被长时间阻塞影响整个系统的实时性。因此在设计Nano上的ROS服务时我们必须问自己几个问题服务处理是否耗时如果耗时是否可以考虑将任务拆解或用话题状态机的方式实现异步通知服务的响应时间要求多高这决定了我们能否接受同步阻塞。服务端是否可能成为瓶颈Nano的CPU性能有限一个设计不良的重计算服务可能拖垮整个节点。对于我们的加法服务例子计算是瞬时的所以同步模型非常合适。但如果你要在Nano上做一个图像识别服务客户端发送一张图片服务端用深度学习模型推理后返回结果这个处理过程可能就需要几百毫秒甚至更久。这时你就需要仔细设计比如在服务端内部使用多线程或队列来处理并发请求避免一个慢请求阻塞后续所有请求。3. Jetson Nano开发环境搭建与功能包创建工欲善其事必先利其器。在Nano上做ROS开发第一步就是确保你的基础环境是干净、可用的。我使用的是Ubuntu 18.04 with ROS Melodic这是官方为Nano长期支持的版本组合稳定性最好。3.1 基础ROS环境验证在创建我们自己的功能包之前先打开终端运行几个命令确保ROS核心功能正常# 1. 检查ROS环境是否已正确加载 echo $ROS_PACKAGE_PATH # 应该输出包含/opt/ros/melodic/share的路径 # 2. 启动ROS Master和基础日志节点在一个终端运行 roscore保持roscore运行。这是所有ROS节点进行通信的“总机”服务通信同样依赖它来查找和连接服务端。3.2 创建专属的功能包PackageROS的代码是以功能包为单元组织的。我们将创建一个专门用于练习服务通信的包。# 切换到你的ROS工作空间src目录下例如 ~/catkin_ws/src cd ~/catkin_ws/src # 使用catkin_create_pkg命令创建包。这里的关键是 # learning_service包名。 # std_msgs rospy roscpp依赖项。 # std_msgs: 包含了ROS标准消息类型我们定义服务时会用到。 # rospy: 我们要用Python编写节点所以需要Python客户端库。 # roscpp: 虽然本篇用Python但通常也加上C依赖以备后用。 catkin_create_pkg learning_service std_msgs rospy roscpp创建成功后你会看到learning_service目录里面自动生成了CMakeLists.txt和package.xml。在Nano上我强烈建议在创建包后立即执行一次工作空间的编译即使还没写代码。这可以提前暴露一些环境配置问题。cd ~/catkin_ws catkin_make source devel/setup.bash3.3 创建服务定义文件.srv服务通信的核心是数据格式的约定。客户端发送什么Request服务端返回什么Response都需要在一个.srv文件中明确定义。这个文件相当于服务的“接口合同”。在我们的加法服务例子中合同很简单请求是两个整数响应是一个整数之和。在learning_service包目录下创建srv文件夹cd ~/catkin_ws/src/learning_service mkdir srv在srv目录下创建AddTwoInts.srv文件内容如下# 请求部分客户端发送 int64 a int64 b --- # 响应部分服务端返回 int64 sum注意三个短横线---是分隔符上方是请求下方是响应。int64是ROS内置的基本数据类型对应Python的int。3.4 配置编译系统以识别服务定义创建了.srv文件后ROS编译系统并不会自动处理它。我们需要修改两个配置文件修改package.xml确保它声明了生成消息所需的依赖。 找到buildtool_dependcatkin/buildtool_depend在其后添加build_dependmessage_generation/build_depend exec_dependmessage_runtime/exec_dependmessage_generation是编译时生成消息/服务代码的依赖message_runtime是运行时依赖。修改CMakeLists.txt这是关键步骤容易出错。找到find_package确保它包含了message_generation。find_package(catkin REQUIRED COMPONENTS roscpp rospy std_msgs message_generation # 添加这一行 )找到add_service_files取消注释并添加我们的.srv文件。add_service_files( FILES AddTwoInts.srv )找到generate_messages确保它被调用并且依赖了std_msgs。generate_messages( DEPENDENCIES std_msgs )找到catkin_package确保CATKIN_DEPENDS中包含了message_runtime。catkin_package( # INCLUDE_DIRS include # LIBRARIES learning_service CATKIN_DEPENDS roscpp rospy std_msgs message_runtime # 确保有message_runtime # DEPENDS system_lib )3.5 编译并验证服务生成配置完成后回到工作空间根目录进行编译cd ~/catkin_ws catkin_make编译成功后最关键的一步来了验证服务定义是否已正确生成Python代码。这是很多新手会卡住的地方尤其是在交叉编译或环境配置有误时。# 首先重新source环境使新生成的包生效 source devel/setup.bash # 然后尝试导入生成的服务模块 python3 -c from learning_service.srv import AddTwoInts, AddTwoIntsRequest, AddTwoIntsResponse; print(Service import successful!)如果这条命令能成功执行没有报ImportError恭喜你服务定义的环境已经准备就绪。如果失败请依次检查CMakeLists.txt和package.xml的修改是否正确。编译过程是否有错误输出。是否执行了source devel/setup.bash。在Nano上我习惯把这条命令加到~/.bashrc末尾避免每次开终端都要手动source。4. 手把手编写Python服务端与客户端环境搞定现在进入核心编码环节。我们将分别创建服务端节点server.py和客户端节点client.py。4.1 服务端Server实现等待与响应服务端的职责是初始化一个ROS节点。向ROS Master注册一个服务例如/add_two_ints。进入循环等待客户端的请求。收到请求后执行处理逻辑这里是加法计算。将处理结果封装成响应返回给客户端。在learning_service包下创建scripts文件夹如果不存在然后创建server.pycd ~/catkin_ws/src/learning_service mkdir -p scripts touch scripts/server.py chmod x scripts/server.py # 赋予可执行权限编辑server.py代码如下#!/usr/bin/env python3 # -*- coding: utf-8 -*- import rospy from learning_service.srv import AddTwoInts, AddTwoIntsResponse def handle_add_two_ints(req): 服务请求处理回调函数。 当客户端调用服务时ROS会调用此函数并传入一个AddTwoIntsRequest对象这里命名为req。 rospy.loginfo(Received request: a%d, b%d, req.a, req.b) # 执行核心逻辑加法计算 sum_val req.a req.b rospy.loginfo(Returning response: [%d %d %d], req.a, req.b, sum_val) # 返回一个AddTwoIntsResponse对象。注意在Python中可以直接返回一个元组或一个Response对象。 # 这里我们显式创建Response对象更清晰。 return AddTwoIntsResponse(sum_val) def add_two_ints_server(): 初始化服务端节点。 # 初始化节点节点名必须唯一。匿名节点anonymousTrue可以避免多次启动时的命名冲突。 rospy.init_node(add_two_ints_server, anonymousTrue) # 创建服务Service。 # 参数1服务名称客户端将通过这个名称来调用服务。 # 参数2服务类型即我们定义的AddTwoInts。 # 参数3处理回调函数收到请求时自动调用。 s rospy.Service(add_two_ints, AddTwoInts, handle_add_two_ints) rospy.loginfo(Ready to add two ints.) # rospy.spin()使节点保持运行直到节点被关闭。它会阻塞在这里等待请求到来。 rospy.spin() if __name__ __main__: try: add_two_ints_server() except rospy.ROSInterruptException: pass代码要点与Nano实操注意#!/usr/bin/env python3指定解释器为python3。在Ubuntu 18.04上默认的python命令可能指向Python 2.7而ROS Melodic同时支持Python 2和3。明确使用python3可以避免版本混乱。rospy.init_node节点名add_two_ints_server。如果多次启动同一个节点名ROS会报错。anonymousTrue参数会在节点名后附加一个随机数便于调试。rospy.Service这是服务创建的核心。服务名/add_two_ints是客户端查找的依据。rospy.spin()这是一个非常重要的调用。它让节点进入等待循环不会让脚本立即退出。如果没有它你的服务端节点注册完服务后就退出了客户端根本找不到它。4.2 客户端Client实现发起请求与接收响应客户端的职责是初始化一个ROS节点。等待指定的服务可用rospy.wait_for_service。创建一个服务代理rospy.ServiceProxy。构造请求数据AddTwoIntsRequest或直接传参。调用服务代理并获取响应。在scripts目录下创建client.pytouch scripts/client.py chmod x scripts/client.py编辑client.py代码如下#!/usr/bin/env python3 # -*- coding: utf-8 -*- import sys import rospy from learning_service.srv import AddTwoInts, AddTwoIntsRequest def add_two_ints_client(x, y): 服务客户端函数。 # 首先等待名为add_two_ints的服务变得可用。 # 这是一个好习惯避免服务端还没启动客户端就尝试调用导致失败。 rospy.loginfo(Waiting for service add_two_ints to become available...) rospy.wait_for_service(add_two_ints) rospy.loginfo(Service available!) try: # 创建服务代理Service Proxy。这就像拿到了服务的“电话号码”。 add_two_ints_proxy rospy.ServiceProxy(add_two_ints, AddTwoInts) # 方法一直接传递参数ROS会帮你封装成Request。这是最简洁的方式。 rospy.loginfo(Calling service with args: %d, %d, x, y) resp1 add_two_ints_proxy(x, y) rospy.loginfo(Response (direct args): %d, resp1.sum) # 方法二显式创建Request对象再调用。这种方式更直观便于理解数据结构。 req AddTwoIntsRequest() req.a 10 req.b 20 rospy.loginfo(Calling service with request object: a%d, b%d, req.a, req.b) resp2 add_two_ints_proxy(req) # 也可以写成 add_two_ints_proxy(req.a, req.b) rospy.loginfo(Response (request object): %d, resp2.sum) return resp1.sum, resp2.sum except rospy.ServiceException as e: # 服务调用可能失败例如服务端处理异常、通信超时等。 rospy.logerr(Service call failed: %s, e) return None, None if __name__ __main__: # 初始化客户端节点。注意节点名不要和服务端重复。 rospy.init_node(add_two_ints_client, anonymousTrue) # 从命令行参数获取要相加的两个数如果没有提供则使用默认值1和2。 if len(sys.argv) 3: try: x int(sys.argv[1]) y int(sys.argv[2]) except ValueError: rospy.logerr(Invalid arguments. Using defaults (1, 2).) x, y 1, 2 else: rospy.loginfo(No arguments provided. Using defaults (1, 2).) x, y 1, 2 rospy.loginfo(Requesting addition of %d and %d, x, y) result add_two_ints_client(x, y) if result[0] is not None: rospy.loginfo(Final results: %d and %d, result[0], result[1])代码要点与Nano实操注意rospy.wait_for_service这是客户端健壮性的关键。在Nano上由于启动顺序或系统负载服务端可能比客户端晚一点准备好。不加这行代码客户端可能立即调用失败。你可以设置超时参数例如rospy.wait_for_service(add_two_ints, timeout5)表示最多等5秒。rospy.ServiceProxy创建代理后调用它就像调用一个本地函数一样。但要注意这是一个阻塞调用。在收到响应或超时之前程序会停在这里。对于耗时服务需要考虑使用多线程或异步调用roscpp中有async方法rospy中需结合多线程实现。命令行参数客户端设计了从sys.argv读取参数的功能这样你可以灵活测试rosrun learning_service client.py 5 10。5. 在Jetson Nano上运行与深度调试代码写完了最激动人心的环节就是让它跑起来。在Nano上运行ROS节点你需要打开至少两个终端或者使用tmux、screen这类终端复用工具。5.1 启动服务与调用服务终端1启动ROS Master和roscore如果还没启动roscore终端2启动服务端节点# 确保已经source了工作空间的环境 source ~/catkin_ws/devel/setup.bash # 启动服务端 rosrun learning_service server.py如果一切正常你将看到输出[INFO] [时间戳]: Ready to add two ints.终端3启动客户端节点source ~/catkin_ws/devel/setup.bash # 使用默认参数1和2 rosrun learning_service client.py # 或者指定参数 rosrun learning_service client.py 100 200在客户端终端你应该看到类似以下的输出[INFO] [时间戳]: Waiting for service add_two_ints to become available... [INFO] [时间戳]: Service available! [INFO] [时间戳]: Requesting addition of 100 and 200 [INFO] [时间戳]: Calling service with args: 100, 200 [INFO] [时间戳]: Response (direct args): 300 [INFO] [时间戳]: Calling service with request object: a10, b20 [INFO] [时间戳]: Response (request object): 30 [INFO] [时间戳]: Final results: 300 and 30同时在服务端终端你会看到[INFO] [时间戳]: Received request: a100, b200 [INFO] [时间戳]: Returning response: [100 200 300] [INFO] [时间戳]: Received request: a10, b20 [INFO] [时间戳]: Returning response: [10 20 30]恭喜一个完整的ROS服务通信流程已经在你的Jetson Nano上跑通了。5.2 使用命令行工具进行探测与调试除了运行节点ROS提供了强大的命令行工具来观察系统状态这对于调试至关重要。rosservice list列出当前系统中所有已注册的服务。运行客户端前后各执行一次可以看到/add_two_ints服务的出现。rosservice info /add_two_ints查看某个服务的详细信息包括服务类型、提供该服务的节点Node、URI等。rosservice type /add_two_ints查看服务的类型输出应为learning_service/AddTwoInts。rosservice call /add_two_ints [args]最强大的工具你可以不写客户端代码直接用命令行调用服务。rosservice call /add_two_ints a: 5 b: 3注意YAML格式a:和b:后面有空格。如果成功会返回sum: 8。这个功能在快速测试服务端逻辑时非常有用。5.3 Jetson Nano专属问题排查实录在Nano上我遇到了几个典型问题这里分享排查思路问题一ImportError: No module named learning_service.srv现象运行rosrun时Python报错找不到自定义的服务模块。排查检查编译首先确认catkin_make是否成功且没有关于learning_service的报错。检查环境运行echo $ROS_PACKAGE_PATH查看你的工作空间路径如/home/nano/catkin_ws/src是否在其中。每次新开终端都必须source devel/setup.bash。检查生成文件到~/catkin_ws/devel/lib/python2.7/dist-packages/learning_servicePython2或~/catkin_ws/devel/lib/python3/dist-packages/learning_servicePython3目录下查看是否有srv文件夹以及里面的_AddTwoInts.py等文件。在Ubuntu 18.04 ROS Melodic的默认配置下catkin_make生成的文件可能在Python2.7的目录下但你的脚本用python3运行。这是最常见的坑解决方案A推荐在CMakeLists.txt中强制指定生成Python3的代码。在find_package(catkin...)之前或之后添加# 设置使用Python3 find_package(PythonLibs 3 REQUIRED) # 或者如果上述不行尝试更明确地指定PYTHON_EXECUTABLE # set(PYTHON_EXECUTABLE /usr/bin/python3)然后重新catkin_make。但这有时会因ROS Melodic的默认配置而复杂。方案B实用直接修改你的Python脚本在文件最开头#!/usr/bin/env python3之后添加以下代码将Python3的site-packages路径加入搜索路径import sys sys.path.append(/home/nano/catkin_ws/devel/lib/python2.7/dist-packages)将/home/nano替换为你的实际家目录。这是一个“硬编码”的解决方案不够优雅但能快速解决问题。方案C一劳永逸将你的工作空间devel目录下的Python3路径如果存在或Python2路径通过PYTHONPATH环境变量永久导出。在~/.bashrc中添加export PYTHONPATH$PYTHONPATH:/home/nano/catkin_ws/devel/lib/python2.7/dist-packages然后source ~/.bashrc。问题二服务调用超时或无响应现象客户端一直卡在Waiting for service...或者调用后长时间无响应然后超时。排查检查roscore确保roscore正在运行。用rosnode list查看应该能看到/rosout。检查服务端节点用rosnode list查看/add_two_ints_server是否存在。用rosnode info /add_two_ints_server查看其状态和发布/订阅的服务。检查网络配置多机通信时如果在多台机器上运行需要正确设置ROS_MASTER_URI和ROS_HOSTNAME。在Nano单机上通常没问题。检查防火墙虽然单机本地通信一般不受影响但可以检查一下是否有防火墙规则阻止了本地回环地址的通信极少见。解决确保启动顺序先roscore再服务端最后客户端。使用rospy.wait_for_service并合理设置超时时间。问题三服务端处理函数崩溃导致客户端无响应现象服务端在处理请求时抛出未捕获的异常例如除零错误、类型转换错误导致整个服务端节点崩溃。客户端会一直等待直到超时默认超时时间可能很长。模拟与解决在服务端的handle_add_two_ints函数里故意制造一个错误def handle_add_two_ints(req): rospy.loginfo(Received request: a%d, b%d, req.a, req.b) # 模拟一个运行时错误 if req.b 0: result req.a / req.b # 除零错误 else: result req.a req.b return AddTwoIntsResponse(result)当客户端传入b0时服务端崩溃客户端卡住。教训服务端的回调函数内部一定要做好异常捕获try-except并返回一个合理的错误响应或者使用rospy.logerr记录日志后返回。一个健壮的服务端应该像这样def handle_add_two_ints(req): try: rospy.loginfo(Received request: a%d, b%d, req.a, req.b) sum_val req.a req.b # 或者更复杂的逻辑 return AddTwoIntsResponse(sum_val) except Exception as e: rospy.logerr(Error processing request: %s, e) # 可以返回一个特殊的错误码或者重新抛出异常让ROS框架处理不推荐 # 简单起见这里返回一个标识错误的结果例如-1 return AddTwoIntsResponse(-1)6. 从加法服务到真实场景服务通信的进阶思考通过加法例子我们掌握了服务通信的基本流程。但在真实的机器人项目中服务通信的应用要复杂和有趣得多。在Jetson Nano这样的边缘计算设备上服务通信常常扮演着“决策中枢”或“任务触发器”的角色。6.1 典型应用场景拓展设备控制与状态查询场景一个主控节点通过服务命令Nano上的相机节点“开始录制”或“停止录制”。相机节点执行操作后返回“成功”或“失败”。实现要点服务定义可能包含命令枚举如START, STOP和状态码。服务端需要处理硬件资源如相机句柄的并发访问安全。算法服务化场景在Nano上部署了一个训练好的神经网络模型如目标检测。其他节点如感知融合节点将图像数据通过服务请求发送过来Nano上的算法服务节点运行推理并将检测结果边框、类别通过响应返回。实现要点这是计算密集型服务。必须考虑服务端的并发处理能力。简单的rospy.Service是单线程的一个请求处理完才处理下一个。对于耗时的推理这会成为性能瓶颈。解决方案是使用多线程或ROS的动作库ActionLib。动作库更适合长时间运行、可抢占、有反馈的任务。配置与参数设置场景机器人启动后通过服务动态设置某个算法模块的参数如PID控制器的Kp, Ki, Kd。实现要点服务端需要将接收到的参数更新到内存中的配置变量并可能持久化到文件。同时要考虑参数校验。6.2 服务通信的局限性及替代方案认识到服务的“同步阻塞”特性是其优势也是劣势后我们就知道它不是万能的。局限性阻塞客户端客户端在等待响应时不能做其他事。无中间状态反馈对于长时间任务客户端只知道开始和结束不知道进度。服务端需高可用如果服务端崩溃所有客户端请求都会失败。不适合持续数据流像传感器数据这种持续不断的流用话题更合适。替代方案选择需要简单请求-响应且处理快用服务Service。本文内容。需要长时间运行、有进度反馈、可取消的任务用动作Action。例如导航到目标点。需要单向、持续、多对多的数据流用话题Topic。例如激光雷达数据。需要动态配置、且希望持久化用参数服务器Parameter Server配合动态重配置dynamic_reconfigure更专业。6.3 在Nano上优化服务性能的实践技巧服务端多线程化使用Python的threading库或rospy的MultiThreadedService。这样服务端可以同时处理多个客户端请求。但要注意线程安全和资源竞争。# 示例创建多线程服务ROS Melodic的rospy似乎没有直接的MultiThreadedService通常用ThreadPool from concurrent.futures import ThreadPoolExecutor import rospy from learning_service.srv import AddTwoInts, AddTwoIntsResponse executor ThreadPoolExecutor(max_workers4) # 根据Nano的CPU核心数调整 def handle_add_two_ints(req): # ... 处理逻辑 ... return AddTwoIntsResponse(sum_val) def add_two_ints_server(): rospy.init_node(add_two_ints_server_mt) # 注意标准rospy.Service是单线程的。 # 要实现多线程一种模式是服务回调函数内部将任务提交给线程池并立即返回一个“已接收”的响应。 # 真正的结果通过另一个话题或回调机制返回。这其实更接近“动作”的模式。 # 对于简单的计算保持单线程可能更简单安全。 s rospy.Service(add_two_ints_mt, AddTwoInts, handle_add_two_ints) rospy.spin() executor.shutdown()注意对于简单的加法服务多线程是杀鸡用牛刀。但对于图像处理、规划算法等耗时操作这种模式值得考虑。更规范的做法是直接使用ROS的ActionLib。客户端设置超时使用rospy.wait_for_service和rospy.ServiceProxy.call时都提供超时参数避免客户端无限期挂起。# 等待服务最多5秒 rospy.wait_for_service(add_two_ints, timeout5) # 调用服务最多3秒 try: resp add_two_ints_proxy.call(req, timeoutrospy.Duration(3)) except rospy.ROSException as e: rospy.logwarn(Service call timed out: %s, e)精简服务数据在Nano和上位机或其他设备通信时网络带宽可能有限。确保你的.srv消息定义只包含必要字段避免传输大图像等数据考虑用话题传输图像用服务传输一个触发指令或小尺寸的处理结果。
返回列表