Arduino/Microduino与OneNet平台及web服务器端的交互

                                                                一、上传

        近期在做硬件编程方面的小学期实验课,采用的硬件是Microduino模块,编程语言风格和C差不多。这种硬件编程的优点是它的语言的灵活性、简单性,因为它把底层都封装了,但是正因为这种过度的封装,导致引入不同的库文件后就会有许多代码大幅度的改动,并且代码不够轻巧,我因为这些缺点在小学期的硬件编程上也算是吃了不少苦头。。。

       当然对硬件编程吐槽归吐槽,还是要大赞可编程硬件,毕竟改改代码就能实现复杂的功能,同时省去了很多对于底层的理解,利于快速的创新开发!由于我在小学期期间内主要研究了Mcookie的WiFi模块的编程,所以对于WiFi模块和服务器端的交互这种较为复杂的功能实现也有些个人心得:

       首先讲解下与移动方OneNet物联网平台的交互,在Microduino中已经有具体相关的示例,在示例代码中WiFi模块连上网络这些都是比较简单的,但其中上传数据这块较为复杂,不过比着葫芦画个瓢也能大致实现向OneNet发送自己想要发送的数据。现附上我的一段实现向OneNet上传数据的代码:   

#include "ESP8266.h"

//CoreUSB UART Port: [Serial1] [D0,D1]  用于实现wifi串口通信

#if defined(__AVR_ATmega32U4__) //ATmega32U4---coreusb版本号

#define EspSerial Serial1

#define UARTSPEED  115200

#endif

 

#define SSID       "CCMC" //无线网账号

/*Service Set Identifier的缩写,意思是:服务集标识。SSID技术可以将一个无线局域网分为几个需要不同身份验证的子网络,每一个子网络都需要独立的身份验证,只有通过身份验证的用户才可以进入相应的子网络,防止未被授权的用户进入本网络。*/

#define PASSWORD   "qwertyuiop" //无线网密码

#define DEVICEID    "*******"//设备ID

#define HOST_NAME  "api.heclouds.com" //网址?

#define HOST_PORT   (80)//端口号

String  apiKey = "**********";//产品API密钥

String jsonToSend;

String postString;

ESP8266 wifi(&EspSerial);

 

void setup()

{

  Serial.begin(115200);//设置波特率为115200

  while (!Serial);

ERR:

  Serial.print("setupbegin\r\n");    

  wifi.setUart(UARTSPEED,DEFAULT_PATTERN);

 EspSerial.begin(UARTSPEED);//设值波特率

  delay(100);

 

  if (wifi.joinAP(SSID,PASSWORD))//wifi验证账号密码

  {

Serial.print(F("Join APsuccess\r\n"));

  }

  else

  {

goto ERR;

  }

 Serial.print(F("setup end\r\n"));

}

 

void loop()

{

  updateData();

  while(true);

}

 

void updateData() //上传数据函数

{

  if(wifi.createTCP(HOST_NAME, HOST_PORT)) //创建tcp连接

  {

   Serial.print(F("create tcp ok\r\n"));

  }

  else

  {

   Serial.print(F("create tcp err\r\n"));

  }

 

  jsonToSend="{\"a\":10000}"; //  \"转义符--> "

 

  postString ="POST/devices/";      //根据数据类型修改格式

  postString +=DEVICEID;  //设备ID

  postString +="/datapoints?type=3 HTTP/1.1";

  postString +="\r\n";

  postString +="api-key:";

  postString += apiKey; //产品的API key

  postString +="\r\n";

  postString +="Host:api.heclouds.com\r\n";

  postString +="Connection:close\r\n";

  postString +="Content-Length:";

  postString +=jsonToSend.length();

  postString +="\r\n";

  postString +="\r\n";

  postString +=jsonToSend;

  postString +="\r\n";

  postString +="\r\n";

  postString +="\r\n";

  //Content-Length:23 后的换行符号一定要。{"shidu":22,"wendu":22}后的换行符可以不要,但尽量使用换行符,这是个习惯问题同时报头结尾还要有两个换行符

  const char *postArray =postString.c_str();    //用指针指向数据

  /*json 要求输入的是 char *类型。而如果你的是 string类型,那么需要通过

    toCharArray()的函数进行转换,而不能使用c_str(),因为 c_str()返回的是 const 类型*/

  wifi.send((constuint8_t*)postArray, strlen(postArray)); //发送数据

  Serial.println(F("Send Success!"));

  postArray = NULL;

 Serial.println(freeRam());

}

 

int freeRam() { //用于查询剩余的内存,使用wifi模块一定要保证剩余内存在600B以上

  extern int __heap_start,*__brkval;

  int v;

  return (int) &v -(__brkval == 0 ? (int) &__heap_start :(int) __brkval);

}

在这里需要注明的是由于microduino引入的库文件可能不同,所以我的这一段代码不一定肯定可以跑通,我把它贴出,是为了分析向OneNet端发送数据的HTTP报头。当然,这个HTTP报头如果是自己分析,肯定不会容易,不过这段报头我也是借鉴microduino中关于OneNet使用的示例进行修改的。在此附上关于OneNet台的HTTP协议的汇总博客:http://open.iot.10086.cn/bbs/forum.php?mod=viewthread&tid=536&extra=page%3D2%26filter%3Dreply%26orderby%3Dreplies

在我贴出的代码段中,有:

if(wifi.createTCP(HOST_NAME, HOST_PORT)) //创建tcp连接

  {

   Serial.print(F("createtcp ok\r\n"));

  }

可以看出WiFi模块首先和OneNET服务器端建立tcp连接,有关tcp协议的细节,可以自己找资料,或者看看《计算机网络》,里面讲解的很详细。在这里,建立tcp连接是之后WiFi模块与服务器之后交互的前提,tcp连接建立后,WIFI模块就可以经它的套接字向服务器端发送HTTP请求报文了。在我看来,WiFi模块的功能就像是浏览器/服务器模式中的浏览器的作用,只不过WiFi模块不像浏览器那般会对接收发送的HTTP报文进行封装,呈现出的是非常易懂的形式,它按照HTTP协议收发数据时都是纯粹的HTTP报文形式。在上面的代码段中,其HTTP请求报文实际上就是如下的格式:

POST/devices/*****/datapoints?type=3HTTP/1.1

api-key:****************

Host:api.heclouds.com

Connection:close 

Content-Length:23

 

{“a”:10000}

 

注意这段报头的格式,其实都是OneNet的官方文档中的API封装好的,我们只需要按照这种形式调用即可。需要说明的是,在此处Http 1.1为长连接,该连接可以在一定时间内保持tcp连接打开,此举在某些情况下可以减少开销,但此处我出于测试,所以在HTTP请求报文末尾追加Connection:close,也就是表示WiFi模块和服务器端就只有一次报文的交互,当WiFi模块接收到服务器发回的HTTP响应报文后,此次tcp连接即断开。(之前调试程序时有tcp连接释放的代码段,但每次串口打印出的都是释放失败,后来仔细想想才发现是这块理解错误!)

       到此,关于WiFi向OneNet平台上传数据部分已经解释完了,理解不深刻,有些分析不得当的地方也希望大家包容~在下一篇会讨论Microduino模块从OneNet平台接收数据。

                                                                            二、接收数据

       关于从OneNet端接收数据其中的工作机制其实和向OneNET上传数据的原理是一样的,所以对于工作机制的解说,我可能会很粗糙。先附上我写的一段从OneNET平台接收数据的代码:

#include "ESP8266.h"

//CoreUSB UART Port: [Serial1] [D0,D1]  用于实现wifi串口通信

#if defined(__AVR_ATmega32U4__)//ATmega32U4---coreusb版本号

#define EspSerial Serial1

#define UARTSPEED  115200

#endif

 

#define SSID       "CCMC" //无线网账号

/*Service Set Identifier的缩写,意思是:服务集标识。SSID技术可以将一个无线局域网分为几个需要不同身份验证的子网络,每一个子网络都需要独立的身份验证,只有通过身份验证的用户才可以进入相应的子网络,防止未被授权的用户进入本网络。*/

#define PASSWORD    "qwertyuiop" //无线网密码

#define DEVICEID    "*****"//设备ID

#define HOST_NAME   "api.heclouds.com" //网址?

#define HOST_PORT   (80) //端口号

String  apiKey = "***********"; //产品API密钥

char buf[10]; 

String jsonToSend;

String postString;

ESP8266wifi(&EspSerial);

 

void setup()

{

  Serial.begin(115200);//设置波特率为115200

  while (!Serial);

ERR:

  Serial.print(F("setup begin\r\n"));

  EspSerial.begin(UARTSPEED); //初始化

  delay(100);

  if (wifi.joinAP(SSID, PASSWORD))//wifi验证账号密码

  {

    Serial.print(F("Join APsuccess\r\n"));

  }

  else

  {

    goto ERR;

   }

  Serial.print(F("setup end\r\n"));

}

 

void loop()

{

   uint8_t buffer[560] = {0};

 

  if (wifi.createTCP(HOST_NAME, HOST_PORT)) //创建tcp连接

  {

    Serial.print(F("create tcpok\r\n"));

  }

  else

  {

    Serial.print(F("create tcperr\r\n"));

  }

 

  postString = "GET/devices/";      //根据数据类型修改格式

  postString += DEVICEID;  //设备ID

  postString +="/datapoints?datastream_id=a&limit=5 "; //数据流选择为a,且限制获取的数据数目为 5

  postString += "HTTP/1.1";

  postString += "\r\n";

  postString += "api-key:";

  postString += apiKey; //产品的API key

  postString += "\r\n";

  postString +="Host:api.heclouds.com\r\n";

  postString +="Connection:close\r\n\r\n";

  //此处connection:close在调试程序时,发现是断开了长连接,所以此后如果还有发送或接受数据,就需要再建立tcp长连接

  const char *postArray =postString.c_str();    //用指针指向数据

  /*json 要求输入的是char *类型。而如果你的是 string类型,那么需要通过toCharArray()的函数进行转换,而不能使用 c_str(),因为 c_str()返回的是 const类型*/

  wifi.send((const uint8_t*)postArray,strlen(postArray)); //发送数据

  Serial.println(postArray);

  Serial.println(F("Send Success!"));

  postArray = NULL;

  uint32_t len = wifi.recv(buffer,sizeof(buffer), 10000);//接受从WiFi回复的信息至数组 buffer中,注:10000--timeout,为设置的响应时间!并返回信息的长度

 

  if (len > 0)

  {

    Serial.print("Received:[");

    for (uint32_t i = 0; i < len; i++) {

      Serial.print((char)buffer[i]);

    }

    Serial.print("]\r\n");

  }

  Serial.println(freeRam());

  while(true);

}

 

int freeRam() { //用于查询剩余的内存,使用wifi模块一定要保证剩余内存在600B以上

  extern int __heap_start, *__brkval;

  int v;

  return (int) &v - (__brkval == 0 ? (int)&__heap_start :(int) __brkval);

}

关于此段代码,可以发现和上一篇上传数据的代码段大部分一致,只不过修改了HTTP请求报文,同时WiFi模块接收了服务器发回的响应报文,获取了自己想要的数据。关于此段接收数据的HTTP报头,其格式如下:

GET/devices/***/datapoints?datastream_id=a&limit=5HTTP/1.1

api-key:*************

Host:api.heclouds.com

Connection:close

 

 

在此,关于从OneNET平台接收数据这一方面已经讲述完毕了,因为理论在上一篇已经讲解很详细了~ 在下一篇,会讲解在WiFi模块硬件编程方面的代码优化小技巧。

 

                                                                           三、内存优化

      在此前详细得解说了WiFi大致的工作原理,同时也附上了与OneNET交互的上传及下发的代码,不过那些都是修改后的代码,接下来我会讲解我的代码是如何一步一步优化的:

a.  用串口打印数据时,可以使用类似于Serial.print(F("setup begin\r\n"));这样的形式打印数据,其实就是在打印内容前加个F()形式的函数,具体工作原理我不是很清楚,只知道这样打印数据时会有效地优化内存。。。

 

b.  在原始案例中倘若调用WiFi模块,setup 内初始化过程中会调用了相当多的函数,我在看了底层函数后,发现其实很多代码是起着辅助调试作用,如果内存够的话这么玩是可以的,毕竟严谨,但是内存都不够了,也就不端着架子了。于是乎,我的WiFi模块的初始化代码是以下这么一段:

void setup()

{

  Serial.begin(115200);//设置波特率为115200

  while (!Serial);

ERR:

  Serial.print(F("setup begin\r\n"));

 

  EspSerial.begin(UARTSPEED); //初始化

  delay(100);

 

  if (wifi.joinAP(SSID, PASSWORD))//wifi验证账号密码

  {

    Serial.print(F("Join APsuccess\r\n"));

  }

  else

  {

    goto ERR;

    //Serial.print(F("Join APfailure\r\n"));

  }

  Serial.print(F("setup end\r\n"));

}

可以看到,这里的代码量有大幅度的减少,函数也砍了大半,不过缺点是初始化的过程中可能就看不到具体的调试信息了。同时我对ERR 以及 goto ERR部分有进行标红,这可以用于如果第一次初始化WiFi因为网络不好没有成功连接,可重复尝试连接

 

c. 在 loop中,案例里有提到这段代码:uint8_tbuffer[1024] = {0};这里初始化了一个很耗内存的数组用于接收从服务器端返还的数据,当然如果是大佬的话,应该会注意修改这一部分。不过我当时太蠢,没敢改这个部分。这里这个数组的大小由返还的http响应报文数据大小决定,原则上,只要HTTP响应报文数据没有接漏,都说明数组的大小是没问题的。

 

d.  postString +="Connection:close\r\n\r\n”;这段代码案例中貌似就是这么写的,不过需要提及的是,此处connection:close 我感觉是优化到了内存。因为http1.1协议应该是长连接,所以默认是connection:keep-alive的,之所以把connection改为关闭,可能是这次loop中不需要再和服务器建立tcp连接,开着也没必要了。当然如果你想在一个loop中多次和服务器接发数据进行交互,可以改为connection:keep-alive,在最后一个接发中再改为close 就ok了,这样的话还有一个好处就是,省去了释放tcp连接的那块函数,因为你在发送后已经断开了连接。

 

e.  最后附上一个用于检测剩余内存的函数,它要比编译运行时看到的内存剩余要精准许多。在原则上coreUSB要求是剩余500-600B之上,才能避免WiFi模块使用时的内存问题

(函数如下:)

int freeRam() { //用于查询剩余的内存,使用wifi模块应该保证剩余内存在600B以上

 extern int __heap_start, *__brkval;

 int v;

 return (int) &v - (__brkval == 0 ? (int) &__heap_start :(int)__brkval);

}

在主程序中只需要Serial.println(freeRam());这段代码,就可以在串口打印出所余内存,其中单位为字节。


             四、Arduino/Microduino与本地web服务器的交互

       在此前的硬件编程分析之后,想必这个标题更吸引人,因为它可以和你写的网页端进行交互,并且可以实时刷新从网页端返还的数据。这意味着,通过硬件可以实现物联网中很多功能,当然在此处,我不打算细说应用场景,应用案例。我只打算简单地介绍下如何和自己搭的服务器进行交互,其实还是和同OneNET交互一样的“套路”,都是在HTTP报文方面下功夫,不过在和自己的服务器进行交互时,HTTP请求报文可不是像OneNet官方文档那般,详细介绍了它的Api调用方式以及HTTP报文的格式。在此处,HTTP报文出于稳重,它的格式需要抓包看一下(虽然大多数HTTP报文格式都差不多,毕竟都是遵守HTTP协议。。),在此处,我推荐fiddle 这款抓包软件,下面是它打开的界面。

Arduino/Microduino与OneNet平台及web服务器端的交互_第1张图片

       先附上我写的和自己的服务器进行交互的代码:

#include "ESP8266.h"

//CoreUSB UART Port: [Serial1] [D0,D1]  wifi串口通信

#if defined(__AVR_ATmega32U4__)//ATmega32U4---coreusb版本号

#define EspSerial Serial1

#define UARTSPEED  115200

#endif

 

#define SSID       "CCMC" //无线网账号

#define PASSWORD   "qwertyuiop" //无线网密码

#define HOST_NAME  "192.168.43.215" //ip地址,自己搭建的服务器的IP地址

#define HOST_PORT   (80)//端口号

 

ESP8266 wifi(&EspSerial);

 

void setup()

{

  Serial.begin(115200);//设置波特率为115200

  while (!Serial);

ERR:

 Serial.print(F("setup begin\r\n"));

 

 EspSerial.begin(UARTSPEED); //初始化

  delay(100);

 

  if (wifi.joinAP(SSID,PASSWORD))//wifi验证账号密码

  {

   Serial.print(F("Join AP success\r\n"));

  }

  else

  {

    goto ERR;

   //Serial.print(F("JoinAP failure\r\n"));

  }

 Serial.print(F("setup end\r\n"));

}

 

void loop(void) {

 

 if(wifi.createTCP(HOST_NAME, HOST_PORT)) //创建tcp连接

  {

   Serial.print(F("create tcp ok\r\n"));

  }

  else

  {

   Serial.print(F("create tcp err\r\n"));

  }

  uint8_t buffer[256+64] ={0};

   char *hello ="GEThttp://192.168.43.215/php/Try/get.php?id=1HTTP/1.1\r\nHost:192.168.43.215\r\nConnection: close\r\n\r\n";

    //当使用Keep-Alive模式(又称持久连接、连接重用)时,Keep-Alive功能使客户端到服务器端的连接持续有效,当出现对服务器的后继请求时,Keep-Alive功能避免了建立或者重新建立连接。

//此处此处由于只获取一次数据,所以无需关心后继请求,故而为Connection:close   

wifi.send((constuint8_t*)hello,strlen(hello));//将信息头发送出去:内容,长度

//boolsend(constuint8_t *buffer, uint32_t len);

    hello=NULL; //释放内存

    uint32_t len =wifi.recv(buffer, sizeof(buffer), 10000);

    uint8_t a=Print(buffer,len);//对于接受的数据进行处理

  }

 

uint8_t Print(uint8_t * buffer, uint32_t len)

{

  if (len > 0)

  {

    uint32_t i = 0; //定义整型变量 i

while ((i < len -1) && (!((buffer[i] == 13) && (buffer[i+ 1] == 10) &&(buffer[i + 2] == 13) &&

 (buffer[i + 3] == 10)))) //用于判别信息头同时只获取信息头后的值

    {

      i++;

    }

    //由于此处我只打算获取权限,1代表可以,0代表不可以,所以只获取一个字符即可 

    return buffer[i + 4];

  }

}

在这里,可以发现我的HTTP报文是:

GEThttp://192.168.43.215/php/Try/get.php?id=1 HTTP/1.1\r\n

Host:192.168.43.215\r\n

Connection:close\r\n\r\n


 

 它的报文格式和之前与OneNet平台的交互的格式其实相仿,下面我来详细解说这段HTTP请求报文的获取方式:

首先讲解下这个地址的含义:http://192.168.43.215/php/Try/get.php?id=1中http://192.168.43.215/php/Try/get.php指代一个本地的我写的网站,192.168.43.215为本地web服务器的ip地址,学过web的同学应该都有了解Localhost,在这里192.168.43.215其实就是该域名对应的本地Ip地址,只不过本地的服务器IP地址只能在局域网内访问,不能够在公网访问。关于IP地址的获取,可以在dos命令下输入ipconfig, 在下面截图中所示的IPV4地址即位本地服务器的ip地址。

Arduino/Microduino与OneNet平台及web服务器端的交互_第2张图片

而后面的?id=1其实为以get形式返还给服务器的数据,在这里附上对应的get.php的源代码,是一个非常简单的脚本文件:

$id =$_GET['id']; //获取GET形式返还的id数值

if($id=1){ echo 1; }

else{ echo 0; }

?>

该脚本的功能就是获取get形式返还的id数据值。

其次关于在WiFi模块硬件编程中如何写此与服务器交互的HTTP请求报文,其实有一个简单的方法,就是在自己浏览器中输入刚刚提到的网址形式:http://192.168.43.215/php/Try/get.php?id=1,然后在fiddle中分析抓到的包:

Arduino/Microduino与OneNet平台及web服务器端的交互_第3张图片

可以看到,在WiFi模块中写的HTTP请求报文只是这个完整请求报文的一部分,至于为什么只选取其中的一部分,是源于对Onenet示例中源码的分析后大胆的删减。不过我之前也有测试过发送完整的HTTP请求报文是绝对可行的,目前可以认为这两种格式的HTTP请求报文格式都是可行的。

 

至于服务器端返还的响应报文在WiFi模块中便不似请求报文那种是可变的,它几乎是傻瓜式的返回,没有任何处理,就和从浏览器抓的响应报文是一样的格式:

Arduino/Microduino与OneNet平台及web服务器端的交互_第4张图片

可是对于硬件来说,返回的响应报文头部都是没有意义的,需要删减,于是我写了一个函数用于剥去返回的响应报文头部:

uint8_t Print(uint8_t * buffer, uint32_t len)

{

  if (len > 0)

  {

    uint32_t i = 0; //定义整型变量 i

while ((i < len -1) && (!((buffer[i] == 13) && (buffer[i+ 1] == 10) &&(buffer[i + 2] == 13) &&

(buffer[i + 3] == 10)))) //用于判别信息头同时只获取信息头后的值

    {

      i++;

    }

    //由于此处我只打算获取权限,1代表可以,0代表不可以,所以只获取一个字符即可 

    return buffer[i + 4];

  }

}

当然需要吐槽这个函数也是傻瓜式遍历的,对于程序的优化肯定还是有优化的空间的,不过我懒并且能力有限所以这一块并没有深入去做。。。

        至此整个程序大致流程都已经分析完了,关于Microduino/Arduino WiFi模块编程的系列也大致讲述完了,希望大家看完后能有些许帮助~

 


                                              

 

 

 


你可能感兴趣的:(Arduino/Microduino与OneNet平台及web服务器端的交互)