极客工坊

 找回密码
 注册

QQ登录

只需一步,快速开始

查看: 736|回复: 2

ESP8266 Non-OS SDK开发探坑之五-简单的HTTP配置服务器

[复制链接]
发表于 2018-9-9 23:48:11 | 显示全部楼层 |阅读模式
本帖最后由 straka 于 2018-9-10 00:04 编辑

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】

原博客格式更友好,欢迎来访:
http://www.straka.cn/blog/esp8266-5-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服务

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;
}
当有客户端连接执行回调:

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里。

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字段里的长度信息,

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

//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上,用的时候才加载到内存。响应函数如下
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对象的操作,所以开启任务队列进行处理:

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页面:

server-config.png wifi-config.png

插播下我重打的板:
PS: 话说有木有人愿意帮我平摊下打板费,买板和配套散件,也当做是鼓励我继续分享哈。。。不喜勿喷哈

AC-DC继电器控制版
微信截图_20180825164147.png


DC-DC继电器控制版

微信图片_20180825162826.png

实物还得等打板,好慢。

代码见:  ESP8266_NONOS_SDK-2.2.1-WebServer

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


原博客格式更友好,欢迎来访:
http://www.straka.cn/blog/esp8266-5-http-configure-server/
回复

使用道具 举报

发表于 2018-9-10 11:34:10 | 显示全部楼层
楼主最近发了多篇8266的文章,貌似用的都是原厂的SDK,
我一直都是用arduino开发环境的,不知道原厂开发环境的体验是怎么样的呢?
回复 支持 反对

使用道具 举报

 楼主| 发表于 2018-9-10 11:56:52 | 显示全部楼层
wing 发表于 2018-9-10 11:34
楼主最近发了多篇8266的文章,貌似用的都是原厂的SDK,
我一直都是用arduino开发环境的,不知道原厂开发环 ...

SDK开发用的eclipse,编程还算方便,主要是起初要耐心看很多说明文档后在开始开发,API手册编的总体不错但是有些地方有遗漏,所以把官方文档都过一遍大概知道都是讲啥的再开始,遇到问题也好解决
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

Archiver|联系我们|极客工坊 ( 浙ICP备09023225号 )

GMT+8, 2018-11-13 09:19 , Processed in 0.048430 second(s), 27 queries .

Powered by Discuz! X3.4 Licensed

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表