文章目录

  • @[toc]
  • 一、前言;
  • 二、`MQTT`的常识;
  • 三、官方核心代码;
  • 四、二次修改完善断开连接;

一、前言;


  • 由于乐鑫的MQTT代码工程存在些不足,本博文已根据部分修正部分代码。具体的刨坑链接:

https://github.com/espressif/ESP8266_RTOS_SDK/issues/285 ,修订时间:2018/8/27

  • esp8266的实时系统rtos是后面才出来支持的,其最后的调用也是调用乐鑫提供的API接口,所以,如果你已经玩转了NONOS下的编程,那么移植rtos代码是非常迅捷的,因为你已经对其的API接口非常熟悉,当然了,熟透一款芯片开发,当然不是一天半天的事情,需要长时间的积累。
  • 那么本博文是基于rtosMQTT协议的实现,优化了官方的代码示范,而且带你走一走MQTT协议的世界。

二、MQTT的常识;


众所周知,MQTT是一种轻捷快速的协议,基于TCP之上,所以为长连接的一种协议,非常适合那些短小消息发送的数据交互的用途,比如APP的推送新闻用途,最常见的用在我们现在物联网领域;毕竟是小且快;


  • 在进行彼此通讯时候,必须确保底层提供了有序、可靠、双向连接的网络连接。比如可以建立TCP/TLS连接。所以基本的通讯如下:

esp8266集成了FreeRtos吗 esp8266移植freertos_服务器


  • 那么设备之间怎么样通讯呢?这就涉及到一些术语;要想指定某一个设备收到此条消息,那么就必须根据topic主题来识别,这个是服务器的事情了;下面列下一些常见的专用名词:

①:ClientID

客户端唯一标识,服务端用于关联一个Session。
只能包含这些 大写字母,小写字母 和 数字(0-9a-zA-Z),23个字符以内,同一时间内 Server 和同一个 ClientID 只能保持一个 TCP 连接,再次连接会踢掉前一个连接的客户端。

②:Keep Alive

顾名思义,目的是保持长连接的可靠性,以及双方对彼此是否在线的确认。
客户端在Connect的时候设置 Keep Alive 时长。如果服务端在 1.5 * KeepAlive 时间内没有收到客户端的报文,它必须断开客户端的网络连接。

③:Will

遗嘱,遗愿;遗嘱消息(Will Message)存储在服务端,当网络连接关闭时,服务端必须发布这个遗嘱消息,所以被形象地称之为遗嘱,可用于通知异常断线。

④:retain

0: 服务端不能存储这个消息,也不能移除或替换任何 现存的保留消息 。
1: 服务端必须存储这个应用消息和它的QoS等级,以便它可以被分发给未来的订阅者,所以如果后面未来有客户端订阅了这个主题,那么这个客户端一上线就会收到此消息。

⑤:qos

0: 【最多一次】 没有回复,不需要存储。有可能丢失(网络异常断开,业务层繁忙或者错误) 。
1: 【至少一次】确保消息到达,但消息重复可能会发生。
2: 【只有一次】确保消息到达一次;

⑥:poyload

用来传输用户的数据,最大允许 256MB ,发布的消息的 Payload允许为空。在很多场合下,代表将持久消息(或者遗嘱消息)清空;格式为UTF-8编码;


三、官方核心代码;

  • 乐鑫已经针对rtos移植了eclipse的标准的paho mqtt,在官方的GitHub已经看到了源码:点我查看,这个库非常出名,很多嵌入式的芯片都是移植这个库。
#define MQTT_CLIENT_THREAD_NAME         "mqtt_client_thread"
#define MQTT_CLIENT_THREAD_STACK_WORDS  2048
#define MQTT_CLIENT_THREAD_PRIO         8

LOCAL xTaskHandle mqttc_client_handle;

static void messageArrived(MessageData* data)
{
    printf("Message arrived: %s\n", data->message->payload);
}

static void mqtt_client_thread(void* pvParameters)
{
    printf("mqtt client thread starts\n");
    MQTTClient client;
    Network network;
    //指定缓存区的大小
    unsigned char sendbuf[80], readbuf[80] = {0};
    int rc = 0, count = 1;
    MQTTPacket_connectData connectData = MQTTPacket_connectData_initializer;
    pvParameters = 0;
    
    //初始化TCP连接
    NetworkInit(&network);
    //初始化客户端,注意后面都是发送和接收数据的缓存区,一定要加大这个缓存区的大小;否则后面发送不成功!
    MQTTClientInit(&client, &network, 30000, sendbuf, sizeof(sendbuf), readbuf, sizeof(readbuf));

    char* address = MQTT_BROKER;
    //底层的TCP开始连接
    if ((rc = NetworkConnect(&network, address, MQTT_PORT)) != 0) {
        printf("Return code from network connect is %d\n", rc);
    }

//后天任务:如果这个不成功执行,就不会自动进去回调方法:messageArrived;
#if defined(MQTT_TASK)

    if ((rc = MQTTStartTask(&client)) != pdPASS) {
        printf("Return code from start tasks is %d\n", rc);
    } else {
        printf("Use MQTTStartTask\n");
    }

#endif
   
    //定义mqtt版本:  3 = 3.1 , 4 = 3.1.1
    connectData.MQTTVersion = 3;
    //定义客户端ID(必须唯一): 大伙们可以定义mac地址作为ID
	connectData.clientID.cstring = "ESP8266_sample";
	//定义连接的账户名,这个根据服务器的选型来弄;【可有可无】
    connectData.username.cstring= "admin";
    //定义连接的账户名密码,这个根据服务器的选型来弄;【可有可无】
    connectData.password.cstring="admin123456";
    //定义连接心跳;
    connectData.keepAliveInterval = 40;
    //清楚会话
    connectData.cleansession = true;

    //连接MQTT服务器
    if ((rc = MQTTConnect(&client, &connectData)) != 0) {
        printf("Return code from MQTT connect is %d\n", rc);
    } else {
        printf("MQTT Connected\n");
    }

    //订阅主题 MQTTSubscribe --->【ESP8266/sample/pub】
    if ((rc = MQTTSubscribe(&client, "ESP8266/sample/pub", 2, messageArrived)) != 0) {
        printf("Return code from MQTT subscribe is %d\n", rc);
    } else {
        printf("fuck MQTT subscribe to topic \"ESP8266/sample/pub\"\n");
    }

    //死循环,时隔一秒发送一则消息
	while (count++) {
        //初始化一则消息结构体
		MQTTMessage message;
		char payload[80];
		message.qos = QOS2;
		message.retained = 0;
		message.payload = payload;
		sprintf(payload,
				"{\"uuid\":\"dsaasdad22\",\"token\":\"saddsa412\",\"ver\":1.0,\"statusCode\":0,\"skill\":%d}",
				count);
		message.payloadlen = strlen(payload);

		printf("MQTT publish to payloadlen :%s\n", 	message.payload);

		if ((rc = MQTTPublish(&client, "ESP8266/sample/pub", &message)) != 0) {
			printf("Return code from MQTT publish is %d\n", rc);
		} else {
			printf(
					"MQTT publish topic \"ESP8266/sample/pub\", message number is %d\n",
					count);
		}

		vTaskDelay(1000 / portTICK_RATE_MS);  //send every 1 seconds
	}

    printf("mqtt_client_thread going to be deleted\n");
    vTaskDelete(NULL);
    return;
}

四、二次修改完善断开连接;


  • 这个库和乐鑫自己做的那份NONOS代码不一样,这个是不会自动重连服务器的,假如你的路由器突然没了外网,导致这个连接不成功,那么就会永远发布不了消息;所以优化如下,代码略多,主要原理:

1、通过判断是否发布消息成功的标志,是否重新连接服务器和订阅主题;

2、 如果把发布消息的任务独立开来,就相当于开启了新的线程,我看了一些高质量的代码,都是创建一则消息队列,处于阻塞等待,直到有消息要发布,则在此死循环内发布,如果不发布,那么重新连接则发布;

3、连接和订阅主题的代码都是在死循环内的,但是初始化客户端的代码千万别在死循环内,因为这个初始化相当于开辟了内存,会占据内存,多次了连接了 ,就相当于开辟多个内存了!

static void Task_MqttClient_Connect(void* pvParameters) {

	bool isNeedQueue = true;

	Network network;
	unsigned char sendbuf[2048], readbuf[2048] = { 0 };
	int rc = 0, count = 0;
	MQTTPacket_connectData connectData = MQTTPacket_connectData_initializer;
	pvParameters = 0;
	NetworkInit(&network);
	MQTTClientInit(&client, &network, 30000, sendbuf, sizeof(sendbuf), readbuf,
			sizeof(readbuf));

    //!!!!!不要把初始化放在里面
	for (;;) {

        //判断是否已经获取了路由器分配的IP
		while (wifi_station_get_connect_status() != STATION_GOT_IP) {
			vTaskDelay(1000 / portTICK_RATE_MS);
		}

		char* address = MQTT_SERVICE;
		connectData.MQTTVersion = 3;
		connectData.clientID.cstring = checkTopic;
		connectData.username.cstring = MQTT_USER_NAME;
		connectData.password.cstring = MQTT_USER_PAW;
		connectData.keepAliveInterval = 40;
		connectData.cleansession = true;

		if ((rc = NetworkConnect(&network, address, MQTT_PORT)) != 0) {
			printf("MClouds NetworkConnect connect is %d\n", rc);
		}

		if ((rc = MQTTStartTask(&client)) != pdPASS) {
			printf("Return code from start tasks is %d\n", rc);
		} else {
			printf("Use MQTTStartTask\n");
		}

		if ((rc = MQTTConnect(&client, &connectData)) != 0) {
			printf("[SY] MClouds connect is %d\n", rc);
			network.disconnect(&network);
			vTaskDelay(1000 / portTICK_RATE_MS);
		}

		if ((rc = MQTTSubscribe(&client, subTopic, QOS0, MessageArrived))
				!= 0) {
			printf("[SY] MClouds sub fail is %d\n", rc);
			network.disconnect(&network);
			vTaskDelay(1000 / portTICK_RATE_MS);
		}

		printf("MQTT subscribe to topic -> %s\n", subTopic);
		xQueueReset(MqttMessageQueueHandler);

		while (1) {

			char payload[2048];
            
			struct esp_mqtt_msg_type *pMsg;
			printf("MqttMessageQueueHandler waitting ..\n");
			
			//阻塞等待
			xQueueReceive(MqttMessageQueueHandler, &pMsg, portMAX_DELAY);
			sprintf(payload, "%s", pMsg->allData);
			//printf("MQTT  publish payload: %s\n", payload);
			os_printf(" [SY] 1 MQTT get freeHeap: %d\n",system_get_free_heap_size());
			

			MQTTMessage message;
			message.qos = QOS0;
			message.retained = false;
			message.payload = (void*) payload;
			message.payloadlen = strlen(payload) + 1;

			if ((rc = MQTTPublish(&client, pubTopic, &message)) != 0) {
				printf("Return code from MQTT publish is %d\n", rc);
			} else {
				printf("MQTT publish succeed ..\n");
			}

			if (rc != 0) {
				break;
			}

		}
		network.disconnect(&network);
	}

	printf("mqtt_client_thread going to be deleted\n");
	vTaskDelete(NULL);
	return;

}

(注意要填写服务器地址,还要熟悉rtos的消息队列。)
1.一定要用最新版的SDK包的工程,而且要看博文前面的刨坑的连接里面的库文件是否更新到您的工程。
2. 由于下面的硬件代码链接不可以修改了,大家下载之后,修改下静态库文件和上面的Task_MqttClient_Connect方法即可。之后通过不断轮询服务器是否断开,如果是则发送消息重连即可。
3.目前2018.8.27为止,v2.0.0的SDK的MQTT还是蛮稳定的。断开连接的可能性较低。