ROS驱动包深度定制:基于JSON协议实现睿尔曼机械臂关节速度查询与扩展
在机器人操作系统(ROS)的实际开发中,我们常常会遇到一个核心需求:如何让机械臂的ROS驱动包更“懂”我们,能够灵活地查询和控制那些隐藏在底层协议中的关键参数。睿尔曼(RealMan)的6自由度机械臂提供了一个基于JSON的通信协议,这为深度定制打开了大门。想象一下,你不仅能让机械臂动起来,还能实时获取每个关节的速度上限,为高级运动规划、安全监控或动态调整提供数据基础。这篇文章就是为你——那些不满足于基础功能,希望深入驱动层进行二次开发的ROS工程师和机械臂控制专家准备的。我们将抛开简单的API调用,深入到ROS节点内部,从话题订阅、JSON数据构建到反馈解析,完整地走一遍为睿尔曼机械臂ROS驱动包添加关节速度查询功能的实战流程。你会发现,驱动包的扩展并非黑盒魔法,而是一系列清晰、可复用的代码实践。
1. 环境准备与核心概念解析
在开始动手修改代码之前,我们需要确保开发环境就绪,并理解几个关键概念。睿尔曼机械臂的ROS驱动包(通常命名为rm_driver)是其与ROS生态系统通信的桥梁。它底层通过TCP Socket与机械臂控制器(默认IP192.168.1.18)建立连接,而上层则通过ROS话题(Topic)和服务(Service)向其他节点暴露控制接口。我们所要做的,就是在这个桥梁上增加新的“车道”。
开发环境基线:
- 操作系统:Ubuntu 20.04 LTS。这是ROS Noetic的推荐系统,保证了库依赖的一致性。
- ROS发行版:Noetic。这是ROS1的最后一个LTS版本,拥有完善的生态和长期支持。
- 机械臂基础功能:确保你的
rm_driver基础包可以正常运行,能够通过ROS控制机械臂进行基本的移动,并且MoveIt!规划功能可用。这验证了底层Socket通信和基础消息解析是正常的。 - 关键文档:手边备好《睿尔曼6自由度机械臂JSON通信协议 v3.5.3》(或更高版本)。这份协议是我们的“地图”,所有查询和设置指令的格式、字段含义、返回值都定义在其中。没有它,开发将寸步难行。
睿尔曼的JSON协议将机械臂指令清晰地分为三类,理解这一点对后续开发至关重要:
| 指令类型 | 核心作用 | 数据流向 | ROS中的典型体现 |
|---|---|---|---|
| 设置 (Set) | 改变机械臂内部参数或状态 | 上位机 → 机械臂 | 发布到特定话题触发回调函数,发送设置指令。 |
| 查询 (Query) | 获取机械臂当前的参数或状态 | 上位机 → 机械臂 → 上位机 | 发布到查询话题,驱动包发送查询指令,等待并解析反馈,最后发布结果。 |
| 反馈 (Feedback) | 机械臂对设置或查询的响应,或主动上报的状态 | 机械臂 → 上位机 | 在驱动包的主循环中被持续解析,并更新内部状态或发布对应话题。 |
我们即将实现的“查询关节最大速度”功能,就是一个典型的查询-反馈流程。整个数据流将贯穿ROS驱动包的多个核心文件,包括头文件(.h)、源文件(.cpp)和自定义消息定义(.msg)。
提示:在开始修改前,强烈建议对你现有的
rm_driver功能包进行备份,或者使用版本控制工具(如git)管理你的修改。这能在出现问题时快速回退。
2. 驱动包扩展实战:添加关节速度查询功能
现在,我们进入核心的代码实战环节。为驱动包添加一个功能,本质上是定义一套新的ROS话题接口,并实现其背后的通信逻辑。我们将以“查询关节最大速度”为例,分步骤拆解。
2.1 第一步:声明话题订阅与回调函数
ROS采用基于话题的发布-订阅模型。要触发一次查询,我们需要一个“开关”。这个开关就是一个ROS话题。当外部节点向这个话题发布消息(哪怕是一个空消息)时,驱动包内的回调函数就会被触发,开始执行查询逻辑。
首先,在驱动包的主要头文件rm_robot.h中,声明我们需要的订阅者(Subscriber)和通信函数原型。这个文件通常包含了驱动节点核心类的定义。
// 在 rm_robot.h 的类定义中(例如 class RM_Robot)添加以下成员变量和函数声明 class RM_Robot { // ... 其他现有成员 ... public: // 查询关节最大速度的通信函数 int Get_Joint_Max_Speed(); // 解析关节最大速度反馈的函数 int Parser_Joint_Max_Speed(char* msg); // 查询关节最大速度的回调函数 void Get_Joint_Max_Speed_Callback(const std_msgs::Empty &msg); private: // 查询关节最大速度的订阅者 ros::Subscriber sub_get_joint_max_speed_cmd_; // 用于发布查询结果的发布者 ros::Publisher joint_max_speed_pub_; // 存储关节速度数据的结构体(假设为RM_Joint) // Arm_Joint_Speed_Max joint_speed_max_msg_; // 会在cpp中定义 };这里,我们使用std_msgs::Empty作为查询话题的消息类型,因为它不携带额外数据,仅仅是一个触发信号。Get_Joint_Max_Speed()函数将负责构造JSON指令并通过Socket发送给机械臂。
2.2 第二步:实现话题订阅与回调函数
接下来,在源文件rm_driver.cpp中,我们需要初始化订阅者,并将话题名、队列大小和回调函数绑定起来。这通常在节点的初始化函数(如init())中完成。
// 在 rm_driver.cpp 的初始化函数中找到添加其他订阅者的地方,加入以下代码 sub_get_joint_max_speed_cmd_ = nh_.subscribe("/rm_driver/get_joint_max_speed_cmd", 10, &RM_Robot::Get_Joint_Max_Speed_Callback, this);"/rm_driver/get_joint_max_speed_cmd":这是我们自定义的查询话题名称。外部节点通过向这个话题发布消息来触发查询。10:消息队列大小,用于缓存来不及处理的消息。&RM_Robot::Get_Joint_Max_Speed_Callback:指向我们回调函数的指针。
然后,实现回调函数本身。这个函数非常简单,主要就是调用负责实际通信的Get_Joint_Max_Speed()函数,并根据返回值进行简单的日志输出。
void RM_Robot::Get_Joint_Max_Speed_Callback(const std_msgs::Empty &msg) { ROS_INFO("Get_Joint_Max_Speed_Callback triggered."); int res = Get_Joint_Max_Speed(); if (res != 0) { ROS_ERROR("Failed to send joint max speed query command! Error code: %d", res); } }2.3 第三步:构建与发送JSON查询指令
这是与机械臂通信的核心。Get_Joint_Max_Speed()函数需要根据JSON协议手册,构造出正确的指令字符串,并通过Socket发送出去。睿尔曼机械臂使用cJSON库来处理JSON的构建和解析,这是一个轻量级的C语言库。
int RM_Robot::Get_Joint_Max_Speed() { cJSON *root = NULL; char *json_str = NULL; char send_buffer[256]; int send_result; // 1. 创建JSON根对象 root = cJSON_CreateObject(); if (root == NULL) { return -1; // 内存分配失败 } // 2. 根据协议添加命令字段 // 查阅协议手册,查询关节最大速度的指令格式为:{"command": "get_joint_max_speed"} cJSON_AddStringToObject(root, "command", "get_joint_max_speed"); // 3. 将cJSON对象打印成字符串 json_str = cJSON_Print(root); if (json_str == NULL) { cJSON_Delete(root); return -2; // JSON打印失败 } // 4. 格式化发送字符串(机械臂协议通常要求以\r\n结尾) snprintf(send_buffer, sizeof(send_buffer), "%s\r\n", json_str); // 5. 通过Socket发送指令 // Arm_Socket 是已经建立好的TCP Socket连接 send_result = send(Arm_Socket, send_buffer, strlen(send_buffer), 0); // 6. 清理内存 free(json_str); cJSON_Delete(root); if (send_result < 0) { ROS_ERROR("Socket send error: %s", strerror(errno)); return -3; // 网络发送失败 } ROS_DEBUG("Sent command: %s", send_buffer); return 0; // 发送成功 }注意:实际的Socket发送可能被封装在类似
package_send()的函数中。请根据你驱动包中现有的网络通信函数进行调整。关键点是构造出符合协议的JSON字符串。
2.4 第四步:解析机械臂的反馈数据
发送查询指令后,机械臂会返回一个JSON格式的反馈。驱动包的主循环会不断接收这些原始数据,并需要将其分发给对应的解析函数。我们需要做两件事:1) 在主循环的解析器Parser_Msg中识别出这是关节速度反馈;2) 编写专门的函数来解析这个反馈的具体数据。
首先,在rm_driver.cpp的Parser_Msg(char* msg)函数中添加识别逻辑。这个函数负责解析所有从机械臂收到的消息。
// 在 Parser_Msg 函数中,通常有一系列 if/else 或 switch 语句来匹配不同的命令 cJSON *root = cJSON_Parse(msg); cJSON *state_item = cJSON_GetObjectItem(root, "state"); if (state_item != NULL && cJSON_IsString(state_item)) { if (strcmp(state_item->valuestring, "joint_max_speed") == 0) { // 识别为关节最大速度反馈,调用专用解析函数 int parse_res = Parser_Joint_Max_Speed(msg); cJSON_Delete(root); if (parse_res == 0) { return ARM_JOINT_MAX_SPEED_FEEDBACK; // 返回一个自定义的成功标识 } else { return -1; // 解析失败 } } } // ... 处理其他类型的反馈 ... cJSON_Delete(root);然后,实现Parser_Joint_Max_Speed(char* msg)函数。根据协议,反馈格式类似于{"state":"joint_max_speed", "joint_speed":[30,30,30,30,30,30]}。数组中的6个整数分别对应6个关节的最大速度(单位可能是0.001°/s,需根据手册确认并转换)。
int RM_Robot::Parser_Joint_Max_Speed(char* msg) { cJSON *root = NULL, *speed_array = NULL; root = cJSON_Parse(msg); if (root == NULL) { return -1; } // 获取关节速度数组 speed_array = cJSON_GetObjectItem(root, "joint_speed"); if (speed_array == NULL || !cJSON_IsArray(speed_array)) { cJSON_Delete(root); return -2; } int array_size = cJSON_GetArraySize(speed_array); if (array_size != 6) { // 睿尔曼6自由度机械臂应有6个关节 ROS_WARN("Joint speed array size is %d, expected 6.", array_size); // 根据实际情况决定是否返回错误 } // 解析并存储数据 for (int i = 0; i < array_size && i < 6; ++i) { cJSON *item = cJSON_GetArrayItem(speed_array, i); if (cJSON_IsNumber(item)) { // 注意单位转换!协议中数值可能放大了1000倍 RM_Joint.joint_max_speed[i] = item->valueint / 1000.0; // 转换为浮点数,单位°/s ROS_DEBUG("Joint %d max speed: %.3f °/s", i+1, RM_Joint.joint_max_speed[i]); } } cJSON_Delete(root); // 触发结果发布(下一步实现) Publish_Joint_Max_Speed(); return 0; }2.5 第五步:定义与发布ROS消息
解析得到的数据需要封装成ROS消息发布出去,供其他节点使用。这需要创建一个自定义消息类型。
定义消息:在
rm_msgs/msg/目录下创建ArmJointSpeedMax.msg文件。std_msgs/Header header string[] joint_name float64[] max_velocity在
rm_msgs包的CMakeLists.txt和package.xml中添加对新消息文件的引用,并重新编译rm_msgs包 (catkin build rm_msgs)。初始化发布者:回到
rm_driver.cpp的初始化函数,添加发布者的初始化。joint_max_speed_pub_ = nh_.advertise<rm_msgs::ArmJointSpeedMax>("/rm_driver/joint_max_speed", 10);实现发布函数:在
Parser_Joint_Max_Speed成功后调用。void RM_Robot::Publish_Joint_Max_Speed() { rm_msgs::ArmJointSpeedMax msg; msg.header.stamp = ros::Time::now(); msg.joint_name = {"joint1", "joint2", "joint3", "joint4", "joint5", "joint6"}; msg.max_velocity.resize(6); for (int i = 0; i < 6; ++i) { msg.max_velocity[i] = RM_Joint.joint_max_speed[i]; } joint_max_speed_pub_.publish(msg); ROS_INFO("Published joint max speed data."); }
2.6 第六步:编译与功能验证
完成所有代码修改后,编译你的驱动包。
cd ~/catkin_ws catkin build rm_driver source devel/setup.bash启动机械臂驱动节点,并打开几个终端进行测试:
- 终端1:启动ROS核心。
roscore - 终端2:运行驱动节点。
rosrun rm_driver rm_driver - 终端3:订阅查询结果话题,准备查看数据。
rostopic echo /rm_driver/joint_max_speed - 终端4:发布查询命令。
rostopic pub -1 /rm_driver/get_joint_max_speed_cmd std_msgs/Empty "{}"
如果一切顺利,你将在终端3中看到打印出的6个关节的最大速度数据,而在终端2的驱动节点日志中,应该能看到发送指令和接收解析的成功信息。这个过程清晰地展示了从外部触发、内部通信、数据解析到结果发布的完整闭环。通过这个范例,你可以举一反三,为驱动包添加任何JSON协议支持的其他查询或设置功能,例如查询当前位姿、设置IO状态、配置力控参数等。驱动包的扩展,从此变得有迹可循。