Bootstrap

联盛德W801系列9-wifi和4G模块(air724ug)并存使用MQTT总结

第一次在产品中使用iot功能,完全没有经验,可能会存在很多bug,请各位大侠不吝赐教,指点一二。

1.air724ug-AT版本的使用

1.1 air724简介

我这次使用的是合宙的air724ug模组,成品是飞思创的FS-MCore-A724UG。
实物图片:
在这里插入图片描述
这个成品有2个软件版本:

1.(AT)
AT版本是直接使用合宙的AT指令。
2.(YunDTU)
YunDTU版本功能完善,覆盖绝大多数应用场景,用户只需通过简单的配置,即可实现产品联网。支持 TCP Client、UDP Client、MQTT、HTTP、阿里云、OneNET、百度云、腾讯云和华为云等多种工作模式,并支持 HTTP、FTP 他升级以及 FOTA 自升级。
以MQTT应用为例,说明一下如何通过简单配置,使用YunDTU功能:
下面的配置界面提供了MQTT通信所需要的全部配置,不过只能有一个订阅主题和一个发布主题,配置好之后,上电就会自动连接MQTT服务器,此时设备只管接收消息和发布消息。
在这里插入图片描述

我用的是AT版本。在优信电子购买,店家提供了详细的资料,包括STM32使用AT指令的例程。

1.2 模块上电上报信息

模块插上中国移动SIM卡,通电后,通过SIM卡,注册网络后会从网络自动获取并激活一个PDP上下文。下面是串口观察到的打印信息:
在这里插入图片描述

1.3 AT指令使用流程

AT指令必须以回车符(0x0D)结束,以回车+换行(0xA)结束也行。
在这里插入图片描述

1.4 MQTT相关的AT指令中关于双引号"的处理

多数指令关于双引号是可有可无的,例如设置订阅主题的指令:
可以有双引号括住订阅主题:

AT+MSUB="/subtop/1",0

也可以不用双引号:

AT+MSUB=/subtop/1,0

唯一的例外就是发布消息的指令 AT+MPUB 中的消息部分,一定要双引号。
在这里插入图片描述
我们一般发送消息使用json格式,比如{“name”:“tom”},这个消息放到AT指令该怎样表示呢?一般的话,我们用C语言字符串表达方法是这样(发布主题可以不用双引号):

char * strPub="AT+MPUB=/pubtopic/1,0,0,\"{\"name\":\"tom\"}\"\r\n";

上面的表达方法是无法正确被AT指令解析的。C语言的双引号使用转义字符反斜杠+双引号来表示,json内嵌的双引号只能使用反斜杠字符+十六进制表示,而C语言字符串中反斜杠需要两个反斜杠表示。
在这里插入图片描述
上面4个红框要被 \\22 替代,要写成下面这样,才能准确地被AT指令正确解析:

char * strPub="AT+MPUB=/pubtopic/1,0,0,\"{\\22name\\22:\\22tom\\22}\"\r\n";

2.通过AT指令控制air724的代码详解

2.1 发送AT指令的函数

/** 发送AT指令并等待应答
 *
 * 指定尝试的次数和每次发送后等待应答的时间
 * 
 * @param tryTimes 	尝试次数
 * @param cmdStr 	AT命令字符串
 * @param replyStr 	期望应答字符串
 * @param waitTime 	每次发送后等待应答的时间
 *
 * @retval 0-没有接收到期望的字符串
 * @retval 1-成功接收到期望的字符串
 */
int		SendATCmdAndWaitReply(int tryTimes,const char * cmdStr, const char* replyStr,int waitTime)
{
	int rx_len,ret;
	u32	lastTime;
	//	1.尝试的次数
	for(int i=0;i<tryTimes;i++){
		if(cmdStr != NULL)		
			tls_uart_write(LTE_4G_COM,(u8 *) cmdStr,strlen(cmdStr));
		
		//	2.记录发送时的时间戳,并做100ms的延时
		lastTime = xTaskGetTickCount();
		vTaskDelay(50);  //	一个时间片是2ms	,这里就是100ms	
		do{
			rx_len = tls_uart_try_read(LTE_4G_COM,1);
			vTaskDelay(10);
			if(rx_len > 0){
				rx_len = tls_uart_try_read(LTE_4G_COM,1);
				memset(demo_uart3->rx_buf,0,100);
				
				//	如果接收到的数据量大于接收缓存 (256 bytes),数据丢弃
				//	因为串口中断的缓存有4K Byte,但是demo_uart3->rx_buf的空间只给了256 Byte
				if(rx_len >= DEMO_UART3_RX_BUF_SIZE){
					do{
						rx_len = 200;
						ret = tls_uart_read(LTE_4G_COM, (u8 *) demo_uart3->rx_buf, rx_len);
					}while(ret > 100);
					continue;
				}
			//	3.判断接收的数据是否有期望的字符串	
				ret = tls_uart_read(LTE_4G_COM, (u8 *) demo_uart3->rx_buf, rx_len); 
				printf("%s\r\n",demo_uart3->rx_buf);
				if(ret > 2){
					demo_uart3->rx_buf[ret] = 0;
					if(strstr(demo_uart3->rx_buf,replyStr) != NULL){
						printf("find %s\n",replyStr);
						return 1;						
					}
				}
			}
		//	4.在等待的时间内,每20ms查询一次串口;超时则重新发送AT指令
		}while((xTaskGetTickCount()-lastTime )< waitTime);
		
	}
	//	5.如果执行到这里,说明失败了
	printf("wait %s fail\n",replyStr);
	return 0;
}

2.2 控制air724模块的状态机流程

在这里插入图片描述
状态机的状态码 :

enum _Stat_4G{
	Stat4G_WaitReady=0,
	Stat4G_CloseEcho=1,		//	1.关闭回显命令
	Stat4G_CGREG,			// 	2.查询网络状态
	Stat4G_CGATT,			//	3.查询是否附着上数据网络
	Stat4G_CSTT,			//	4.设置接入点 APN,可无参数
	Stat4G_CIICR,			//	5.激活移动场景
	Stat4G_CIFSR,			//	6.获取IP地址
	
	Stat4G_Mconfig,			//	7.设置MQTT接入密码
	Stat4G_Mipstart,		//	8.设置MQTT服务器IP 或域名
	Stat4G_Mconnect,		//	9.设置 心跳时间
	
	Stat4G_Msub,			//	10.设置 订阅主题
	Stat4G_MQuerySubMsg,	//	11.在这里等待消息,死循环
	Stat4G_4GIdle,			//	12.有了wifi,4G关闭电源
	Stat4G_4GPowerReset,	//	13.出现错误,关闭电源50ms后再次上电,进入重连状态   WaitReady
	
};

状态机相关源码(使用W801的串口3控制4G模块):

const char * StrNect="NECT";		//	命令 ipstart 的应答
const char * StrAct="ACK";			//	命令 connect 的应答    订阅消息成功后的应答
const char * StrReady="READY";
const char * StrSMS="SMS";
const char * StrOK="OK";
const char * Strdot=".";
const char * StrATE0="ATE0\r\n";
const char * StrGREG="AT+CGREG?\r\n";	// 	2.查询网络状态
const char * StrGATT="AT+CGATT?\r\n";//	3.查询是否附着上数据网络
const char * StrCSTT="AT+CSTT\r\n";	//	4.设置接入点 APN,可无参数
const char * StrIICR="AT+CIICR\r\n";	//	5.激活移动场景
const char * StrIFSR="AT+CIFSR\r\n";
const char * StrMconnectQry="AT+MCONNECT=?\r\n";
stat4G = Stat4G_WaitReady;
    for (;;)
    {
		ticks20ms++;
		if(ticks20ms > 50*20){
			ticks20ms = 0;
		//	0.不知道4G模块有没有MQTT的心跳,这里使用查询MQTT状态来模拟心跳
			if(Stat4G_MQuerySubMsg == stat4G){
				tls_uart_write(LTE_4G_COM,(u8 *) StrMconnectQry,strlen(StrMconnectQry));				
                printf("PING\n");
			}
		}
		//	1.	20ms运行一次
		vTaskDelay(10);  
		//	有2种消息
		if(xQueueReceive(demo_uart3->demo_uart_q,&(cmdmsg),0)){
			//	消息1.  4G模块处于断电状态,如果 wifi断开了,重连一次失败,会发送重启4G的消息
			if((cmdmsg == DEMO_MSG_4G_RESTART)){			
				tls_gpio_write(POWER_4G_CTRL_PIN,1);
				stat4G = Stat4G_WaitReady;
			//	消息2.	本来4G模块处于正常通信中 ,wifi连上网络了,优先使用wifi,要断开4G电源,任务进入空闲等待
			}else if(cmdmsg == DEMO_MSG_4G_ENTER_IDLE){
				tls_gpio_write(POWER_4G_CTRL_PIN,0);
				stat4G = Stat4G_4GIdle;
			}
		}
        switch ( stat4G)
        {
        case Stat4G_WaitReady:
                printf("wait %s\n",StrSMS);
			if(SendATCmdAndWaitReply(60,NULL,StrSMS,1000)){
				stat4G++;
				vTaskDelay(5*1000);
			}else{
				stat4G = Stat4G_4GPowerReset;				
			}  

        break;
        case Stat4G_CloseEcho://	1.关闭回显命令
                printf("StrATE0\n");
			if(SendATCmdAndWaitReply(TRY_TIME_MAX,StrATE0,StrOK,500)){
				stat4G++;
			}else{
				SendATCmdAndWaitReply(2,StrRESET,StrOK,200);
				stat4G = Stat4G_WaitReady;
			}
        break;
		
        case Stat4G_CGREG:// 	2.查询网络状态 --OK
                printf("%s\n",StrGREG);
			if(SendATCmdAndWaitReply(TRY_TIME_MAX,StrGREG,StrOK,500)){
				stat4G++;
			}else{
				SendATCmdAndWaitReply(2,StrRESET,StrOK,200);
				stat4G = Stat4G_WaitReady;
			}
        break;
        case Stat4G_CGATT:	//	3.查询是否附着上数据网络
                printf("%s\n",StrGATT);
			if(SendATCmdAndWaitReply(TRY_TIME_MAX,StrGATT,StrOK,500)){
				stat4G++;
			}else{
				SendATCmdAndWaitReply(2,StrRESET,StrOK,200);
				stat4G = Stat4G_WaitReady;
			}
        break;
        case Stat4G_CSTT:	//	4.设置接入点 APN,可无参数
                printf("%s\n",StrCSTT);
			if(SendATCmdAndWaitReply(TRY_TIME_MAX,StrCSTT,StrOK,500)){
				stat4G++;
			}else{
				SendATCmdAndWaitReply(2,StrRESET,StrOK,200);
				stat4G = Stat4G_WaitReady;
			}
        break;
        case Stat4G_CIICR:	//	5.激活移动场景 StrIICR
                printf("%s\n",StrIICR);
			if(SendATCmdAndWaitReply(TRY_TIME_MAX,StrIICR,StrOK,500)){
				stat4G++;
			}
        break;
        case Stat4G_CIFSR:	//	6.获取IP地址
                printf("%s\n",StrIFSR);
			if(SendATCmdAndWaitReply(TRY_TIME_MAX,StrIFSR,Strdot,500)){
				stat4G++;
			}else{
				SendATCmdAndWaitReply(2,StrRESET,StrOK,200);
				stat4G = Stat4G_WaitReady;
			}
        break;
			
        case Stat4G_Mconfig:	//	7.设置MQTT接入密码
                printf("set MQTT ID,pwd\n");
			sprintf(Buf,StrMconfig,MAC2STR(g_macBuf));
			if(SendATCmdAndWaitReply(TRY_TIME_MAX,Buf,StrOK,1000)){
				stat4G++;
			}else{
				SendATCmdAndWaitReply(2,StrRESET,StrOK,200);
				stat4G = Stat4G_WaitReady;
			}
            break;
        case Stat4G_Mipstart:	//	8.设置MQTT服务器IP 或域名
                printf("set MQTT server\n");
			if(SendATCmdAndWaitReply(TRY_TIME_MAX,StrMipstart,StrNect,1000)){
				stat4G++;
			}else{
				SendATCmdAndWaitReply(2,StrRESET,StrOK,200);
				stat4G = Stat4G_WaitReady;
			}
            break;
        case Stat4G_Mconnect:	//	9.设置 心跳时间
                printf("set heart beat\n");
			if(SendATCmdAndWaitReply(TRY_TIME_MAX,StrMconnect,StrAct,2000)){
				stat4G++;
			}else{
				SendATCmdAndWaitReply(2,StrRESET,StrOK,200);
				stat4G = Stat4G_WaitReady;
			}
            break;	
        case Stat4G_Msub:		//	10.设置 订阅主题
                printf("set sub topic\n");
				sprintf(Buf,StrMsubTop,MAC2STR(g_macBuf));
			if(SendATCmdAndWaitReply(TRY_TIME_MAX,Buf,StrAct,1000)){
				stat4G++;
				g_internetStat = internetStat_4GConnect;
			}else{
				SendATCmdAndWaitReply(2,StrRESET,StrOK,200);
				stat4G = Stat4G_WaitReady;
			}
            break;
        case Stat4G_MQuerySubMsg:	//	查询消息 -- 大部分时间在这里		 				
			rx_len = tls_uart_try_read(LTE_4G_COM,1);
			if(rx_len > 0){
				vTaskDelay(5);
				rx_len = tls_uart_try_read(LTE_4G_COM,1);
				//	如果接收到的数据量大于接收缓存 (256 bytes),数据丢弃
				if(rx_len >= DEMO_UART3_RX_BUF_SIZE){
					do{
						rx_len = 200;
						ret = tls_uart_read(LTE_4G_COM, (u8 *) demo_uart3->rx_buf, rx_len);
					}while(ret > 100);
					continue;
				}
				ret = tls_uart_read(LTE_4G_COM, (u8 *) demo_uart3->rx_buf, rx_len); 
				demo_uart3->rx_buf[ret] = 0;
					printf("Msg: %s\n",demo_uart3->rx_buf);
				//	处理订阅数据
					char *p = NULL;//, *q = NULL;
					
					p = strstr(demo_uart3->rx_buf, "byte,");
					if (p != NULL)
					{
						p = p+5;					

						rx_len  =p  - demo_uart3->rx_buf;	//	json数据首地址相对于整串数据的偏移量
						if((ret -rx_len) < 50)	{			//	json数据的长度不应该大于50
							mqtt_msg_process(p);
						}
					}
					异常情况判断
					p = strstr(demo_uart3->rx_buf, "ERROR");
					if(p != NULL){
						stat4G = Stat4G_4GPowerReset;
						break;
					}
					p = strstr(demo_uart3->rx_buf, "DEACT");
					if(p != NULL){
						stat4G = Stat4G_4GPowerReset;
						break;//
					}
			}
            break;
		case Stat4G_4GIdle:			
			break;
		case Stat4G_4GPowerReset:			
			tls_gpio_write(POWER_4G_CTRL_PIN,0);
			vTaskDelay(25);  
			tls_gpio_write(POWER_4G_CTRL_PIN,1);
			stat4G = Stat4G_WaitReady;
			break;
        default:
            break;
        }	
    }

3. wifi和4G并存策略

一开机,同时启动wifi连接(没有配过网就进入配网)和4G连接;如果wifi连上了,就切断4G电源;wifi断线后重连一次失败(观察过 W801从wifi断线到重连失败的时间约22秒),启动4G连接。

3.1wifi断网后,启动4G模块,如果W801的MQTT已经启动,则要关闭W801的MQTT心跳:

在下图的代码块1中,发送4G重启消息,关闭W801的MQTT心跳。
在这里插入图片描述

在状态机中查询到重启消息,4G模块通电并启动连接:

代码块1:

    case NETIF_WIFI_JOIN_FAILED:
		//	已经连上wifi后,又断网,重连1次都失败后,启动4G
		if(internetStat_WifiConnect == g_internetStat){
				g_internetStat = internetStat_TryWifi;				
				if(demo_uart3 != NULL)
					tls_os_queue_send(demo_uart3->demo_uart_q, (void *) DEMO_MSG_4G_RESTART, 0);
					
				if(flagMqttIsStart ){   
					tls_os_queue_send(mqtt_demo_task_queue, (void *)MQTT_DEMO_CMD_STOP_TIMER, 0);	
				}
			printf("WIFI_JOIN_FAILED,start 4G\n");
		}else{
			printf("NETIF_WIFI_JOIN_FAILED,%d\n",failTimes);
		}
        break;

在《wm_mqtt_demo.c》的函数 mqtt_demo_task 增加 MQTT_DEMO_CMD_STOP_TIMER 的消息处理:

static void mqtt_demo_task(void *p)
{
    int ret;
    void *msg;
    struct tls_ethif *ether_if = tls_netif_get_ethif();

    if (ether_if->status)    {
        wm_printf("sta ip: %v\n", ether_if->ip_addr.addr);
        tls_os_queue_send(mqtt_demo_task_queue, (void *)MQTT_DEMO_CMD_START, 0);
    }

    for ( ; ; )
    {
        ret = tls_os_queue_receive(mqtt_demo_task_queue, (void **)&msg, 0, 0);
        if (!ret)
        {
            switch((u32)msg)
            {
            case MQTT_DEMO_CMD_START:
                do  {
                    ret = mqtt_demo_init();
                    if (ret)
                        break;
                    tls_os_queue_send(mqtt_demo_task_queue, (void *)MQTT_DEMO_CMD_LOOP, 0);
                }
                while (0);
                break;
            case MQTT_DEMO_CMD_HEART:
                wm_printf("send heart ping\r\n");
                mqtt_ping(&mqtt_demo_mqtt_broker);
                break;
            case MQTT_DEMO_CMD_LOOP:
                mqtt_demo_loop();
                break;
			case MQTT_DEMO_CMD_STOP_TIMER:
                wm_printf("delete heart timer\r\n");
				tls_os_timer_stop(mqtt_demo_heartbeat_timer);
            default:
                break;
            }
        }
    }
}

3.2 air724正常工作中,wifi连上网,需要关闭4G模块的电源,并使4G模块任务进入空转

上图的代码块2就是wifi连上了网络产生的事件,此时要发送4G模块进入空转的消息 DEMO_MSG_4G_ENTER_IDLE 。并启动W801的MQTT任务(如果已经启动过,只需要重连MQTT)
代码块2:

	//	此时要切断4G电源,告诉4G任务进入等待
		if((internetStat_4GConnect == g_internetStat)
			||(internetStat_Try4G == g_internetStat)
			||(internetStat_TryWifi == g_internetStat)
			||(internetStat_WifiOneshot == g_internetStat)){
			if(demo_uart3 != NULL)
				tls_os_queue_send(demo_uart3->demo_uart_q, (void *) DEMO_MSG_4G_ENTER_IDLE, 0);
			printf("JOIN_SUCCESS when 4G connect,4G off\n");
		//	启动 wifi MQTT
		if(flagMqttIsStart == 0){
			mqtt_demo();
			flagMqttIsStart = 1;
		}else{
			tls_os_queue_send(mqtt_demo_task_queue, (void *)MQTT_DEMO_CMD_START, 0);
			printf("wifi mqtt retart\n");
		}
		g_internetStat = internetStat_WifiConnect;

4G模块的任务查询到 DEMO_MSG_4G_ENTER_IDLE 消息后,关闭4G电源,进入空转:
在这里插入图片描述

4.切断air724电源后,串口引脚会有漏电流,使得4G模块的电源灯微亮

针对这种情况,切断4G模块电源后,要把串口引脚稍作处理,可以有2种方法:

1.引脚配置为输出模式,然后输出低电平;
2.配置为输出,浮空。

不知道那种好,我使用了第二种。上图的程序修改为:

		if(xQueueReceive(demo_uart3->demo_uart_q,&(cmdmsg),0)){
			//	消息1.  4G模块处于断电状态,如果 wifi断开了,重连一次失败,会发送重启4G的消息
			if((cmdmsg == DEMO_MSG_4G_RESTART)){	
				wm_uart3_tx_config(WM_IO_PA_05);
				wm_uart3_rx_config(WM_IO_PA_06);
				tls_gpio_write(POWER_4G_CTRL_PIN,1);
				stat4G = Stat4G_WaitReady;
			//	消息2.	本来4G模块处于正常通信中 ,wifi连上网络了,优先使用wifi,要断开4G电源,任务进入空闲等待
			}else if(cmdmsg == DEMO_MSG_4G_ENTER_IDLE){
				tls_io_cfg_set(WM_IO_PA_05, WM_IO_OPT5_GPIO);	//	关闭电源时,把IO口设置为输入高阻				
				tls_gpio_cfg(WM_IO_PA_05, WM_GPIO_DIR_INPUT, WM_GPIO_ATTR_FLOATING);
				tls_io_cfg_set(WM_IO_PA_06, WM_IO_OPT5_GPIO);	//	关闭电源时,把IO口设置为输入高阻				
				tls_gpio_cfg(WM_IO_PA_06, WM_GPIO_DIR_INPUT, WM_GPIO_ATTR_FLOATING);
				tls_gpio_write(POWER_4G_CTRL_PIN,0);
				stat4G = Stat4G_4GIdle;
			}
		}
;