使用 MQTT - C 访问 IoTDA 平台:一个完整的嵌入式示例
引言
在物联网(IoT)开发领域,设备与平台之间的通信至关重要。MQTT 作为一种轻量级的消息传输协议,因其高效、可靠的特性,在物联网场景中得到了广泛应用。华为的 IoTDA(IoT Device Access)平台为开发者提供了便捷的设备接入和管理能力。本文将详细介绍如何使用 MQTT - C 库实现设备与 IoTDA 平台的通信,并结合具体代码示例进行深入剖析。
本次示例程序旨在演示如何使用MQTT - C 库访问 IoTDA 平台。该程序包含了设备连接、消息订阅、消息解析以及 LED 控制等功能,适合对嵌入式系统开发和物联网通信感兴趣的开发者学习参考。MQTT - C库是一个相对小众的C语言库,但是它代码短小,简单易用,非常适合嵌入式领域。
华为IoTDA平台配置
在运行程序前必须在华为IoTDA平台上建立设备,并配置好物模型。
建立设备
我需要先在平台创建产品和设备,有关产品和设备的创建,可以参考我以前的博文:【HZHY-AI300G智能盒试用连载体验】在华为IoTDA平台上建立设备_hzhy-ai300g智盒-CSDN博客
建立物模型
在线开发产品模型前需要创建产品。创建产品需要输入产品名称、协议类型、数据格式、所属行业和设备类型等信息,产品模型会使用这些信息作为设备能力字段取值。物联网平台提供了标准模型和厂商模型,这些模型涉及多个领域,模型中提供了已经编辑好的产品模型文件,您可以根据自己的需要对产品模型中的字段进行修改和增删;如果选择自定义产品模型,则需要完整定义产品模型。
操作步骤
- 访问设备接入服务,单击“管理控制台”进入“设备接入”控制台。选择您的实例,单击实例卡片进入。
- 单击左侧导航栏的“产品”,在产品列表中,找到对应的产品,单击产品进入产品详情页。
- 在产品详情基本信息页面,单击“自定义模型”,添加服务。
- 输入“服务ID”、“服务类型”和“服务描述”,然后单击“确定”。
- “服务ID”:采用首字母大写的命名方式。比如:WaterMeter、StreetLight。
- “服务类型”:建议和服务ID保持一致。
- “服务描述”:比如路灯上报的环境光强度和路灯开关状态的属性。
- 添加服务后,在“添加服务”区域,对属性和命令进行定义。每个服务下,可以包含属性和命令,也可以只包含其中之一,请根据此类设备的实际情况进行配置。
- 单击步骤4新增的服务ID,在展开的页面单击“新增属性”,在弹出窗口中配置属性的各项参数,然后单击“确定”。
我建立了一个smarthome的物模型,包括温度、湿度和LED状态3个属性,其中LED状态属性是平台可以修改的,另外两个都是设备上报的。
项目结构与关键组件
1. 头文件与宏定义
程序对接入地址的定义如下(为了安全,其中的部分登录信息做了修改):
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "mqtt.h"
#include "templates/posix_sockets.h"
#include <pthread.h>
#include "cJSON.h"#define ADDRESS "bde4cbe7aa.st1.iotda-device.cn-north-4.myhuaweicloud.com"
#define PORT "1883"
#define CLIENT_ID "67a2xxxxxxxc_test_0_0_2025042802"
#define USERNAME "67a2xxxxx1872637d83a1c_test"
#define PASSWORD "c42xxxxxxxc3f6763"
#define TOPIC "$oc/devices/设备ID/sys/messages/down"
#define QOS 1
#define SEND_BUF_SIZE 1024
#define RECV_BUF_SIZE 1024
代码中引入了多个必要的头文件,包括标准库、MQTT 库、线程库以及 JSON 解析库。同时,通过宏定义指定了连接 IoTDA 平台所需的地址、端口、客户端 ID、用户名、密码等信息。
2. 核心函数
2.1 control_led
函数
该函数用于模拟 LED 的开关操作,根据传入的状态参数打印相应的信息。
// 模拟 LED 操作函数
void control_led(int state) {if (state == 1) {printf("LED 已打开\n");} else {printf("LED 已关闭\n");}
}
2.2 publish_response_callback
函数
此函数是消息回调函数,当 MQTT 客户端接收到消息时会触发该函数。它会将接收到的非空字符结尾的主题名称转换为 C 风格字符串,打印消息信息,并对 JSON 格式的消息进行解析。如果主题包含
/sys/properties/set
,则构造新主题并创建新线程发布响应消息。
// 消息回调函数
void publish_response_callback(void** unused, struct mqtt_response_publish *published)
{/* 注意:published->topic_name 不是以空字符结尾的(这里我们会将其转换为 C 字符串) */char* topic_name = (char*) malloc(published->topic_name_size + 1);if (topic_name == NULL) {perror("内存分配失败");return;}memcpy(topic_name, published->topic_name, published->topic_name_size);topic_name[published->topic_name_size] = '\0';printf("收到消息:('%s'): %s\n", topic_name, (const char*) published->application_message);// 解析 JSON 消息cJSON *root = cJSON_Parse((const char*) published->application_message);if (root == NULL) {const char *error_ptr = cJSON_GetErrorPtr();if (error_ptr != NULL) {fprintf(stderr, "JSON 解析错误,错误位置: %s\n", error_ptr);}free(topic_name);return;}// 查找 services 数组cJSON *services = cJSON_GetObjectItem(root, "services");if (!cJSON_IsArray(services)) {fprintf(stderr, "未找到 services 数组或不是数组类型\n");cJSON_Delete(root);free(topic_name);return;}// 遍历 services 数组int array_size = cJSON_GetArraySize(services);for (int i = 0; i < array_size; i++) {cJSON *service = cJSON_GetArrayItem(services, i);if (!cJSON_IsObject(service)) {continue;}// 查找 serviceId 为 smarthome 的项cJSON *serviceId = cJSON_GetObjectItem(service, "serviceId");if (cJSON_IsString(serviceId) && strcmp(serviceId->valuestring, "smarthome") == 0) {// 查找 properties 对象cJSON *properties = cJSON_GetObjectItem(service, "properties");if (cJSON_IsObject(properties)) {// 查找 LED状态 字段cJSON *led_status = cJSON_GetObjectItem(properties, "LED状态");if (cJSON_IsString(led_status) && led_status->valuestring != NULL) {printf("解析到 LED 状态字段: %s\n", led_status->valuestring);// 模拟 LED 操作if (strcmp(led_status->valuestring, "ON") == 0) {control_led(1);} else {control_led(0);}}}break;}}// 检查主题是否包含 /sys/properties/setconst char* target_str = "/sys/properties/set";char* pos = strstr(topic_name, target_str);if (pos != NULL) {// 计算新主题长度size_t new_topic_len = strlen(topic_name) + strlen("/response");char* new_topic = (char*) malloc(new_topic_len + 1);if (new_topic == NULL) {perror("新主题内存分配失败");free(topic_name);cJSON_Delete(root);return;}// 构造新主题strncpy(new_topic, topic_name, pos - topic_name + strlen(target_str));new_topic[pos - topic_name + strlen(target_str)] = '\0'; // 确保字符串结束strncat(new_topic, "/response", new_topic_len - strlen(new_topic));strncat(new_topic, pos + strlen(target_str), new_topic_len - strlen(new_topic));new_topic[new_topic_len] = '\0'; // 确保字符串结束// 分配参数结构体内存PublishArgs *args = (PublishArgs *)malloc(sizeof(PublishArgs));if (args == NULL) {perror("参数结构体内存分配失败");free(new_topic);free(topic_name);cJSON_Delete(root);return;}args->client = &client;args->new_topic = new_topic;args->response_message = response_message;args->message_size = strlen(response_message) + 1;// 创建新线程发布消息pthread_t publish_thread;if (pthread_create(&publish_thread, NULL, publish_message_thread, args) != 0) {perror("创建发布消息线程失败");free(new_topic);free(args);}// 分离线程,让系统自动回收资源pthread_detach(publish_thread);}cJSON_Delete(root); // 释放 cJSON 对象内存free(topic_name);
}
2.3 publish_message_thread
函数
该函数是一个线程函数,用于将响应消息发布到新主题。发布完成后,会释放动态分配的内存。
// 新线程函数,用于发布消息
void* publish_message_thread(void* arg) {PublishArgs *args = (PublishArgs *)arg;// 发布消息到新主题printf("发布消息:('%s'): %s\n", args->new_topic, (const char*) args->response_message);int rc = mqtt_publish(args->client, args->new_topic, args->response_message, args->message_size, MQTT_PUBLISH_QOS_0);if (rc != MQTT_OK) {printf("发布消息到新主题失败,错误码: %d\n", rc);} else {printf("已发布消息到新主题: %s\n", args->new_topic);}// 释放内存free(args->new_topic);free(args);return NULL;
}
2.4 connect_to_server
函数
该函数负责初始化 MQTT 客户端并连接到 IoTDA 服务器。如果初始化或连接失败,会打印相应的错误信息。
// 连接服务器
int connect_to_server(struct mqtt_client *client, mqtt_pal_socket_handle sockfd) {int rc;// 初始化 MQTT 客户端rc = mqtt_init(client, sockfd, sendbuf, SEND_BUF_SIZE, recvbuf, RECV_BUF_SIZE, publish_response_callback);if (rc != MQTT_OK) {printf("MQTT 初始化失败,错误码: %d\n", rc);return -1;}// 连接到服务器rc = mqtt_connect(client, CLIENT_ID, NULL, NULL, 0, USERNAME, PASSWORD, MQTT_CONNECT_CLEAN_SESSION, 400);if (rc != MQTT_OK) {printf("连接失败,错误码: %d\n", rc);return -1;}return 0;
}
3. main
函数
main函数是程序的入口,它会创建套接字,调用 connect_to_server 函数连接到服务器,启动一个线程来刷新客户端,监听 IoTDA 消息,直到用户按下 CTRL - D 退出程序。
int main(int argc, char *argv[]) {mqtt_pal_socket_handle sockfd;int rc;// 创建套接字sockfd = open_nb_socket(ADDRESS, PORT);if (sockfd == -1) {perror("创建套接字失败");}// 连接 MQTT 服务器if (connect_to_server(&client, sockfd) != 0) {close(sockfd); // 使用 close 函数关闭套接字return -1;}/* 启动一个线程来刷新客户端(处理客户端的入站和出站流量) */pthread_t client_daemon;if(pthread_create(&client_daemon, NULL, client_refresher, &client)) {fprintf(stderr, "启动客户端守护线程失败。\n");return -1;}/* start publishing the time */printf("listening for IoTDA messages.\n");printf("Press CTRL-D to exit.\n\n");/* block */while(fgetc(stdin) != EOF);// 断开连接mqtt_disconnect(&client);close(sockfd); // 使用 close 函数关闭套接字return 0;
}
JSON 消息解析与处理
程序使用 cJSON 库对接收到的 JSON 格式消息进行解析。在 publish_response_callback
函数中,会查找 services
数组,遍历数组找到 serviceId
为 smarthome
的项,然后在其 properties
对象中查找 LED状态
字段,根据字段值调用 control_led
函数模拟 LED 操作。
多线程处理
为了提高程序的并发性能,程序使用了多线程技术。在 publish_response_callback
函数中,如果需要发布响应消息,会创建一个新线程 publish_message_thread
来处理消息发布,避免阻塞主线程。同时,使用 pthread_detach
函数分离线程,让系统自动回收线程资源。
测试
程序启动后,在API Explorer中对程序进行控制。
程序正确执行。
总结
通过本文的介绍,我们了解了如何使用 MQTT - C 库实现设备与 IoTDA 平台的通信。从项目的结构和关键组件入手,详细分析了核心函数的功能和实现原理,以及 JSON 消息解析和多线程处理的方法。这个示例程序为开发者提供了一个完整的嵌入式物联网通信解决方案,开发者可以根据实际需求进行扩展和优化。