ESP8266 Non-OS SDK開發探坑之五-簡單的HTTP配置服務器

ESP8266 Non-OS SDK開發探坑之五-簡單的HTTP配置服務器

Starting with ESP8266 — Light a LED

Starting with ESP8266 (2)–Touch to control relay status-circuit design & electronic components selection

Starting with ESP8266(3) — Touch to control Relay-Programming & PCB design

Starting with ESP8266(4)–User parameters securely save & load on flash

Starting with ESP8266(5)–Simple HTTP configure server

經過一段時間的折騰,總算把esp8266搞入門了,開始正式開發了

esp8266的模塊要聯網進行控制,首先肯定是得配置wifi信息,

1、原始的方法是寫到代碼裏,定義個宏,定義個變量。。。

2、串口通信方式、AT指令之類的。。。

3、初始化softAP模式,然後提供個tcpserver,由手機app實現TCP傳輸配置,這個目前產品上用的比較多,在需要斷開家裏wifi連上設備wifi配置完再重連家裏wifi的都屬這一類

4、初始化SoftAP模式,然後提供webserver,由手機通過瀏覽器訪問進行配置,這個是本文實現的方法

5、airkiss,這個比較便捷,產品上也用的比較多,不需要斷開任何wifi,直接進行一段時間的掃描、廣播、配置。

其中1、2方法顯然只適合diy人士,做產品是不行的,3、4、5方法各有優劣,3缺點是需要安裝app,優點是交互性好,4缺點是界面不友好(UI都需要esp8266提供),配置過程略繁瑣,但是隻要一部手機就能配置,不用下載app,5的原理還蠻有意思,巧妙利用了無線傳輸物理層某些字段明文傳輸,並且包數據長length字段可由應用層控制的特點進行信息傳輸,當然安全性不太高,理論上只要能接收到信號都能解析,畢竟是明文傳輸,但是比較方便,有新設備加入,只需要一臺設備能發送對應的信息即可完成配置,幾乎不需要人工介入。

前幾篇充滿了對esp8266的吐槽,不過隨着深入瞭解,對esp8266更多了些喜歡,麻雀雖小,五臟俱全,esp8266具備了實現上述5種方法的軟硬件基礎,而且官方也給了接口和例程,所以難度就小了很多。

爲了探坑,我決定造輪子。。。。。。寫個簡單的HTTP Server完成初始化配置, 同時完成tcp客戶端定時上傳數據,和tcp服務端遠程控制的功能。

這篇先講WebServer,及其配套的方法,下一篇再說下TCP Server和Client

簡單說說可行性,HTTP協議比較簡單,基於TCP協議,只要能開啓TCP服務即可實現Web服務,顯然ESP8266的能力完全可以cover,那便只要開啓TCP服務,監聽某端口,能監聽80便是最好,然後在接收回調裏完成請求解析、頭部信息解析,數據提取,以及發送響應結果。這裏我只實現最基本的HTTP協議內容,完成基本網頁通信,也就是解析了頭部的GET、POST請求,解析Conten-Length字段,實現響應重定向Location,並自定義了很基礎的幾個html靜態頁面。其實內存夠大,完全可以把頁面弄的很華麗,就是有點沒必要了。

先初始化web服務

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

void ICACHE_FLASH_ATTR

WebServInit(uint32 port){

espConnServ.type = ESPCONN_TCP;

espConnServ.state = ESPCONN_NONE;

espConnServ.proto.tcp = &espTcp;

espConnServ.proto.tcp->local_port = port;

espconn_regist_connectcb(&espConnServ, WebServListenCB);

 

#ifdef WEB_SERV_SSL_ENABLE

    espconn_secure_set_default_certificate(default_certificate, default_certificate_len);

    espconn_secure_set_default_private_key(default_private_key, default_private_key_len);

    espconn_secure_accept(&espConnServ);

#else

espconn_accept(&espConnServ);

#endif

espconn_regist_time(&espConnServ,600, 1); // client connectted timeout, unit: second, 0~7200

WebServOn = true;

}

當有客戶端連接執行回調:

 

1

2

3

4

5

6

7

8

9

10

LOCAL void ICACHE_FLASH_ATTR

WebServListenCB(void *arg){

    struct espconn *pEspConn = arg;

    os_printf("server: "IPSTR":%d connected\n", IP2STR(pEspConn->proto.tcp->remote_ip),pEspConn->proto.tcp->remote_port);

 

    espconn_regist_recvcb(pEspConn, WebServRecvCB);

    espconn_regist_sentcb(pEspConn, WebServSentCB);

    espconn_regist_reconcb(pEspConn, WebServReconCB);

    espconn_regist_disconcb(pEspConn, WebServDisconCB);

}

其中定義了幾個回調。

重點是接收回調:

接收回調裏先是提取HTTP的方法(GET、POST)和請求的URL地址及參數,提取後放在傳入指針 URLParam裏。

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

LOCAL bool ICACHE_FLASH_ATTR

ParseURL(char *pRecv, unsigned short length, URLParam *pUrlParam){

if(pRecv==NULL){ return false;}

char *pTemp = NULL;

char *pTemp2 = NULL;

 

if(os_strncmp(pRecv,"GET ",4)==0){

pUrlParam->eMethod = GET;

}else if(os_strncmp(pRecv,"POST ",5)==0){

pUrlParam->eMethod = POST;

}else{ return false;}

 

pTemp = (char*)os_strstr(pRecv,"/");

if(pTemp==NULL){

return false;

}else{

char *pEnd = (char*)os_strstr(pTemp," HTTP");

if(pEnd==NULL || pEnd<pTemp){ return false; }

 

pTemp2 = (char*)os_strstr(pTemp,"?");

if(pTemp2!=NULL && pEnd>pTemp2){

os_memcpy(pUrlParam->szPath, pTemp+1, (pTemp2-pTemp-1 )<MAX_PATH?(pTemp2-pTemp-1 ):MAX_PATH);

os_memcpy(pUrlParam->szParam,pTemp2+1,(pEnd-pTemp2-1)<MAX_PARAM?(pEnd-pTemp2-1):MAX_PARAM);

}else{

if(pEnd-pTemp-1>0){

os_memcpy(pUrlParam->szPath, pTemp+1, (pEnd-pTemp-1)<MAX_PATH?(pEnd-pTemp-1):MAX_PATH);

}else{

os_memset(pUrlParam->szPath,0,sizeof(pUrlParam->szPath));

os_memset(pUrlParam->szParam,0,sizeof(pUrlParam->szParam));

}

}

}

return true;

}

如果是POST方法,則提取post數據,就是判斷HTTP頭部結尾標識\r\n\r\n,並比對Content-Length字段裏的長度信息,

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

LOCAL bool ICACHE_FLASH_ATTR

GetPostData(char *pRecv, unsigned short length, char **pPostData){

if(pRecv==NULL){ return false;}

*pPostData = (char*)os_strstr(pRecv,"\r\n\r\n");

if(*pPostData==NULL){

return false;

}

*pPostData += 4;

 

char* pContLen = (char *)os_strstr(pRecv,"Content-Length: ");

if(pContLen == NULL){ return false;}

pContLen+=16;

 

char *pLenEnd = (char *)os_strstr(pContLen,"\r\n");

if(pLenEnd == NULL || pLenEnd-pContLen>9){ return false;}

 

char lenBuf[11]={0};

os_memcpy(lenBuf, pContLen, pLenEnd-pContLen);

uint32 contLen = atoi(lenBuf);

if(length-(*pPostData-pRecv) != contLen){ return false;}

return true;

}

再提取post參數並組裝成json對象,這樣解析方便並且將來可以和TCP server的參數解析方法統一,目前沒這麼做。其中用到了cjson庫,關於cjson庫的移植參考了博客:

https://blog.csdn.net/yannanxiu/article/details/52713746

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

//this function will modify the string context param pData pointed to

LOCAL bool ICACHE_FLASH_ATTR

ParsePostData(char *pData, unsigned short length, PostParam *pParam){

if(pData==NULL)return false;

if(pData[0]=='{'){

pParam->eEncode = JSON_ENCODE;

pParam->jsonData = cJSON_Parse(pData);

if(NULL == pParam->jsonData){

TRACE("parse json string from post data error:%s\r\n",pData);

return false;

}

}else{

pParam->eEncode = URL_ENCODE;

char *pPtr = pData;

pParam->jsonData = cJSON_CreateObject();

 

while(pPtr){

char *pSplit = NULL;

char *pKV = NULL;

 

pSplit = os_strstr(pPtr,"&");

pKV = os_strstr(pPtr,"=");

if(pKV){

if(pSplit!=NULL){

pSplit[0] = '\0';

}

pKV[0] = '\0';

cJSON_AddStringToObject(pParam->jsonData,pPtr,pKV+1);

}

if(pSplit==NULL){

break;

}

pPtr = pSplit +1;

}

}

return true;

}

完成上述解析後,即可對請求進行處理並相應,爲了節省內存空間,定義了很多static const char數組對象,這些對象一般存在flash上,用的時候才加載到內存。響應函數如下

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

LOCAL void ICACHE_FLASH_ATTR

WebServResponse(void *arg, HttpStatusCode statCode, const char *pData, const char *pRedirect)

{

    uint16 length = 0;

    char *pBuf = NULL;

    char headBuf[256];

    os_memset(headBuf, 0, 256);

    struct espconn * pEspConnClient = arg;

 

    char *pCodeDspt=NULL;

    switch(statCode){

    case SUCCESS:

     pCodeDspt = "OK";

     break;

    case REDIRECTION:

     pCodeDspt = "redirection";

     break;

    case BAD_REQUEST:

     pCodeDspt = "Bad Request";

     length = os_strlen(headBuf);

     break;

    case SERV_ERROR:

     pCodeDspt = "Server Internal Error";

     break;

    default:

     pCodeDspt = "Server Internal Error";

     break;

    }

os_sprintf(headBuf, "HTTP/1.1 %d %s\r\nServer: ESP8266\r\nContent-type: text/html;charset=utf-8\r\nPragma: no-cache\r\n",statCode,pCodeDspt);

if(pRedirect){

os_sprintf(headBuf+os_strlen(headBuf),"Location: http://192.168.4.1/%s\r\n\r\n",pRedirect);

}else{

os_sprintf(headBuf+os_strlen(headBuf),"\r\n");

}

length = os_strlen(headBuf);

 

if(statCode == SUCCESS){

os_sprintf(headBuf+os_strlen(headBuf)-2,"Content-Length: %d\r\n\r\n",pData ? os_strlen(pData) : 0);

length = os_strlen(headBuf);

if (pData) {

length = os_strlen(headBuf);

pBuf = (char *)os_zalloc(length + os_strlen(pData) + 1);

if(pBuf != NULL){

os_memcpy(pBuf, headBuf, length);

os_memcpy(pBuf + length, pData, os_strlen(pData));

length += os_strlen(pData);

}else{

statCode = SERV_ERROR;

//ignore the redirection because of the unexpected error

os_sprintf(headBuf, "HTTP/1.1 500 Server Internal Error\r\nContent-Length: 0\r\nServer: ESP8266\r\n\r\n");

length = os_strlen(headBuf);

}

}

}

 

    TRACE("head:%s",headBuf);

 

    if (pData && pBuf!=NULL) {

        TRACE("buf:%s",pBuf);

#ifdef WEB_SERV_SSL_ENABLE

        espconn_secure_sent(pEspConnClient, pBuf, length);

#else

        espconn_sent(pEspConnClient, pBuf, length);

#endif

    } else {

#ifdef WEB_SERV_SSL_ENABLE

        espconn_secure_sent(pEspConnClient, headBuf, length);

#else

        espconn_sent(pEspConnClient, headBuf, length);

#endif

    }

    if (pBuf) {

        os_free(pBuf);

    }

}

支持狀態碼成功(200 SUCCESS),重定向(301 REDIRECTION),錯誤的請求(400 BAD REQUEST),服務器錯誤(500 SERVER INTERNAL ERROR)等簡單的狀態。由於在接收回調函數中不便於進行一些操作,比如和wifi狀態相關的對象操作或者espconn對象的操作,所以開啓任務隊列進行處理:

 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

void ICACHE_FLASH_ATTR

WebServTask(os_event_t *e){

struct espconn *pEspConn;

struct station_config *pStatConf;

uint8 WifiMode;

switch(e->sig){

case WSIG_START_SERV:

WifiMode = wifi_get_opmode_default();

if(!(WifiMode & SOFTAP_MODE)){

WifiMode = WifiMode | SOFTAP_MODE;

wifi_set_opmode(WifiMode);

}

WebServInit(e->par);

break;

case WSIG_DISCONN:

pEspConn = (struct espconn*) e->par;

if(espconn_disconnect(pEspConn)!=0){  //error code is ESPCONN_ARG

TRACE("client disconnect failed, argument illegal\r\n");

}

break;

case WSIG_WIFI_CHANGE:

pStatConf = (struct station_config *)e->par;

bool ret = WifiStationConfig(pStatConf);

TRACE("wifi station connect return:%s",ret?"true":"false");

break;

case WSIG_REMOTE_SERVCHG:

wifi_set_opmode(STATION_MODE);

system_os_post(TCPCOMM_TASK_PRIO,TSIG_REMOTE_SERVCHG,0x00);

break;

default:

break;

}

}

手機連上ESP8266 AP後訪問html頁面:

 

插播下我重打的板:

AC-DC繼電器控制版

DC-DC繼電器控制版

 

實物還得等打板,好慢。

代碼見:  ESP8266_NONOS_SDK-2.2.1-WebServer

https://github.com/atp798/BlogStraka/

原博客:

http://www.straka.cn/blog/esp8266-5-http-configure-server/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章