第五十五章 基于MQTT协议连接阿里云服务器
本章主要学习lwIP提供的MQTT协议文件使用,通过 MQTT 协议将设备连接到阿里云服务器,实现远程互通。由于MQTT 协议是基于 TCP 的协议实现的,所以我们只需要在单片机端实现 TCP 客户端程序并使用 lwIP提供的MQTT文件来连接阿里云服务器。 本章分为如下几个部分: 55.1 MQTT协议简介 55.2 硬件设计 55.3 软件设计 55.4 下载验证
55.1 MQTT协议简介 (1) MQTT是什么? MQTT(Message Queuing Telemetry Transport,消息队列遥测传输协议),是一种基于发布/订阅(Publish/Subscribe)模式的轻量级通讯协议,该协议构建于TCP/IP协议上,由IBM在1999年发布,目前最新版本为v3.1.1。MQTT最大的优点在于可以以极少的代码和有限的带宽,为远程设备提供实时可靠的消息服务。做为一种低开销、低带宽占用的即时通讯协议,MQTT在物联网、小型设备、移动应用等方面有广泛的应用,MQTT协议属于应用层。 (2) MQTT协议特点 MQTT 是一个基于客户端与服务器的消息发布/订阅传输协议。MQTT 协议是轻量、简单开放和易于实现的,这些特点使它适用范围非常广泛。在很多情况下,包括受限境中,如:机器与机器(M2M)通信和物联网(IoT)。其在,通过卫星链路通信传感器、医疗设备、智能家居、及一些小型化设备中已广泛使用。 (3) MQTT协议原理及实现方式 实现 MQTT 协议需要:客户端和服务器端MQTT 协议中有三种身份:发布者(Publish)、代理(Broker)(服务器)、订阅者(Subscribe)。其中,消息的发布者和订阅者都是客户端,消息代理是服务器,消息发布者可以同时是订阅者,如下图所示。 图55.1.1 MQTT订阅和发布过程 MQTT 传输的消息分为:主题(Topic)和消息的内容(payload)两部分。 Topic:可以理解为消息的类型,订阅者订阅(Subscribe)后,就会收到该主题的消息内容(payload)。 Payload:可以理解为消息的内容,是指订阅者具体要使用的内容。 55.1.1 MQTT协议实现原理 1,要在客户端与代理服务端建立一个TCP连接,建立连接的过程是由客户端主动发起的,代理服务一直是处于指定端口的监听状态,当监听到有客户端要接入的时候,就会立刻去处理。客户端在发起连接请求时,携带客户端ID、账号、密码(无账号密码使用除外,正式项目不会允许这样)、心跳间隔时间等数据。代理服务收到后检查自己的连接权限配置中是否允许该账号密码连接,如果允许则建立会话标识并保存,绑定客户端ID与会话,并记录心跳间隔时间(判断是否掉线和启动遗嘱时用)和遗嘱消息等,然后回发连接成功确认消息给客户端,客户端收到连接成功的确认消息后,进入下一步(通常是开始订阅主题,如果不需要订阅则跳过)。如下图所示: 图55.1.1.1 客户端与代理服务器建立连接示意图 2,客户端将需要订阅的主题经过SUBSCRIBE报文发送给代理服务,代理服务则将这个主题记录到该客户端ID下(以后有这个主题发布就会发送给该客户端),然后回复确认消息SUBACK报文,客户端接到SUBACK报文后知道已经订阅成功,则处于等待监听代理服务推送的消息,也可以继续订阅其他主题或发布主题,如下图所示: 图55.1.1.2 客户端向服务器订阅示意图 3,当某一客户端发布一个主题到代理服务后,代理服务先回复该客户端收到主题的确认消息,该客户端收到确认后就可以继续自己的逻辑了。但这时主题消息还没有发给订阅了这个主题的客户端,代理要根据质量级别(QoS)来决定怎样处理这个主题。所以这里充分体现了是MQTT协议是异步通信模式,不是立即端到端反应的,如下图所示: 图55.1.1.3 客户端向代理服务器发送主题 如果发布和订阅时的质量级别QoS都是至多一次,那代理服务则检查当前订阅这个主题的客户端是否在线,在线则转发一次,收到与否不再做任何处理。这种质量对系统压力最小。 如果发布和订阅时的质量级别QoS都是至少一次,那要保证代理服务和订阅的客户端都有成功收到才可以,否则会尝试补充发送(具体机制后面讨论)。这也可能会出现同一主题多次重复发送的情况。这种质量对系统压力较大。 如果发布和订阅时的质量级别QoS都是只有一次,那要保证代理服务和订阅的客户端都有成功收到,并只收到一次不会重复发送(具体机制后面讨论)。这种质量对系统压力最大。 55.1.2 配置远程服务器 配置阿里云服务器步骤 ,如下所示。 第一步:注册阿里云平台,打开产品分类/物联网/物联网平台,如下图所示。 图55.1.2.1 打开物联网应用开发 点击上图中的“管理控制台”按键进去物联网平台页面。 第二步:在物联网平台页面下点击公共实例/设备管理/产品/创建设备,在此界面下填写项目名称等相关信息,如下图所示: 图55.1.2.4 产品参数填写 注:上图中的节点类型、连网方式、数据格式以及认证模式的选择,其他产品参数根据用户爱好设置。 第三步:创建产品之后点击设备管理添加设备,如下图所示。 图55.1.2.5 填写设备参数 第四步:进入创建的设备,点击查看三元组内容,如下图所示。 图55.1.2.5 设备信息 这三个参数非常重要!!!!!!!!!!,在本章实验中会用到。 第五步:打开“产品/查看/功能定义”路径,在该路径下添加功能定义,如下图所示。 图55.1.2.6 添加功能 第六步:打开自定义功能并发布上线,这里我们添加了两个CurrentTemperature和RelativeHumidity标签。 55.2 硬件设计 1. 例程功能 本章实验功能简介:lwIP连接阿里云实现数据上存。 2. 硬件资源 1)LED灯 LED-IO1 2)XL9555 IIC_INT-IO0(需在P5连接IO0) IIC_SDA-IO41 IIC_SCL-IO42 3)SPILCD CS-IO21 SCK-IO12 SDA-IO11 DC-IO40(在P5端口,使用跳线帽将IO_SET和LCD_DC相连) PWR- IO1_3(XL9555) RST- IO1_2(XL9555) 4)ESP32-S3内部WiFi 3. 原理图 本章实验使用的WiFi为ESP32-S3的片上资源,因此并没有相应的连接原理图。 55.3 软件设计 55.3.1 程序流程图 程序流程图能帮助我们更好的理解一个工程的功能和实现的过程,对学习和设计工程有很好的主导作用。下面看看本实验的程序流程图: 图55.3.1.1 程序流程图 55.3.2 程序解析 在本章节中,我们主要关注两个文件:lwip_demo.c和lwip_demo.h。lwip_demo.h文件主要定义了阿里云提供的三元组内容和计算得出的MQTT参数,这部分内容请参考阿里云提供的手册 “如何计算MQTT签名参数”章节,所以作者暂不详细解释。主要关注点是lwip_demo.c文件中的函数。在lwip_demo函数中,我们配置了相关的MQTT参数,并创建了一个名为mqtt_event_handler 的事件回调函数。这个事件回调函数通过获取MQTT事件ID来处理连接过程中所需的操作。接下来,我们将分别详细解释lwip_demo函数和mqtt_event_handler事件回调函数。 /** * @param 无 * @retval 无 */ void lwip_demo(void) { char mqtt_publish_data[] = "alientek esp32-s3"; /* 设置客户端的信息量 */ esp_mqtt_client_config_t mqtt_cfg = { .broker.address.hostname = HOST_NAME, /* MQTT地址 */ .broker.address.port = HOST_PORT, /* MQTT端口号 */ .broker.address.transport = MQTT_TRANSPORT_OVER_TCP, /* TCP模式 */ .credentials.client_id = CLIENT_ID, /* 设备名称 */ .credentials.username = (char*)USER_NAME, /* 产品ID */ .credentials.authentication.password = PASSWORD, /* 计算出来的密码 */ }; esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqtt_cfg); esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL); esp_mqtt_client_start(client); while(1) { if (g_publish_flag == 1) { esp_mqtt_client_publish(client,DEVICE_PUBLISH, (char *)mqtt_publish_data,strlen(mqtt_publish_data),1,0); } vTaskDelay(1000); } } 这个函数主要负责MQTT的连接配置。它首先创建了一个MQTT控制块,用于存储配置参数以及发送和接收数据。接着,它定义了一个回调函数,用于处理和响应MQTT连接过程中的各种事件。最后,它启动MQTT并发送连接请求到服务器。一旦成功连接到MQTT服务器,它就可以开始循环发布数据。现在,让我们深入了解这个回调函数的工作原理。 /** * @brief 错误日记 * @param message :错误消息 * @param error_code :错误码 * @retval 无 */ static void log_error_if_nonzero(const char *message, int error_code) { if (error_code != 0) { ESP_LOGE(TAG, "Last error %s: 0x%x", message, error_code); } } /** * @brief 注册接收MQTT事件的事件处理程序 * @param handler_args:注册到事件的用户数据 * @param base :处理程序的事件库 * @param event_id :接收到的事件的id * @param event_data :事件的数据 * @retval 无 */ static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) { esp_mqtt_event_handle_t event = event_data; esp_mqtt_client_handle_t client = event->client; int msg_id; switch ((esp_mqtt_event_id_t)event_id) { case MQTT_EVENT_CONNECTED: /* 连接事件 */ ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED"); msg_id=esp_mqtt_client_publish(client,"/topic/qos1","data_3",0,1,0); ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id); msg_id = esp_mqtt_client_subscribe(client, "/topic/qos0", 0); ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id); msg_id = esp_mqtt_client_subscribe(client, "/topic/qos1", 1); ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id); msg_id = esp_mqtt_client_unsubscribe(client, "/topic/qos1"); ESP_LOGI(TAG, "sent unsubscribe successful, msg_id=%d", msg_id); g_publish_flag = 1; /* 订阅主题 */ msg_id = esp_mqtt_client_subscribe(client, DEVICE_SUBSCRIBE, 0); ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id); break; case MQTT_EVENT_DISCONNECTED: /* 断开连接事件 */ break; case MQTT_EVENT_SUBSCRIBED: /* 订阅事件 */ ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id); msg_id=esp_mqtt_client_publish(client,"/topic/qos0","data",0,0,0); ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id); break; case MQTT_EVENT_UNSUBSCRIBED: /* 取消订阅事件 */ ESP_LOGI(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id); break; case MQTT_EVENT_PUBLISHED: /* 发布事件 */ ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id); break; case MQTT_EVENT_DATA: /* 接收数据事件 */ printf("TOPIC=%.*s\r\n", event->topic_len, event->topic); printf("DATA=%.*s\r\n", event->data_len, event->data); break; case MQTT_EVENT_ERROR: if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) { log_error_if_nonzero("reported from esp-tls", event->error_handle->esp_tls_last_esp_err); log_error_if_nonzero("reported from tls stack", event->error_handle->esp_tls_stack_err); log_error_if_nonzero("captured as transport's socket errno", event->error_handle->esp_transport_sock_errno); ESP_LOGI(TAG, "Last errno string (%s)", strerror(event->error_handle->esp_transport_sock_errno)); } break; default: ESP_LOGI(TAG, "Other event id:%d", event->event_id); break; } } 在这个回调函数中,主要处理与MQTT的交互过程。当系统接收到MQTT服务器的连接应答时,它会发送订阅主题报文。当系统接收到MQTT服务器的订阅应答报文时,它会发布一个订阅完成报文。因此,每个状态事件都需要读者根据项目需求进行相应的修改。 55.4 下载验证 程序下载成功后,打开阿里云平台的物联网平台设备管理,可以看到此时的设备处于连接状态,如下图所示。 图55.4.1 设备处于连接 MQTT连接成功后,可在日记服务中找到ESP32-S3设备发布的数据,如下图所示。 图55.4.2 设备发布的数据 我们可点击上图中的查看,可看到设备发布的消息内容,如下图所示。 图55.4.3 查看发布的消息内容
|