第一次在产品中使用iot功能,完全没有经验,可能会存在很多bug,请各位大侠不吝赐教,指点一二。
我这次使用的是合宙的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指令的例程。
模块插上中国移动SIM卡,通电后,通过SIM卡,注册网络后会从网络自动获取并激活一个PDP上下文。下面是串口观察到的打印信息:
AT指令必须以回车符(0x0D)结束,以回车+换行(0xA)结束也行。
多数指令关于双引号是可有可无的,例如设置订阅主题的指令:
可以有双引号括住订阅主题:
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";
/** 发送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;
}
状态机的状态码 :
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;
}
}
一开机,同时启动wifi连接(没有配过网就进入配网)和4G连接;如果wifi连上了,就切断4G电源;wifi断线后重连一次失败(观察过 W801从wifi断线到重连失败的时间约22秒),启动4G连接。
在下图的代码块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;
}
}
}
}
上图的代码块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电源,进入空转:
针对这种情况,切断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;
}
}