参考教程:https://www.bilibili.com/video/BV1L7411c7jw/?spm_id_from=333.1387.favlist.content.click
四、ESP8266作为网络服务器
1、建立基本网络服务器
(1)网络服务器有很多种类型,通常承担网络服务器工作的设备都是运算能力比较强大的电脑。ESP866-NodeMCU虽然只能实现一些基本的网络服务功能,不过这些基本的网络服务功能已经足够开发物联网项目了。
(2)如果ESP866-NodeMCU作为服务器,与电脑主机、手机都接入同一个Wi-Fi网络中,那么电脑或者手机想要访问ESP866-NodeMCU的网站首页,大致流程如下所示。

(3)ESP8266WebServer类是网络服务器的抽象,创建对象时,需将应用层服务对应的运输层端口号传入构造函数中,针对HTTP协议,其使用的运输层熟知端口号为80。ESP8266WebServer类的成员函数有如下几个(包括但不限于):
①begin:启动ESP8266的网络服务功能,该函数无参数。
②on:配置网络服务器如何处理用户端发来的不同的HTTP请求。
其有三个参数,第一个参数为用户端可能请求访问的网站目录(以字符串的形式表示,比如根目录"/"),第二个参数为请求行中的方法(可省略,省略则不判断),第三个参数为一个函数句柄,当用户端用指定方法请求访问第一个参数指定的目录时,服务器执行函数句柄指向的子函数,子函数中一般有响应HTTP请求并发出响应报文的语句
③onNotFound:配置网络服务器如何处理用户端发来的无法满足的HTTP请求。
其只有一个参数,为一个函数句柄,当用户端请求访问的目录不存在时,服务器执行函数句柄指向的子函数,子函数中一般有响应HTTP请求并发出"错误提示"响应报文的语句
④send:发送HTTP响应报文。
其有三个参数,第一个参数为状态码(比如404表示找不到页面),第二个参数为首部行的首部字段名(比如"text/plain"表示纯文本,"text/html"表示网页代码),第三个参数为首部字段名对应的值(比如纯文本对应的应为一段文本,用户收到后将其解读为纯文本信息),有时候没有需要特别添加的首部行,则后两个参数可省略
⑤handleClient:处理客户端的请求(这不是配置函数,而是功能函数,需要反复调用),该函数无参数。
(4)使用ESP8266实现最基本的网页服务功能,其程序流程如下图所示。

(5)将以下示例程序粘贴到代码区中,然后将其编译并上传NodeMCU开发板,开启电脑热点"Zevalin_Computer",ESP8266会建立起一个小型服务器,接下来打开浏览器,在地址栏中输入NodeMCU的IP地址并按下回车,可以看到"Hello from ESP8266"的文本信息。
cpp
#include <ESP8266WiFi.h> //ESP8266WiFi库
#include <ESP8266WiFiMulti.h> //ESP8266WiFiMulti库
#include <ESP8266WebServer.h> //ESP8266WebServer库
ESP8266WiFiMulti wifiMulti; //建立ESP8266WiFiMulti对象,对象名称是'wifiMulti'
ESP8266WebServer esp8266_server(80); //建立ESP8266WebServer对象,对象名称为esp8266_server
void handleRoot() { //处理网站根目录"/"的访问请求
esp8266_server.send(200, "text/plain", "Hello from ESP8266");
}
void handleNotFound(){ //处理404情况
esp8266_server.send(404, "text/plain", "404: Not found");
}
void setup(void){
Serial.begin(9600); //启动串口通讯
//配置Wi-Fi网络的所有可能选项
wifiMulti.addAP("Zevalin_Computer", "00114514");
wifiMulti.addAP("Zevalin_esp8266", "00114514");
int i = 0;
while (wifiMulti.run() != WL_CONNECTED) { //连接信号最强的Wi-Fi网络
delay(1000);
Serial.print(i++); Serial.print(' ');
}
Serial.println('\n');
Serial.print("Connected to "); Serial.println(WiFi.SSID());
Serial.print("IP address:\t"); Serial.println(WiFi.localIP());
//--------"启动网络服务功能"程序部分开始--------
esp8266_server.begin();
esp8266_server.on("/", handleRoot);
esp8266_server.onNotFound(handleNotFound);
//--------"启动网络服务功能"程序部分结束--------
Serial.println("HTTP esp8266_server started"); //告知用户ESP8266网络服务功能已经启动
}
void loop(void){
esp8266_server.handleClient(); //处理http服务器访问
}
2、通过网络服务实现开发板基本控制
(1)进一步介绍ESP8266WebServer类的成员函数:
①on函数除了能够配置收到用户端请求(GET方法)后给出相应的回应,还能配置收到用户端请求(POST请求)后做出相应的行为,比如"点灯"。
②sendHeader函数用于向HTTP响应报文中的响应头部添加一个首部行,它与send函数的区别是,调用sendHeader添加首部行后不会将响应报文立即发送,而调用send函数则会立即将响应报文立即发送,且send函数会自动构建状态行、自动附加必要的头部。

(2)使用ESP8266实现通过网络服务实现开发板基本的点灯控制,其程序流程如下图所示。

(3)将以下示例程序粘贴到代码区中,然后将其编译并上传NodeMCU开发板,开启电脑热点"Zevalin_Computer",ESP8266会建立起一个小型服务器,接下来打开浏览器,在地址栏中输入NodeMCU的IP地址并按下回车,可以看到一个标识文本为"Toggle LED"的按键,按下它后浏览器向开发板发送POST请求,开发板就会切换LED的状态,同时浏览器的页面将跳转回根目录。
cpp
#include <ESP8266WiFi.h> //ESP8266WiFi库
#include <ESP8266WiFiMulti.h> //ESP8266WiFiMulti库
#include <ESP8266WebServer.h> //ESP8266WebServer库
ESP8266WiFiMulti wifiMulti; //建立ESP8266WiFiMulti对象,对象名称是'wifiMulti'
ESP8266WebServer esp8266_server(80); //建立ESP8266WebServer对象,对象名称为esp8266_server
void handleRoot() { //处理根目录请求(网站主页给出一个"Toggle LED"按键,按下它将用POST方法发送"/LED"的目录请求)
esp8266_server.send(200, "text/html", "<form action=\"/LED\" method=\"POST\"><input type=\"submit\" value=\"Toggle LED\"></form>");
}
void handleLED() { //处理LED控制请求
digitalWrite(LED_BUILTIN,!digitalRead(LED_BUILTIN)); //改变LED的点亮或者熄灭状态
esp8266_server.sendHeader("Location","/"); //跳转回页面根目录
esp8266_server.send(303); //发送Http相应代码303跳转
}
void handleNotFound(){ //处理404情况
esp8266_server.send(404, "text/plain", "404: Not found");
}
void setup(void){
Serial.begin(9600); //启动串口通讯
pinMode(LED_BUILTIN, OUTPUT); //设置内置LED引脚为输出模式以便控制LED
wifiMulti.addAP("Zevalin_Computer", "00114514");
wifiMulti.addAP("Zevalin_esp8266", "00114514");
int i = 0;
while (wifiMulti.run() != WL_CONNECTED) { //连接信号最强的Wi-Fi网络
delay(1000); Serial.print(i++); Serial.print(' ');
}
Serial.println('\n');
Serial.print("Connected to "); Serial.println(WiFi.SSID());
Serial.print("IP address:\t"); Serial.println(WiFi.localIP());
esp8266_server.begin(); //启动网站服务
esp8266_server.on("/", HTTP_GET, handleRoot); //设置服务器根目录即'/'的函数'handleRoot'
esp8266_server.on("/LED", HTTP_POST, handleLED); //设置处理LED控制请求的函数'handleLED'
esp8266_server.onNotFound(handleNotFound); //设置处理404情况的函数'handleNotFound'
Serial.println("HTTP esp8266_server started"); //告知用户ESP8266网络服务功能已经启动
}
void loop(void){
esp8266_server.handleClient(); //处理http服务器访问
}
3、通过网络服务将开发板引脚状态显示在网页中
(1)为了便于学习,本节将使用D3引脚作为演示案例,因为它已经与开发板上的FLASH按键开关连接好了,当没有按下该按键时D3引脚将会保持高电平状态,当按下该按键时D3引脚会保持低电平状态。

(2)使用ESP8266实现通过网络服务将开发板引脚状态显示在网页中,其程序流程如下图所示。

(3)将以下示例程序粘贴到代码区中,然后将其编译并上传NodeMCU开发板,开启电脑热点"Zevalin_Computer",ESP8266会建立起一个小型服务器,接下来打开浏览器,在地址栏中输入NodeMCU的IP地址并按下回车,可以看到网页中打印出D3引脚的电平信息,按下FLASH开关不松开,然后刷新网页,可以看到网页中打印出D3引脚的电平信息发生变化。
cpp
#include <ESP8266WiFi.h> //ESP8266WiFi库
#include <ESP8266WiFiMulti.h> //ESP8266WiFiMulti库
#include <ESP8266WebServer.h> //ESP8266WebServer库
#define buttonPin D3 //按钮引脚D3宏定义
ESP8266WiFiMulti wifiMulti; //建立ESP8266WiFiMulti对象,对象名称是'wifiMulti'
ESP8266WebServer esp8266_server(80); //建立ESP8266WebServer对象,对象名称为esp8266_server
bool pinState; //存储引脚状态的变量
void handleRoot(){
String displayPinState; //定义存储按键状态的字符串变量
if(pinState == HIGH){ //当按键引脚D3为高电平
displayPinState = "Button State: HIGH"; //字符串赋值高电平信息
}
else{ //当按键引脚D3为低电平
displayPinState = "Button State: LOW"; //字符串赋值低电平信息
}
esp8266_server.send(200, "text/plain", displayPinState); //向浏览器发送按键状态信息
}
void handleNotFound(){ //处理404情况
esp8266_server.send(404, "text/plain", "404: Not found");
}
void setup(void){
Serial.begin(9600); //启动串口通讯
pinMode(buttonPin, INPUT_PULLUP); //将按键引脚设置为输入上拉模式
wifiMulti.addAP("Zevalin_Computer", "00114514");
wifiMulti.addAP("Zevalin_esp8266", "00114514");
int i = 0;
while (wifiMulti.run() != WL_CONNECTED) { //连接信号最强的Wi-Fi网络
delay(1000); Serial.print(i++); Serial.print(' ');
}
Serial.println('\n');
Serial.print("Connected to "); Serial.println(WiFi.SSID());
Serial.print("IP address:\t"); Serial.println(WiFi.localIP());
esp8266_server.begin(); //启动网站服务
esp8266_server.on("/", handleRoot); //设置服务器根目录的函数'handleRoot'
esp8266_server.onNotFound(handleNotFound); //设置处理404情况的函数'handleNotFound'
Serial.println("HTTP esp8266_server started"); //告知用户ESP8266网络服务功能已经启动
}
void loop(void){
esp8266_server.handleClient(); //不断处理http服务器访问
pinState = digitalRead(buttonPin); //不断获取引脚状态
}
(4)以上程序虽然能让客户端获取引脚状态,但如果引脚状态更新,而客户端又没有发送新的HTTP请求(刷新网页相当于重新发出一次HTTP请求),那么客户端显示的引脚状态是错误的。为了解决这个问题,可以修改handleRoot函数,使它向客户端发送的不再是简单的D3引脚电平状态字符串,而是一串稍复杂的html代码,其对应的网页除了打印D3引脚电平状态信息以外,还会每隔5s自动刷新一次,即发送一次新的HTTP请求,这样,服务器就能每隔5s将最新的D3引脚电平状态与客户端同步一次。
cpp
void handleRoot() { //处理网站目录"/"的访问请求
esp8266_server.send(200, "text/html", sendHTML(pinState));
}
String sendHTML(bool buttonState){
String htmlCode = "<!DOCTYPE html> <html>\n";
htmlCode +="<head><meta http-equiv='refresh' content='5'/>\n"; //该页面每隔5s自动刷新一次,也就是说每隔5s发出一次访问根目录请求
htmlCode +="<title>ESP8266 Butoon State</title>\n";
htmlCode +="<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}\n";
htmlCode +="body{margin-top: 50px;} h1 {color: #444444;margin: 50px auto 30px;} h3 {color: #444444;margin-bottom: 50px;}\n";
htmlCode +="</style>\n";
htmlCode +="</head>\n";
htmlCode +="<body>\n";
htmlCode +="<h1>ESP8266 BUTTON STATE</h1>\n";
if(buttonState) //根据D3引脚状态制定相应的html代码,客户端接收后将其对应的网页信息打印在浏览器中
{htmlCode +="<p>Button Status: HIGH</p>\n";}
else
{htmlCode +="<p>Button Status: LOW</p>\n";}
htmlCode +="</body>\n";
htmlCode +="</html>\n";
return htmlCode;
}
五、ESP8266闪存文件系统及应用
1、ESP8266闪存文件系统介绍
(1)通常来说,ESP8266闪存文件系统(Serial Peripheral Interface Flash File System)有4MB的空间,1MB空间用于存储应用程序,3MB空间用于存储文件(其中有一部分是系统文件,不可修改)。

(2)SPIFFS是FS库提供的一个类(或者说实例),是对闪存文件系统的抽象。SPIFFS提供的几个成员函数如下:
①format函数:用于清除闪存中专门用于存放文件的"SPIFFS文件系统分区"里的数据,不会影响主程序。
②begin函数:用于启动闪存文件系统,并返回启动结果(True表示成功,False表示失败,失败并不意味着系统损坏,可能是配置有问题导致的)。
③open函数:用于以指定方式打开指定文件,其第一个参数为文件路径(或者说被操作文件的位置及名称),第二个参数为打开方式(字符串"w"表示写访问,字符串"r"表示读访问,字符串"a"表示追加信息)。
④exists函数:用于判断闪存中是否有指定文件,函数参数为文件路径(或者说指定文件的位置及名称)。
⑤openDir函数:用于打开指定路径的"目录",并返回一个Dir对象,函数参数为被读取的文件夹路径。
⑥remove函数:用于移除指定文件,函数参数为文件路径(或者说指定文件的位置及名称),如移除成功则返回True,否则返回False(如需要移除的文件本就不存在)。
⑦info函数:用于获取闪存文件系统信息,以FSInfo对象的类型返回。
(3)File也是FS库提供的一个类,是对文件的抽象,使用open函数打开一个文件时,open函数的返回值类型就是File。File类提供的成员函数如下:
①println函数:向File对象指向的文件中写入字符串信息,函数参数为字符串。(用"w"方式打开文件时,每次写入前都会先将文件原有内容清除,而用"a"方式打开文件时,每次写入都是直接在文件原有内容后面追加新内容)
②read函数:从File对象指向的文件中读取一个字符的信息并返回,然后文件指针右移一字节,指向下一个字符。
③close函数:关闭File对象指向的文件,防止其它代码段对文件进行误操作。
④size函数:获取File对象指向的文件大小并返回,单位为字节。
(4)Dir也是FS库提供的一个类,是对目录的抽象,有一个文件指针成员指向目录中的其中一个文件,使用openDir函数打开一个文件夹时,openDir函数的返回值类型就是Dir。Dir类提供的成员函数如下:
①next函数:用于检查目录中是否还有"下一个文件",如有则返回True,文件指针成员指向"下一个文件",如无则返回False。
②fileName函数:用于获取Dir对象的文件指针指向的文件的名称。
(5)FSInfo也是FS库提供的一个类,其成员属性是闪存文件系统的信息,比如可用空间总和、已用空间等,使用info函数获取闪存文件系统信息时,info函数的返回值类型就是FSInfo。



2、ESP8266闪存文件系统基本操作示例
(1)在菜单栏中选择"工具"→"Flash Size",弹出的几个选项用于指示用户需要使用的闪存文件系统空间大小,比如程序员需要使用1MB的闪存文件系统空间,则选择"4MB(FS:1MB OTA:~1019KB)"即可。
(2)将以下示例程序粘贴到代码区中,然后将其编译并上传NodeMCU开发板,这样即可通过程序向闪存文件系统写入指定信息。
cpp
#include <FS.h>
String file_name = "/taichi-maker/notes.txt"; //被读取的文件位置和名称
void setup() {
Serial.begin(9600);
Serial.println("");
Serial.println("SPIFFS format start");
SPIFFS.format(); //格式化闪存文件系统
Serial.println("SPIFFS format finish");
if(SPIFFS.begin()){ //启动闪存文件系统
Serial.println("SPIFFS Started.");
}
else
{
Serial.println("SPIFFS Failed to Start.");
}
File dataFile = SPIFFS.open(file_name, "w"); //建立File对象用于向SPIFFS中的file对象(即/notes.txt)写入信息
dataFile.println("Hello IOT World."); //向dataFile写入字符串信息
dataFile.close(); //完成文件写入后关闭文件
Serial.println("Finished Writing data to SPIFFS");
}
void loop() {
}
(3)在上一程序运行后,将以下示例程序粘贴到代码区中,然后将其编译并上传NodeMCU开发板,这样即可通过程序从闪存文件系统读取先前写入的信息。
cpp
#include <FS.h>
String file_name = "/taichi-maker/notes.txt"; //被读取的文件位置和名称
void setup() {
Serial.begin(9600);
Serial.println("");
if(SPIFFS.begin()){ //启动闪存文件系统
Serial.println("SPIFFS Started.");
}
else {
Serial.println("SPIFFS Failed to Start.");
}
//确认闪存中是否有file_name文件
if (SPIFFS.exists(file_name)){
Serial.print(file_name);
Serial.println(" FOUND.");
}
else {
Serial.print(file_name);
Serial.print(" NOT FOUND.");
}
//建立File对象用于从SPIFFS中读取文件
File dataFile = SPIFFS.open(file_name, "r");
//读取文件内容并且通过串口监视器输出文件信息
for(int i=0; i<dataFile.size(); i++){
Serial.print((char)dataFile.read()); //从文件中读取1个字符并以char类型输出
}
//完成文件读取后关闭文件
dataFile.close();
}
void loop() {
}
(4)在上一程序运行后,将以下示例程序粘贴到代码区中,然后将其编译并上传NodeMCU开发板,这样即可通过程序向闪存文件系统中的文件添加信息。
cpp
#include <FS.h>
String file_name = "/taichi-maker/notes.txt"; //被读取的文件位置和名称
void setup(){
Serial.begin(9600);
Serial.println("");
if(SPIFFS.begin()){ //启动闪存文件系统
Serial.println("SPIFFS Started.");
}
else{
Serial.println("SPIFFS Failed to Start.");
}
//确认闪存中是否有file_name文件
if (SPIFFS.exists(file_name)){
Serial.print(file_name);
Serial.println(" FOUND.");
File dataFile = SPIFFS.open(file_name, "a"); //建立File对象用于向SPIFFS中的file对象(即/notes.txt)写入信息
dataFile.println("This is Appended Info."); //向dataFile添加字符串信息
dataFile.close(); //完成文件操作后关闭文件
Serial.println("Finished Appending data to SPIFFS");
}
else{
Serial.print(file_name);
Serial.print(" NOT FOUND.");
}
}
void loop(){
}
(5)在上一程序运行后,将以下示例程序粘贴到代码区中,然后将其编译并上传NodeMCU开发板,这样即可通过程序读取闪存文件系统指定目录中的内容。
cpp
#include <FS.h>
String file_name = "/taichi-maker/myFile.txt"; //被读取的文件位置和名称
String folder_name = "/taichi-maker"; //被读取的文件夹
void setup(){
Serial.begin(9600);
Serial.println("");
if(SPIFFS.begin()){ //启动闪存文件系统
Serial.println("SPIFFS Started.");
}
else{
Serial.println("SPIFFS Failed to Start.");
}
File dataFile = SPIFFS.open(file_name, "w"); //建立File对象用于向SPIFFS中的file对象(即myFile.txt)写入信息
dataFile.println("Hello Taichi-Maker."); //向dataFile写入字符串信息
dataFile.close(); //完成文件写入后关闭文件
Serial.println(F("Finished Writing data to SPIFFS"));
//显示目录中文件内容以及文件大小
Dir dir = SPIFFS.openDir(folder_name); //建立"目录"对象
while (dir.next()){ //dir.next()用于检查目录中是否还有"下一个文件"
Serial.println(dir.fileName()); //输出文件名
}
}
void loop(){
}
(6)在上一程序运行后,将以下示例程序粘贴到代码区中,然后将其编译并上传NodeMCU开发板,这样即可通过程序从闪存文件系统中删除指定文件。
cpp
#include <FS.h>
String file_name = "/taichi-maker/notes.txt"; //被读取的文件位置和名称
void setup(){
Serial.begin(9600);
Serial.println("");
if(SPIFFS.begin()){ //启动闪存文件系统
Serial.println("SPIFFS Started.");
}
else{
Serial.println("SPIFFS Failed to Start.");
}
if (SPIFFS.remove(file_name)){ //从闪存中删除file_name文件
Serial.print(file_name);
Serial.println(" remove sucess");
}
else{
Serial.print(file_name);
Serial.println(" remove fail");
}
}
void loop(){
}
(7)在上一程序运行后,将以下示例程序粘贴到代码区中,然后将其编译并上传NodeMCU开发板,这样即可通过程序获取闪存文件系统的信息,如可用空间总和等。
cpp
#include <FS.h>
FSInfo fs_info;
void setup(){
Serial.begin(9600);
SPIFFS.begin(); //启动SPIFFS
Serial.println(""); Serial.println("SPIFFS Started.");
//闪存文件系统信息
SPIFFS.info(fs_info);
//可用空间总和(单位:字节)
Serial.print("totalBytes: ");
Serial.print(fs_info.totalBytes);
Serial.println(" Bytes");
//已用空间(单位:字节)
Serial.print("usedBytes: ");
Serial.print(fs_info.usedBytes);
Serial.println(" Bytes");
//最大文件名字符限制(含路径和'\0')
Serial.print("maxPathLength: ");
Serial.println(fs_info.maxPathLength);
//最多允许打开文件数量
Serial.print("maxOpenFiles: ");
Serial.println(fs_info.maxOpenFiles);
//存储块大小
Serial.print("blockSize: ");
Serial.println(fs_info.blockSize);
//存储页大小
Serial.print("pageSize: ");
Serial.println(fs_info.pageSize);
}
void loop(){
}