一、上传
近期在做硬件编程方面的小学期实验课,采用的硬件是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 这款抓包软件,下面是它打开的界面。
先附上我写的和自己的服务器进行交互的代码:
#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地址。
而后面的?id=1,其实为以get形式返还给服务器的数据,在这里附上对应的get.php的源代码,是一个非常简单的脚本文件:
<?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中分析抓到的包:
可以看到,在WiFi模块中写的HTTP请求报文只是这个完整请求报文的一部分,至于为什么只选取其中的一部分,是源于对Onenet示例中源码的分析后大胆的删减。不过我之前也有测试过发送完整的HTTP请求报文是绝对可行的,目前可以认为这两种格式的HTTP请求报文格式都是可行的。
至于服务器端返还的响应报文在WiFi模块中便不似请求报文那种是可变的,它几乎是傻瓜式的返回,没有任何处理,就和从浏览器抓的响应报文是一样的格式:
可是对于硬件来说,返回的响应报文头部都是没有意义的,需要删减,于是我写了一个函数用于剥去返回的响应报文头部:
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模块编程的系列也大致讲述完了,希望大家看完后能有些许帮助~