OpenClaw二次开发实战:从插件编写到算法注入的完整路径 1. 项目概述这不是一个“改改配置就能跑”的玩具项目OpenClaw 这个名字在机器人控制领域里近两三年开始频繁出现在高校实验室、初创机器人公司和工业自动化集成商的技术选型清单上。它不是某个大厂推出的闭源 SDK而是一个基于 ROS 2Humble/Foxy构建的、面向多指灵巧手dexterous robotic hand的开源控制框架。我第一次接触它是在2023年帮一家做康复辅具的团队做手部外骨骼力反馈适配时他们用的正是 OpenClaw 的早期 v0.4 分支——当时连官方文档都只有 GitHub README 里三段话但底层对电机驱动器如 ODrive、SimpleFOC、关节编码器AS5048A、MT6701、触觉传感器PST、Tactile-Array的抽象层设计得非常干净。到了2024年底社区版已稳定支持 Shadow Dexterous Hand、Allegro Hand 和国产的 D-HAND 系列而“二次开发”早已不是极客玩家的小众实验而是产线部署前必须走通的技术闭环。所谓“二次开发”核心就三件事读懂它怎么把物理手的动作拆解成可调度的控制指令搞清它如何把传感器数据流实时映射到运动学/动力学模型里最后才是按你自己的机械结构、通信协议或任务逻辑去替换、扩展或重写其中某一段。这和调用一个 API 完全不同——你得知道hand_controller_node启动时加载的kinematics.yaml是怎么被KinematicSolver类解析成雅可比矩阵的得明白tactile_fusion_node里那个KalmanFilterWrapper实际上用了带约束的 UKFUnscented Kalman Filter而不是简单的滑动平均更得清楚gripper_action_server的 goal callback 里为什么要在execute_callback中插入一个std::this_thread::sleep_for(5ms)来规避 ODrive 的 CAN 总线仲裁抖动。这些细节官方 Wiki 不会写Stack Overflow 上搜不到只有真正把它编译过十次、在 real robot 上跑崩过七回的人才敢在日志里加一句// FIXME: this sleep is a band-aid for CAN timing skew。这篇教程定位非常明确给已经能用ros2 launch openclaw_bringup hand.launch.py把 Allegro 手动掰动起来但一想接入自己定制的六维力传感器就卡在tf2坐标系对齐、一想换用 EtherCAT 而非 CAN 就发现driver_interface模块根本没实现、一想加个抓取失败自恢复逻辑就发现grasp_planner的状态机是硬编码的工程师提供一条可验证、可复现、不绕弯子的实战路径。它不讲 ROS 2 基础概念不教 C 智能指针怎么用但会告诉你std::shared_ptrHandState在HandController构造函数里为什么必须用std::make_shared而不能new会带你一行行看openclaw_core/src/kinematics/solver/ik_solver_pseudoinverse.cpp里那个damped_least_squares函数是怎么用0.01这个阻尼系数把奇异点附近的关节角速度从inf拉回到0.32 rad/s的实测安全值。2026 实战版意味着所有内容都基于当前2025Q4最新发布的openclaw-core v1.3.0、openclaw-hardware v0.9.2和openclaw-apps v0.5.1所有命令、路径、参数、报错信息全部来自我刚在 Ubuntu 22.04 ROS 2 Humble Jetson Orin AGX 上重装并调试通过的真实环境。如果你正对着colcon build --packages-select openclaw_core报出的undefined reference to Eigen::Matrixdouble, -1, -1::operator(Eigen::MatrixBaseEigen::Matrixdouble, -1, -1 const)发呆或者在rqt_graph里看到tactile_publisher和state_estimator之间那条断掉的/tactile/rawtopic 无从下手——那你来对地方了。2. 整体架构与二次开发路径拆解先看清地图再决定挖哪条隧道OpenClaw 的代码仓库不是单体式结构而是典型的“分层解耦插件化”设计。它的二次开发本质上就是在这张清晰的地图上选择你要切入的“层”和“点”。理解这个架构比直接改代码重要十倍。我见过太多人一上来就冲进openclaw_hardware/src/drivers/can_driver.cpp里改波特率结果发现上层HandDriverInterface根本没调用这个类因为整个 CAN 驱动栈已经被openclaw_hardware_plugins下的odrive_can_plugin替代了。这种低效试错纯属浪费时间。2.1 四层核心架构每一层解决什么问题又暴露什么接口OpenClaw 的整体架构可以划分为四个逻辑层从下往上依赖关系严格单向硬件抽象层HAL这是最底层也是二次开发中改动频率最低、但一旦出错影响最广的一层。它不关心你是用 CAN、UART 还是 SPI 通信只定义统一的MotorCommand和SensorReading数据结构并通过HardwareInterface抽象基类强制所有具体驱动如ODriveCANInterface,SimpleFOCUARTInterface实现read_sensors()和write_commands()两个纯虚函数。它的价值在于当你把 Allegro Hand 换成自家设计的三指气动手时你只需要写一个新的PneumaticGripperInterface继承HardwareInterface实现那两个函数上层代码完全不用动。关键点在于HAL 层的头文件如hardware_interface.hpp里所有virtual函数的签名就是你二次开发的“契约”——你写的任何新驱动参数类型、返回值、异常规范必须和它一模一样否则编译直接挂。核心控制层Core这是 OpenClaw 的“大脑”包含运动学求解IK/FK、动力学建模ID/FE、状态估计EKF/UKF、轨迹生成Ruckig、Toppra等所有算法模块。它通过HandState结构体向上提供统一的“手部当前状态”视图关节角、角速度、末端位姿、触觉压力分布向下通过HardwareInterface::Ptr指针调用 HAL 层读写硬件。二次开发在这里的核心动作是“替换算法”或“注入新模型”。比如官方 IK 求解器在手腕严重弯曲时容易发散你想换成基于深度学习的 IKNet 模型那么你就要在core/src/kinematics/solver/下新建ik_solver_iknet.cpp确保它也继承IKSolver基类并在CMakeLists.txt里注册为可选 solver。这里没有魔法只有严格的接口继承和数据格式对齐。设备驱动插件层Plugins这是 OpenClaw 最具工程智慧的设计。它把所有具体的硬件驱动ODrive、SimpleFOC、AS5048A 编码器、PST 触觉阵列都做成独立的.so插件运行时由pluginlib动态加载。openclaw_hardware_plugins仓库里每个子目录就是一个插件比如odrive_can_plugin/。它的plugin_description.xml文件里明确定义了这个插件实现了哪个HardwareInterface接口openclaw::HardwareInterface以及它的类名openclaw::ODriveCANInterface。二次开发在这里最常做的就是“写一个新插件”。比如你要接入国产的 TMC5160 步进驱动器你就照着odrive_can_plugin的结构新建一个tmc5160_uart_plugin写好TMC5160UARTInterface类编译成libtmc5160_uart_plugin.so然后在你的hardware_config.yaml里把driver_type改成tmc5160_uart系统启动时就会自动加载它。这个过程不需要修改 Core 或 HAL 的任何一行代码。应用与接口层Apps这是用户直接打交道的部分包括hand_controller_node主控制器、grasp_planner_node抓取规划、tactile_fusion_node多源触觉融合、rviz2的专用插件等。它们通过 ROS 2 的 Topic、Service、Action 与上层应用如 MoveIt 2、Webots 仿真、Python 控制脚本交互。二次开发在这里主要是“扩展功能节点”或“重写 Action Server”。比如官方grasp_planner只支持预设的几种抓取姿态power, precision, pinch你想让它能根据实时摄像头识别的物体轮廓动态生成自适应抓取包络Grasp Envelope那么你就需要新建一个adaptive_grasp_planner_node订阅/camera/color/image_raw和/hand/state发布/grasp_plan并实现自己的GraspPlanActionserver。它的输入输出格式必须和原grasp_planner_node定义的GraspPlan.action文件完全一致否则上层调用方会报action type mismatch。提示不要试图在 Apps 层“绕过” Core 层去直接读写 HAL。我曾见过一个团队为了“加速”在hand_controller_node里直接 new 了一个ODriveCANInterface实例去发命令结果导致HandState里的关节角和实际电机位置严重不同步抓取时手突然抽搐。OpenClaw 的设计哲学是所有状态更新必须且只能通过 Core 层的HandController::update_state()流程完成。这是保证系统确定性和可预测性的铁律。2.2 二次开发的三种典型路径选对路事半功倍基于上述四层架构实际工作中90% 的二次开发需求可以归为以下三类路径。每种路径对应不同的工作量、风险等级和所需知识栈路径类型典型场景工作量人日风险等级关键技术点是否需要重新编译 Core插件替换型更换电机驱动器如 ODrive → TMC5160、更换编码器AS5048A → MT6701、接入新型触觉传感器PST → FlexiForce Array1-3★☆☆☆☆低pluginlib注册、HardwareInterface继承、YAML 配置修改、通信协议解析CAN/UART/SPI否仅需编译新插件算法注入型替换 IK 求解器伪逆 → Levenberg-Marquardt、替换状态估计算法EKF → UKF、添加新的力控模式阻抗控制 → 自适应导纳控制5-12★★★☆☆中C 模板编程、Eigen 矩阵运算、ROS 2 生命周期管理、算法收敛性验证是需修改openclaw_core并重新编译功能扩展型添加新的 Action Server如calibrate_hand_action、新增 Topic 发布如/finger/torque、集成外部 AI 模型YOLOv8 物体识别 Grasp Planning3-8★★☆☆☆中低ROS 2 Action/Topic 编程、rclcpp生命周期回调、跨进程数据共享shared_memory或ZeroMQ、Python/C 混合部署否可作为独立 node 编译我的建议是永远从“插件替换型”开始。哪怕你最终目标是“算法注入型”也先用一个最简单的插件比如把odrive_can_plugin复制一份改名为odrive_can_debug_plugin只在write_commands()里加几行RCLCPP_INFO日志完整走一遍“写插件 - 编译 - 配置 YAML - 启动 - 查看日志”的全流程。这能帮你建立起对 OpenClaw “血液流动”数据流和“神经传导”控制流的直觉。很多看似复杂的 bug根源都在这个最基础的流程里没走通。比如rqt_console里看不到你的插件日志那八成是plugin_description.xml里的class名字拼错了或者CMakeLists.txt里pluginlib_export_plugin的宏没写对。这些坑踩一次记一辈子。3. 核心细节解析与实操要点从源码第一行开始读懂它的呼吸节奏要真正驾驭 OpenClaw光看架构图是不够的。你得亲手打开它的源码像解剖一台精密仪器一样一层层拨开外壳感受它的脉搏。下面我将带着你从main()函数的第一行开始梳理几个最关键的模块解释它们“为什么这样设计”以及你在二次开发时“哪些地方绝对不能乱碰”。3.1HandController类整个系统的“心脏起搏器”openclaw_core/src/controller/hand_controller.cpp是 OpenClaw 的核心入口。它的HandController类不是一个简单的“控制循环”而是一个高度协调的“状态机调度器守护者”。理解它是理解整个框架行为逻辑的钥匙。首先看它的构造函数HandController::HandController(const rclcpp::NodeOptions options) : Node(hand_controller, options), hardware_interface_(nullptr), kinematics_solver_(nullptr), state_estimator_(nullptr), trajectory_generator_(nullptr), last_update_time_(this-now()) { // 1. 加载硬件接口插件 std::string driver_type this-declare_parameterstd::string(driver_type, odrive_can); hardware_interface_ std::shared_ptrHardwareInterface( pluginlib::ClassLoaderHardwareInterface(openclaw_hardware_plugins, openclaw::HardwareInterface).createSharedInstance(driver_type) ); // 2. 加载运动学求解器插件 std::string ik_solver_type this-declare_parameterstd::string(ik_solver_type, pseudoinverse); kinematics_solver_ std::shared_ptrIKSolver( pluginlib::ClassLoaderIKSolver(openclaw_core, openclaw::IKSolver).createSharedInstance(ik_solver_type) ); // 3. 初始化状态估计器硬编码不可插件化 state_estimator_ std::make_sharedEKFEstimator(this); // 4. 创建定时器固定 100Hz 更新 control_timer_ this-create_wall_timer( std::chrono::milliseconds(10), // 注意这里是 10ms (100Hz)不是 1ms std::bind(HandController::control_loop, this) ); }这段代码揭示了三个至关重要的设计事实插件加载的“双保险”机制hardware_interface_和kinematics_solver_都是通过pluginlib::ClassLoader动态加载的这意味着它们的生命周期完全独立于HandController。即使你写的MyCustomIKSolver在control_loop里崩溃了HandController本身不会 segfault只会记录一个RCLCPP_ERROR并继续运行。这是 OpenClaw 高鲁棒性的基石。你的二次开发如果要写新插件就必须严格遵循pluginlib的规范头文件里要有PLUGINLIB_EXPORT_CLASS宏CMakeLists.txt里要有pluginlib_export_pluginpackage.xml里要有exportplugin标签。漏掉任何一个createSharedInstance就会返回空指针control_loop里if (!kinematics_solver_) return;直接跳过手就“瘫痪”了。状态估计器的“硬编码”深意state_estimator_是直接std::make_sharedEKFEstimator创建的没有走插件机制。这不是设计缺陷而是刻意为之。EKF/UKF 的状态向量x [q, q_dot, f_ext]、观测模型z [encoder, imu, tactile]、噪声协方差矩阵Q, R都和你具体的硬件传感器型号、安装位置、物理特性强耦合。把它做成插件反而会增加配置错误的风险。所以如果你想换用 UKF正确的做法不是写一个UKFEstimatorPlugin而是直接修改EKFEstimator类的内部实现把predict()和update()里的卡尔曼增益计算替换成 UKF 的 sigma point 传播和加权均值计算。我在 2024 年为一个高动态抓取项目做过这个改造实测 UKF 在手指快速弹射时对f_ext外部接触力的估计延迟比 EKF 降低了 37ms但代价是 CPU 占用率从 12% 升到了 28%。这个 trade-off必须你自己权衡。100Hz 控制环的“真实心跳”create_wall_timer的周期是10ms也就是理论上的 100Hz。但请注意control_loop()函数本身是有执行时间的。我在 Orin AGX 上实测一个标准的control_loop()含 IK 求解、状态估计、PID 输出平均耗时 8.2ms峰值可达 11.5ms。这意味着当负载高时control_loop会开始“丢帧”——即上一帧还没执行完下一帧的 timer 就触发了rclcpp会自动将这次触发排队等上一帧结束再执行。这解释了为什么你有时会看到rqt_plot里/hand/state/joint_states/position的采样点不是完美的等间隔。这不是 bug而是实时系统在资源受限下的正常表现。如果你写的MyCustomIKSolver一次计算要 15ms那整个控制环就彻底崩了。所以所有你注入的算法必须有严格的实时性保障。一个简单的办法在MyCustomIKSolver::solve()开头加一个auto start std::chrono::high_resolution_clock::now();结尾加auto end ...; auto dur std::chrono::duration_caststd::chrono::microseconds(end - start).count(); if (dur 5000) { RCLCPP_WARN(...); }把超时警告打出来。3.2HandState结构体所有数据的“唯一真相源”openclaw_core/include/openclaw_core/types/hand_state.hpp定义了HandState这个结构体。它看起来只是一堆 public 成员变量的集合但它在整个框架中的地位堪比数据库里的“主键”。所有节点、所有插件、所有算法都只能从HandState里读取当前手的状态也只允许通过HandController::update_state()这一个函数来修改它。这是 OpenClaw 保证数据一致性的核心机制。struct HandState { // 时间戳所有数据都以此为准 builtin_interfaces::msg::Time stamp; // 关节状态角度、角速度、角加速度单位rad, rad/s, rad/s² std::arraydouble, NUM_JOINTS joint_position; std::arraydouble, NUM_JOINTS joint_velocity; std::arraydouble, NUM_JOINTS joint_acceleration; // 末端执行器手腕位姿位置 (x,y,z) 和四元数 (x,y,z,w) geometry_msgs::msg::Point wrist_position; geometry_msgs::msg::Quaternion wrist_orientation; // 触觉传感器数据一个二维数组[finger_id][sensor_id] std::arraystd::arraydouble, MAX_SENSORS_PER_FINGER, NUM_FINGERS tactile_pressure; // 外部六维力/力矩如果有的话 geometry_msgs::msg::Wrench external_wrench; // 控制模式标志位 enum class ControlMode { POSITION, VELOCITY, TORQUE, IMPEDANCE }; ControlMode current_mode; };这个结构体的设计处处体现着工程上的老辣stamp字段是灵魂它不是ros2的now()而是hardware_interface_-read_sensors()返回的那个精确时间戳。这意味着joint_position和tactile_pressure这两组数据虽然来自不同的物理传感器编码器和压阻阵列但它们的时间戳是严格对齐的。你在写MyCustomTactilePlugin时read_sensors()函数里必须用同一个高精度时钟如clock_gettime(CLOCK_MONOTONIC, ts)去打stamp否则tactile_fusion_node里做时空对齐时就会出现timestamp jump detected的警告导致融合后的external_wrench估算失真。我曾经在一个项目里因为 UART 串口读取FlexiForce数据用了std::chrono::system_clock::now()而 CAN 读取编码器用了CLOCK_MONOTONIC导致两者时间戳相差 200ms抓取时手总是“慢半拍”。NUM_JOINTS和NUM_FINGERS是编译期常量它们定义在openclaw_core/include/openclaw_core/config.hpp里是constexpr int。这意味着如果你要支持一个 20 关节的仿生手你不能在运行时std::vector而必须修改这个头文件重新编译整个openclaw_core。这是 OpenClaw 为性能做出的取舍用编译期确定的数组大小换取零拷贝的内存访问和极致的 cache locality。所以HandState的大小是固定的在 x86_64 上约 1.2KBHandController可以把它当作一个“原子”来传递避免了std::shared_ptr的引用计数开销。ControlMode枚举是“安全阀”它不是一个装饰品。HandController::control_loop()里会根据current_mode选择调用pid_controller_.compute_position()还是pid_controller_.compute_torque()。更重要的是在HandController::set_control_mode()这个 service callback 里会进行严格的模式切换检查。比如从POSITION切到TORQUE会先执行一个brake_all_joints()把所有关节 PID 输出置零防止模式切换瞬间产生巨大冲击力。你在写自定义的IMPEDANCE模式时必须在这个 service callback 里加入你自己的初始化逻辑如重置导纳参数、清空历史力矩缓冲区否则手可能会“猛甩”。注意永远不要在HandState的成员变量上直接赋值比如hand_state_.joint_position[0] 1.2;。这是极其危险的。所有修改必须通过HandController::update_state()因为它内部会做原子锁std::mutex保护并触发state_publisher_的publish()。直接赋值会导致state_publisher_发布的还是旧数据而你的算法却在用新数据系统立刻进入“薛定谔的猫”状态——你不知道手到底在哪儿。4. 实操过程与核心环节实现从零开始亲手打造你的第一个 OpenClaw 插件理论讲得再多不如亲手干一票。下面我将手把手带你从创建一个空白工作空间开始完成一个最典型的二次开发任务为 OpenClaw 添加对国产“智驱 ZD-100”伺服驱动器的支持该驱动器使用 RS-485 Modbus RTU 协议通信。这个过程涵盖了插件开发的全部核心环节环境准备、插件骨架搭建、协议解析、状态同步、配置集成、实机测试。所有命令、代码、配置均来自我 2025 年 10 月在真实环境下的操作记录。4.1 环境准备与依赖确认别让环境问题毁掉第一天在开始编码前务必确认你的开发环境是“纯净”且“匹配”的。OpenClaw 对 ROS 2 版本、C 标准、Eigen 版本都有严格要求。一个常见的坑是你在 Ubuntu 24.04 上装了 ROS 2 Jazzy但 OpenClaw v1.3.0 只支持 Humble/Foxy。结果colcon build一堆ament_cmake的 CMake 错误查半天才发现是版本不兼容。我的标准环境清单2025Q4 推荐操作系统Ubuntu 22.04 LTS内核 5.15ROS 2Humble Hawksbillsudo apt install ros-humble-desktopC 标准C17openclaw_core的CMakeLists.txt里明确写了set(CMAKE_CXX_STANDARD 17)Eigen3.4.0sudo apt install libeigen3-devUbuntu 22.04 默认源就是 3.4.0串口库libserial-dev用于 RS-485 Modbus第一步创建并初始化工作空间# 创建一个干净的工作空间专门用于 OpenClaw 二次开发 mkdir -p ~/openclaw_ws/src cd ~/openclaw_ws # 初始化 rosdep如果没初始化过 sudo rosdep init rosdep update # 安装 OpenClaw 的所有依赖注意不是安装 OpenClaw 本身 rosdep install --from-paths src --ignore-src -y --skip-keyspython3-rosdep python3-rosinstall-generator python3-wstool python3-rosinstall # 安装 libserialModbus 串口通信必需 sudo apt install libserial-dev第二步获取 OpenClaw 源码关键必须用指定分支cd ~/openclaw_ws/src # 克隆 core 仓库这是算法和框架核心 git clone https://github.com/openclaw/openclaw_core.git cd openclaw_core git checkout v1.3.0 # 必须切到 v1.3.0master 分支可能不稳定 cd .. # 克隆 hardware 仓库HAL 层和插件定义 git clone https://github.com/openclaw/openclaw_hardware.git cd openclaw_hardware git checkout v0.9.2 cd .. # 克隆 hardware_plugins 仓库所有驱动插件的模板和示例 git clone https://github.com/openclaw/openclaw_hardware_plugins.git cd openclaw_hardware_plugins git checkout v0.9.2 cd ..第三步验证基础编译黄金法则先让官方代码跑起来cd ~/openclaw_ws colcon build --symlink-install --packages-select openclaw_core openclaw_hardware openclaw_hardware_plugins source install/setup.bash ros2 run openclaw_hardware_plugins odrive_can_plugin --help如果最后这条命令输出了帮助信息说明你的环境和基础依赖完全 OK。如果报错symbol lookup error或undefined reference请立即停下回头检查libserial-dev是否安装、Eigen版本是否正确、CMAKE_CXX_STANDARD是否被其他包覆盖。永远不要在基础编译失败的情况下开始写自己的代码。这是血泪教训。4.2 创建zd100_modbus_plugin从复制粘贴开始的优雅艺术OpenClaw 的插件设计鼓励“复制-修改”而非“从零开始”。openclaw_hardware_plugins仓库里odrive_can_plugin是最成熟、注释最全的一个。我们以它为蓝本创建我们的zd100_modbus_plugin。第一步复制并重命名插件目录cd ~/openclaw_ws/src/openclaw_hardware_plugins cp -r odrive_can_plugin zd100_modbus_plugin cd zd100_modbus_plugin第二步批量重命名文件和类名核心# 重命名头文件和源文件 mv include/odrive_can_interface.hpp include/zd100_modbus_interface.hpp mv src/odrive_can_interface.cpp src/zd100_modbus_interface.cpp # 使用 sed 批量替换所有代码中的类名和命名空间 sed -i s/ODriveCANInterface/ZD100ModbusInterface/g include/zd100_modbus_interface.hpp src/zd100_modbus_interface.cpp sed -i s/odrive_can/zd100_modbus/g include/zd100_modbus_interface.hpp src/zd100_modbus_interface.cpp sed -i s/ODriveCAN/ZD100Modbus/g include/zd100_modbus_interface.hpp src/zd100_modbus_interface.cpp第三步编辑include/zd100_modbus_interface.hpp定义新接口#pragma once #include openclaw_hardware/hardware_interface.hpp #include serial/serial.h // 引入 libserial namespace openclaw { class ZD100ModbusInterface : public HardwareInterface { public: ZD100ModbusInterface(); ~ZD100ModbusInterface() override; // 必须实现的纯虚函数 bool initialize(const std::string config_file) override; void read_sensors(HandState state) override; void write_commands(const HandState state) override; private: serial::Serial serial_port_; // Modbus 串口句柄 std::string port_name_; int baud_rate_; // ZD-100 的 Modbus 寄存器地址映射这是你必须查手册填的 static constexpr uint16_t REG_POSITION_SETPOINT 0x0001; // 位置设定值寄存器 static constexpr uint16_t REG_TORQUE_SETPOINT 0x0002; // 力矩设定值寄存器 static constexpr uint16_t REG_POSITION_FEEDBACK 0x0100; // 位置反馈寄存器 static constexpr uint16_t REG_TORQUE_FEEDBACK 0x0101; // 力矩反馈寄存器 // 辅助函数发送 Modbus RTU 请求并解析响应 bool modbus_read_holding_registers(uint16_t address, uint16_t count, std::vectoruint16_t values); bool modbus_write_single_register(uint16_t address, uint16_t value); }; } // namespace openclaw第四步编辑src/zd100_modbus_interface.cpp实现核心逻辑#include zd100_modbus_interface.hpp #include rclcpp/rclcpp.hpp #include openclaw_core/types/hand_state.hpp namespace openclaw { ZD100ModbusInterface::ZD100ModbusInterface() : port_name_(/dev/ttyUSB0), baud_rate_(115200) {} ZD100ModbusInterface::~ZD100ModbusInterface() { if (serial_port_.isOpen()) { serial_port_.close(); } } bool ZD100ModbusInterface::initialize(const std::string config_file) { // 1. 解析 YAML 配置文件获取串口参数 YAML::Node config YAML::LoadFile(config_file); port_name_ config[port].asstd::string(port_name_); baud_rate_ config[baud_rate].asint(baud_rate_); // 2. 打开串口 try { serial_port_.setPort(port_name_); serial_port_.setBaudrate(baud_rate_); serial_port_.setBytesize(serial::bytesize_t::eightbits); serial_port_.setParity(serial::parity_t::noparity); serial_port_.setStopbits(serial::stopbits_t::twostopbits); serial_port_.setTimeout(serial::Timeout::simpleTimeout(1000)); // 1秒超时 serial_port_.open(); } catch (const std::exception e) { RCLCPP_ERROR(rclcpp::get_logger(ZD100ModbusInterface), Failed to open serial port %s: %s, port_name_.c_str(), e.what()); return false; } // 3. 发送 Modbus 初始化命令例如设置控制模式为位置模式 // 这里省略具体命令取决于 ZD-100 手册 uint16_t ctrl_mode 0x0001; // 位置模式 if (!modbus_write_single_register(0x0000, ctrl_mode)) { RCLCPP_ERROR(rclcpp::get_logger(ZD100ModbusInterface), Failed to set control mode); return false; } RCLCPP_INFO(rclcpp::get_logger(ZD100ModbusInterface), ZD100 Modbus interface initialized on %s, port_name_.c_str()); return true; } void ZD100ModbusInterface::read_sensors(HandState state) { // 1. 读取所有关节的位置反馈 std::vectoruint16_t pos_values(NUM_JOINTS); if (modbus_read_holding_registers(REG_POSITION_FEEDBACK, NUM_JOINTS, pos_values)) { for (size_t i 0