ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ROS2服务通信原理与实战:从IDL定义到急停确认

ROS2服务通信原理与实战:从IDL定义到急停确认 1. 这不是“调用个函数”那么简单ROS2服务通信到底在解决什么问题ROS2服务通信这个词在ros2菜鸟教程、ros2学习笔记、ros2入门21讲里反复出现但很多人第一次看到“服务”两个字下意识会联想到HTTP API或者远程RPC——这其实是个危险的类比。我带过十几期ROS2机器人开发实训几乎每期都有学员卡在“为什么不能直接用话题topic传参数非要搞个服务”这个问题上。答案不在代码里而在机器人系统的真实运行逻辑中。举个最典型的例子你让小乌龟turtlesim画一个正方形它得先收到“启动移动”的指令再执行但如果你只是往/cmd_vel话题里塞速度指令它根本不知道“这次是画正方形的第一条边”还是“紧急避障的瞬时转向”。话题是广播式的、无状态的、持续流式的——就像教室里的广播喇叭谁都能听但没人知道你是不是在回应某个人。而服务service是点对点的、有请求-响应语义的、一次性的——它像办公室里敲门进领导办公室说“张工我需要您审批这个采购单”然后等他签完字还给你。这个“请求-响应”闭环才是机器人系统里动作触发、状态切换、参数配置、故障复位这类关键操作的底层契约。你看到的example_interfaces/srv/AddTwoInts表面看只是加法计算器但它背后承载的是ROS2服务通信的完整协议栈从客户端发起请求、序列化打包、通过底层DDS中间件传输、服务端反序列化解析、执行业务逻辑、构造响应、再原路返回。整个链路里rclcpp不是魔法它是C层对ROS2客户端库rcl的封装把底层DDS的复杂性屏蔽掉让你专注写request-a 1; request-b 2;这种直白代码。而AddTwoInts之所以被选为官方示例是因为它的IDL接口定义语言文件只有三行int64 a、int64 b、int64 sum没有数组、没有嵌套结构、没有可选字段——它刻意剔除了所有干扰项逼你直面服务通信最核心的骨架请求类型、响应类型、同步等待机制。所以当你在ubuntu 22.04安装ros2 humble后跑ros2 run examples_rclcpp_minimal_service service_main别只盯着终端里打印的“Ready to add two ints”要意识到此刻你的进程正在和DDS域里的另一个节点建立临时会话通道这个通道的生命周期严格绑定于一次请求-响应周期用完即销毁。它不像话题那样长期占用网络资源也不像动作action那样支持取消和反馈流。它的设计哲学就是“轻量、确定、原子”。这也是为什么在机械臂视觉抓取仿真中我们用服务来触发“拍照-识别-规划路径”这一整套流程的启动而不是用话题去广播“开始抓取”因为后者无法保证下游节点是否已就绪、是否已加载好模型、是否相机已校准——服务调用天然携带了“阻塞等待成功”的语义这是机器人系统可靠性的基石。2. 服务通信的底层脉络从IDL定义到rclcpp实现的全链路拆解ROS2服务通信不是凭空出现的它是一条从接口定义IDL开始贯穿编译生成、节点注册、序列化、DDS传输、反序列化、业务执行、响应回传的完整数据通路。很多初学者卡在“编译报错”或“节点找不到服务”根源往往是对这条链路上某个环节的误解。下面我带你一节一节剥开不跳过任何关键细节。2.1 IDL文件服务接口的唯一真相源所有ROS2服务都始于一个.srv文件比如AddTwoInts.srv。它的语法极其简洁但每一行都不可省略int64 a int64 b --- int64 sum注意那个---它不是分隔符而是IDL语法的强制标记上面是请求字段request下面是响应字段response。很多人误以为可以写成int64 a, b # 错不允许逗号分隔 --- int64 sum或者漏掉---结果rosidl_generator_cpp在生成头文件时会直接报错提示“invalid srv file format”。这是因为ROS2的IDL解析器是严格按行解析的它不支持任何语法糖。我曾经帮一个团队排查连续三天的编译失败最后发现是他们在.srv文件末尾多了一个空行导致解析器读到EOF前意外终止——这种细节在ros2手册里不会写但在实际工程中每天都在发生。生成的C头文件如add_two_ints.hpp里你会看到两个结构体Request_和Response_它们被包裹在一个ServiceType模板中。关键点在于Request_和Response_不是普通struct而是继承自rosidl_runtime_cpp::Message的类这意味着它们自带序列化/反序列化能力。当你写client-async_send_request(request, callback)时rclcpp内部会自动调用Request_::serialize()方法把a和b的值编码成二进制流再交给DDS发送。这个过程完全透明但理解它能帮你快速定位序列化错误——比如如果你在自定义服务里用了std::string而没在.srv里声明string data而是写了char[] data编译器会报错因为char[]不是ROS2支持的IDL基本类型。2.2 rclcpp服务端不是“写个函数”而是注册一个回调入口写服务端节点核心是两行代码auto service this-create_serviceexample_interfaces::srv::AddTwoInts( add_two_ints, std::bind(MinimalService::handle_add_two_ints, this, _1, _2));这里create_service的第二个参数是一个std::function它绑定了成员函数handle_add_two_ints。但很多人忽略了一个致命细节这个回调函数的签名必须严格匹配。正确的签名是void handle_add_two_ints( const std::shared_ptrexample_interfaces::srv::AddTwoInts::Request request, std::shared_ptrexample_interfaces::srv::AddTwoInts::Response response)注意request是shared_ptrresponse也是shared_ptr且response是非const引用。为什么因为rclcpp在调用你的回调前已经为你new好了Response对象并把指针传进来你的任务就是填充response-sum而不是new一个新对象再赋值——那样会导致内存泄漏。我见过太多人写成response std::make_sharedexample_interfaces::srv::AddTwoInts::Response(); // 错 response-sum request-a request-b;结果服务端永远不返回客户端一直阻塞。原因很简单rclcpp只认它自己分配的那个response对象的地址你make_shared出来的新对象它根本看不到。2.3 rclcpp客户端异步与同步的取舍远不止async_send_request和send_request的区别客户端有两种调用方式异步async_send_request和同步send_request。表面上看前者带callback后者返回Future但它们的线程模型差异巨大。send_request是阻塞式调用后当前线程会挂起直到收到响应或超时。适合在单线程节点里做简单测试比如ros2 run examples_rclcpp_minimal_client client_main。async_send_request是非阻塞式立即返回响应通过callback处理。但callback的执行线程由rclcpp的Executor决定——默认是SingleThreadedExecutor意味着所有callback都在同一个线程里串行执行。如果你的callback里有耗时操作比如调用OpenCV处理图像整个节点的其他callback包括定时器、话题订阅都会被卡住。解决方案是换用MultiThreadedExecutor或者更推荐的做法在callback里只做轻量级数据搬运把重计算放到独立线程。我在livox avia配置使用ros2的激光雷达数据预处理中就遇到过因服务callback阻塞导致IMU数据丢失的问题最终把点云滤波逻辑移到了std::thread里用std::promise和std::future与callback通信才彻底解决。提示send_request的超时时间单位是std::chrono::seconds但rclcpp::Duration::from_seconds(5)和std::chrono::seconds(5)在某些版本里行为不一致建议统一用rclcpp::Duration(5, 0)避免因精度问题导致超时失效。3. 从零搭建一个真实可用的服务以“机器人急停确认”为例的实操全流程光看AddTwoInts是学不会服务通信的。我带你做一个工业场景里真正用得上的服务EmergencyStopConfirm.srv。它的需求很明确当安全PLC检测到急停按钮被按下它需要向主控节点发送一个服务请求主控必须在500ms内返回确认否则PLC将切断动力电源。这个场景对实时性、可靠性、错误处理要求极高正好覆盖服务通信的所有关键点。3.1 定义服务接口IDL里的每一个字都是约束创建emergency_stop_confirm.srv# 请求字段PLC传来的唯一ID和时间戳 uint64 plc_id builtin_interfaces/msg/Time timestamp --- # 回应字段主控的确认状态和附加信息 bool confirmed string reason # 如OK, PLC_ID_MISMATCH, TIMEOUT注意两点builtin_interfaces/msg/Time是ROS2内置时间类型不是std_msgs::msg::Time也不是ros::Time那是ROS1的。用错类型rosidl生成会失败。reason字段用string而非char[32]因为ROS2的string是动态分配的能容纳任意长度描述而固定数组在IDL里不被支持。生成后你会得到emergency_stop_confirm.hpp里面Request_有两个成员plc_iduint64_t和timestampbuiltin_interfaces::msg::Time。Response_有confirmedbool和reasonstd::string。3.2 实现服务端不只是逻辑更是实时保障服务端节点emergency_stop_server.cpp的核心逻辑class EmergencyStopServer : public rclcpp::Node { public: EmergencyStopServer() : Node(emergency_stop_server) { // 注册服务注意服务名必须全小写符合ROS2命名规范 service_ this-create_servicecustom_interfaces::srv::EmergencyStopConfirm( emergency_stop_confirm, [this](const std::shared_ptrcustom_interfaces::srv::EmergencyStopConfirm::Request request, std::shared_ptrcustom_interfaces::srv::EmergencyStopConfirm::Response response) { // 关键所有实时操作必须在回调内完成不能跨线程 auto now this-now(); auto diff_ms (now - request-timestamp).nanoseconds() / 1000000; if (diff_ms 500) { // 超过500ms拒绝 response-confirmed false; response-reason TIMEOUT; RCLCPP_WARN(this-get_logger(), PLC request timeout: %ld ms, diff_ms); return; } // 验证PLC ID实际项目中可能查数据库或配置表 if (request-plc_id ! EXPECTED_PLC_ID) { response-confirmed false; response-reason PLC_ID_MISMATCH; RCLCPP_ERROR(this-get_logger(), Invalid PLC ID: %lu, request-plc_id); return; } // 执行急停确认逻辑发信号给运动控制器记录日志 response-confirmed true; response-reason OK; RCLCPP_INFO(this-get_logger(), Emergency stop confirmed for PLC %lu, request-plc_id); }); } private: rclcpp::Servicecustom_interfaces::srv::EmergencyStopConfirm::SharedPtr service_; static constexpr uint64_t EXPECTED_PLC_ID 0x123456789ABCDEF0ULL; };编译时CMakeLists.txt要添加find_package(custom_interfaces REQUIRED) # ... 其他内容 add_executable(emergency_stop_server src/emergency_stop_server.cpp) ament_target_dependencies(emergency_stop_server rclcpp custom_interfaces) install(TARGETS emergency_stop_server DESTINATION lib/${PROJECT_NAME})注意custom_interfaces是你自定义服务包的名字必须在package.xml里声明build_dependrosidl_default_generators/build_depend和exec_dependrosidl_default_runtime/exec_depend否则colcon build会找不到生成的头文件。3.3 实现客户端模拟PLC的健壮调用客户端plc_emulator.cpp要模拟PLC的行为class PLCEmulator : public rclcpp::Node { public: PLCEmulator() : Node(plc_emulator) { client_ this-create_clientcustom_interfaces::srv::EmergencyStopConfirm(emergency_stop_confirm); // 启动一个定时器每2秒模拟一次急停请求 timer_ this-create_wall_timer( 2s, [this]() { if (!client_-wait_for_service(1s)) { RCLCPP_WARN(this-get_logger(), Service not available, waiting...); return; } auto request std::make_sharedcustom_interfaces::srv::EmergencyStopConfirm::Request(); request-plc_id 0x123456789ABCDEF0ULL; request-timestamp this-now(); // 记录请求发出时刻 // 异步调用避免阻塞定时器 auto future client_-async_send_request(request); // 绑定future的回调注意future.get()会阻塞所以用then future.wait_for(std::chrono::seconds(1)); // 等待1秒足够了 try { auto result future.get(); if (result-confirmed) { RCLCPP_INFO(this-get_logger(), Emergency stop confirmed: %s, result-reason.c_str()); } else { RCLCPP_ERROR(this-get_logger(), Emergency stop rejected: %s, result-reason.c_str()); } } catch (const std::exception e) { RCLCPP_ERROR(this-get_logger(), Service call failed: %s, e.what()); } }); } private: rclcpp::Clientcustom_interfaces::srv::EmergencyStopConfirm::SharedPtr client_; rclcpp::TimerBase::SharedPtr timer_; };这里的关键技巧是future.wait_for()和future.get()的组合。wait_for确保不无限等待get()获取结果。如果服务端崩溃或网络中断future.get()会抛出std::runtime_error必须用try-catch捕获否则节点会crash。我在rviz2安装使用ros2的调试中就因没加catch导致可视化界面闪退花了半天才定位到是服务调用异常未处理。4. 服务通信的陷阱与实战排错那些文档里绝不会写的坑服务通信看似简单但实际项目中90%的问题都出在环境、配置和认知偏差上。下面是我踩过的、帮别人修过的、以及客户现场高频出现的典型问题每个都附带真实日志和解决方案。4.1 “Service not found”不是代码错了是节点没连上DDS域现象客户端ros2 node list能看到服务端节点ros2 topic list也能看到话题但ros2 service list里没有你的服务client-wait_for_service()永远返回false。日志片段[WARN] [1712345678.123456789] [plc_emulator]: Service not available, waiting...根本原因服务端和客户端不在同一个DDS域domain id。ROS2默认domain id是0但如果系统里有多个ROS2实例比如同时跑humble和foxy或者你手动设置了RMW_IMPLEMENTATIONrmw_cyclonedds_cpp而CycloneDDS的配置文件里指定了不同的domain就会导致节点“互相看不见”。验证方法# 查看当前shell的domain设置 echo $ROS_DOMAIN_ID # 查看DDS实现 echo $RMW_IMPLEMENTATION # 检查服务端节点实际使用的domain需在服务端代码里加日志 RCLCPP_INFO(this-get_logger(), Using domain ID: %d, rcl_get_domain_id());解决方案统一设置export ROS_DOMAIN_ID42选一个0-100之间的数然后重启所有节点。或者在/etc/ros/humble/下创建local_setup.bash写入export ROS_DOMAIN_ID42并source它。注意ROS_DOMAIN_ID必须是整数不能是字符串。我曾见过有人写成export ROS_DOMAIN_ID42导致DDS初始化失败日志里只显示“Failed to create participant”根本看不出是domain问题。4.2 “Serialization error”IDL和生成代码的版本错配现象服务端收到请求但request-a和request-b的值是随机大数如-1234567890123456789或者request-timestamp.nanosec是0。日志片段[INFO] [1712345678.123456789] [minimal_service]: Received a-1234567890123456789, b0原因.srv文件修改后没有重新colcon build或者colcon build时没有clean旧的生成文件导致客户端用新IDL生成的代码服务端用旧IDL生成的代码二者内存布局不一致序列化数据被错位解析。解决方案# 彻底清理不要只删build目录 cd ~/ros2_ws rm -rf build install log colcon build --packages-select custom_interfaces # 先单独编译接口包 colcon build --packages-select emergency_stop_server emergency_stop_client source install/setup.bash关键点接口包interface package必须最先编译且所有依赖它的节点包必须在其之后编译。这是ROS2工作空间的硬性依赖规则违反它必然出错。4.3 “Callback never called”Executor没spin或者线程被阻塞现象客户端调用async_send_request后callback函数从不执行rclcpp::spin(node)也卡住。日志片段只有[INFO] [1712345678.123456789] [plc_emulator]: Sending emergency stop request...再无下文。原因rclcpp::spin()需要在一个线程里持续运行才能处理incoming消息。如果你的main函数里只写了int main(int argc, char * argv[]) { rclcpp::init(argc, argv); auto node std::make_sharedPLCEmulator(); rclcpp::spin(node); // 这里会阻塞但timer和callback需要它来驱动 rclcpp::shutdown(); return 0; }看起来没问题但PLCEmulator的构造函数里创建了timer而timer的回调是在rclcpp::spin()的循环里被调度的。如果spin()没启动timer根本不会触发。更隐蔽的情况是你在callback里写了sleep(5)导致整个Executor线程卡死其他所有callback包括timer都无法执行。解决方案确保rclcpp::spin()被调用且在main函数末尾。如果要用多线程显式创建MultiThreadedExecutorrclcpp::executors::MultiThreadedExecutor executor; executor.add_node(node); executor.spin();4.4 “Timeout exceeded”网络延迟或QoS不匹配现象客户端wait_for_service(1s)返回false或者async_send_request的future超时。日志片段[WARN] [1712345678.123456789] [plc_emulator]: Service not available, waiting...原因服务端节点启动慢于客户端或者网络QoSQuality of Service策略不兼容。ROS2服务默认使用RELIABLE可靠性策略和KEEP_ALL历史深度但如果客户端和服务端的QoS不一致DDS会拒绝建立连接。验证方法# 查看服务端QoS ros2 interface show example_interfaces/srv/AddTwoInts # 查看当前节点的QoS需在代码里加log RCLCPP_INFO(this-get_logger(), Service QoS: reliability%d, history%d, service_-get_service_options().qos.reliability(), service_-get_service_options().qos.history());解决方案在create_service时显式指定QoSrclcpp::Serviceexample_interfaces::srv::AddTwoInts::SharedPtr service; rclcpp::QoS qos(1); qos.reliability(RMW_QOS_POLICY_RELIABILITY_RELIABLE); service this-create_serviceexample_interfaces::srv::AddTwoInts(add_two_ints, callback, qos);或者在客户端create_client时用相同QoS。5. 服务通信的进阶应用与动作Action、参数Parameter的协同设计服务通信不是孤立的它必须和ROS2的其他通信原语协同工作才能构建出健壮的机器人系统。很多初学者试图用服务解决所有问题结果陷入“服务地狱”——每个功能都暴露为服务节点间耦合度爆炸。下面我分享三个真实场景中的协同模式。5.1 服务动作长时任务的启动与监控问题你想让机械臂执行一个耗时30秒的“视觉抓取”任务。如果只用服务客户端会阻塞30秒无法做任何事如果只用动作action客户端无法在任务开始前做前置检查比如确认夹爪气压是否足够。解决方案服务负责前置校验和任务启动动作负责过程监控和结果返回。流程客户端调用/check_preconditions服务传入目标物体ID。服务端检查气压、相机状态、电池电量返回true或false及原因。若校验通过客户端再调用/start_grasp_action动作传入相同物体ID。动作服务器启动抓取流程并通过feedback流实时报告进度“移动到上方”、“下降中”、“夹紧”。动作完成后返回final result成功/失败/超时。这样服务承担了“守门员”角色动作承担了“执行者汇报员”角色职责清晰互不干扰。我在ros2机械臂视觉抓取仿真项目中就是这么设计的比纯服务方案稳定得多。5.2 服务参数动态配置的原子更新问题雷达点云滤波器的阈值需要在线调整。如果用set_parameters每次只能设一个参数而滤波器通常需要min_range、max_range、angle_filter三个参数同步生效否则中间状态会出错。解决方案用服务封装参数组的原子更新。定义UpdateLidarFilter.srvfloat64 min_range float64 max_range float64 angle_filter --- bool success string message服务端收到请求后一次性调用node-set_parameters_atomically({param1, param2, param3})确保三个参数要么全部更新成功要么全部失败。这比逐个set_parameter安全得多也避免了因网络抖动导致的参数不一致。5.3 服务话题事件驱动的响应链问题当机器人进入“低电量”状态需要触发一系列动作发布警告话题、调用导航服务返回充电站、调用机械臂服务收起末端执行器。解决方案用话题广播事件用服务执行具体动作。架构电池管理节点发布/battery_state话题包含state字段NORMAL/LOW/CRITICAL。导航节点订阅此话题当收到LOW时调用/navigate_to_charging_station服务。机械臂节点也订阅此话题当收到LOW时调用/stow_arm服务。这样电池节点只负责“通知”不关心谁来响应导航和机械臂节点各自决定是否响应、如何响应。松耦合易扩展。我在ubuntu 24.04安装ros2 jazzy的AGV调度系统中就是用这套模式后来增加“声光报警”节点只需订阅同一话题完全不用改电池节点代码。6. 性能与调试工具链让服务通信从“能跑”到“稳跑”服务通信上线后不能只满足于“功能正确”还要关注性能、可观测性和可维护性。ROS2提供了一套强大的调试工具但很多人只会用ros2 service list错过了关键信息。6.1 使用ros2 interface深入探查服务细节ros2 interface show不仅能看IDL还能看QoS策略ros2 interface show example_interfaces/srv/AddTwoInts # 输出包含 # Request: # int64 a # int64 b # Response: # int64 sum # QoS Profile: # Reliability: RELIABLE # History: KEEP_LAST # Depth: 10 # ...更重要的是ros2 interface proto它能生成protobuf格式的IDL用于跨语言集成比如Python客户端调用C服务ros2 interface proto example_interfaces/srv/AddTwoInts # 输出类似 # syntax proto3; # package example_interfaces.srv; # message AddTwoInts_Request { # int64 a 1; # int64 b 2; # } # message AddTwoInts_Response { # int64 sum 1; # }6.2 使用ros2 topic echo监听服务底层话题服务通信在DDS层其实是通过一对隐式话题实现的/service_name/_request和/service_name/_response。你可以用ros2 topic echo直接监听它们这对调试序列化问题极有用# 监听请求话题需先启动服务端 ros2 topic echo /add_two_ints/_request # 输出 # a: 1 # b: 2 # --- # 监听响应话题 ros2 topic echo /add_two_ints/_response # 输出 # sum: 3 # ---如果看到a和b的值不对说明序列化有问题如果根本收不到消息说明DDS连接失败。6.3 使用rqt_graph可视化服务连接rqt_graph默认不显示服务需要勾选“Display services”选项。它能清晰展示哪些节点提供了服务圆柱体图标哪些节点调用了服务箭头指向圆柱体服务名是否拼写一致大小写敏感我在ros2小乌龟测试中曾因把服务名写成/add_two_ints带斜杠和add_two_ints不带斜杠混用rqt_graph一眼就暴露了两个孤立的节点比看日志快十倍。6.4 自定义服务监控节点实时统计成功率生产环境中你需要知道服务的健康度。写一个简单的监控节点class ServiceMonitor : public rclcpp::Node { public: ServiceMonitor() : Node(service_monitor) { // 订阅所有服务的_request和_response话题需用正则匹配 // 统计每秒请求数、成功率、平均延迟 // 发布到/metrics/service_stats话题供Prometheus采集 } };虽然ROS2没有内置服务监控但通过监听底层话题你可以轻松实现。我在fastdds ros2 封装层的性能调优中就是靠这个监控节点发现了DDS的max_samples配置过小导致高并发时请求丢失。7. 从ROS2服务到微ROS面向资源受限设备的轻量化演进当你的机器人系统要部署到ESP32-S3这样的MCU上标准ROS2服务通信就力不从心了。这时micro-ROS登场。它不是ROS2的简化版而是针对嵌入式场景重构的通信栈。7.1 micro-ROS服务通信的三大变化IDL生成目标不同micro-ROS用rosidl_microxrcedds生成C代码而非C。.srv文件一样但生成的add_two_ints.h里是struct和typedef没有std::shared_ptr。通信层替换不依赖DDS而是用microxrceddseProsima的轻量DDS实现或freertos的队列。rclcmicro-ROS Client替代rclcppAPI更精简。资源约束显性化必须显式声明内存池大小、最大服务数、最大请求大小。比如rclc_service_t service; RCCHECK(rclc_service_init_best_effort( service, support, ROSIDL_GET_SRV_TYPE_SUPPORT(example_interfaces, srv, AddTwoInts), /add_two_ints, allocator));7.2 在VSCode PlatformIO中开发micro-ROS服务micro-ros ros2 esp32s3 vscode platformio是当前热门组合。关键步骤PlatformIO项目里platformio.ini要指定platform espressif32和board esp32dev。src/main.c里初始化顺序必须是rmw_uros_set_context→rclc_support_init→rclc_node_init→rclc_service_init。服务回调函数签名是C风格void add_two_ints_service_callback(const void * req, void * res) { const example_interfaces__srv__AddTwoInts_Request * request (const example_interfaces__srv__AddTwoInts_Request *)req; example_interfaces__srv__AddTwoInts_Response * response (example_interfaces__srv__AddTwoInts_Response *)res; response-sum request-a request-b; }最大的坑是micro-ROS不支持std::string所有字符串必须用char[SIZE]且SIZE要在.srv里声明。比如string reason要改成char reason[64]否则编译失败。我在livox avia配置使用ros2的固件升级模块中就是用micro-ROS服务实现“请求固件版本”和“触发OTA升级”把原本需要Linux主机做的事直接搬到了雷达内部MCU上大幅降低了系统复杂度。8. 写在最后服务通信的本质是机器人世界的契约精神我带过的所有ROS2学员最终能真正驾驭服务通信的都不是那些最早写出AddTwoInts的人而是那些在调试EmergencyStopConfirm时反复修改IDL、重编译、抓包、看DDS日志折腾了两天终于让PLC和主控握手成功的家伙。因为那一刻他们理解了服务通信不是API调用而是一种契约客户端承诺发送符合IDL的请求服务端承诺在约定时间内返回符合IDL的响应DDS作为公证人确保消息不丢失、不错位rclcpp作为翻译官把底层协议变成C对象。所以下次当你看到ros2服务、ros2小乌龟测试、ros2机器人开发从入门到实践pdf里的示例别急着复制粘贴。先问自己这个服务解决了什么真实问题它的请求和响应字段是否精确表达了业务语义它的QoS策略是否匹配了实时性要求它的错误处理是否覆盖了所有可能的失败路径机器人系统没有银弹服务通信只是其中一块砖。但把这块砖砌牢了后面的墙才不会塌。
返回列表