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 這款抓包軟件,下面是它打開的界面。
?????? 先附上我寫的和自己的服務器進行交互的代碼:
#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模塊編程的系列也大致講述完了,希望大家看完后能有些許幫助~
?
??????????????????????????????????????????????
?
?
?
總結
以上是生活随笔為你收集整理的Arduino/Microduino与OneNet平台及web服务器端的交互的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 搭建WAMP 环境时,解决Windows
- 下一篇: PHP和OneNet平台交互