本文将解析《完美图解物联网Iot实操 ESP8266》中 第五章 P177页 动手做的代码2(使用SPIFFS文件系统的代码)
首先我们先动手使用Arduino IDE编译并且上传代码,上传后记得使用工具中ESP8266 SPIFFS上传工具上传SPIFFS文件夹的内容,否则应用将无法使用。你很有可能看到下面这样的画面。
FileNotFind 默认的404页面
至于为什么后面会详细说明,如果上传SPIFFS文件夹上传成功,访问你的ESP8266的IP看到的应该是这样的画面。
正常页面
下面我们开始解析一下代码吧,这里首先假定大家已经有开发过Arduino 应用的基础。
完整代码
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <FS.h>
const byte LED_PIN = 2;
const byte PWM_PIN = 0;
const char ssid[] = "TP-LINK_B5672C";
const char pass[] = "19970929";
const char* host = "jarvis";
ESP8266WebServer server(80);
// 定义处理首页请求的自定义函数
String getContentType(String filename){
if(server.hasArg("download")) return "application/octet-stream";
else if(filename.endsWith(".htm")) return "text/html";
else if(filename.endsWith(".html")) return "text/html";
else if(filename.endsWith(".css")) return "text/css";
else if(filename.endsWith(".js")) return "application/javascript";
else if(filename.endsWith(".png")) return "image/png";
else if(filename.endsWith(".gif")) return "image/gif";
else if(filename.endsWith(".jpg")) return "image/jpeg";
else if(filename.endsWith(".ico")) return "image/x-icon";
else if(filename.endsWith(".xml")) return "text/xml";
else if(filename.endsWith(".pdf")) return "application/x-pdf";
else if(filename.endsWith(".zip")) return "application/x-zip";
else if(filename.endsWith(".gz")) return "application/x-gzip";
return "text/plain";
}
bool handleFileRead(String path){
Serial.println("handleFileRead: " + path);
if(path.endsWith("/")) path += "index.htm";
String contentType = getContentType(path);
String pathWithGz = path + ".gz";
if(SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)){
if(SPIFFS.exists(pathWithGz))
path += ".gz";
File file = SPIFFS.open(path, "r");
size_t sent = server.streamFile(file, contentType);
file.close();
return true;
}
return false;
}
void setup( ){
pinMode(LED_PIN, OUTPUT);
Serial.begin(115200);
SPIFFS.begin(); // 启用SPIFFS文件系统
WiFi.begin(ssid, pass);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.print("Connected to ");
Serial.println(ssid);
Serial.print("IP address: ");
Serial.println(WiFi.localIP());
IPAddress ip = WiFi.localIP();
if (!MDNS.begin(host, ip)) {
Serial.println("Error setting up MDNS responder!");
while(1) {
delay(1000);
}
}
Serial.println("mDNS responder started");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.print("Connected! IP address: ");
Serial.println(WiFi.localIP());
server.on ("/sw", []() {
String state = server.arg("led");
if (state == "ON") {
digitalWrite(LED_PIN, HIGH);
} else if (state == "OFF") {
digitalWrite(LED_PIN, LOW);
}
Serial.print("LED_PIN: ");
Serial.println(state);
});
server.on ("/pwm", []() {
String pwm = server.arg("led");
int val = pwm.toInt();
analogWrite(PWM_PIN, val);
Serial.print("PWM: ");
Serial.println(val);
});
// 处理根路径以及「不存在的」路径
server.onNotFound([](){
if(!handleFileRead(server.uri()))
server.send(404, "text/plain", "FileNotFound");
});
server.begin();
Serial.println("HTTP server started");
MDNS.setInstanceName("Cubie's ESP8266");
MDNS.addService("http", "tcp", 80);
}
void loop( ){
server.handleClient();
}
头文件
可以看到我们这里引用的头文件分别是
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>
#include <FS.h>
下面分别说说看不同头文件的用处
ESP8266WIFI.h
这个头文件是提供ESP8266 WIFI功能的头文件,只有这个头文件在才能使用ESP8266的WIFI功能,否则无法使用WIFI(相当于把ESP8266当做没有wifi功能的atmega328p用一样)。
所有的Wifi对象定义在这个头文件里面。
ESP8266WebServer.h
这个头文件是提供ESP8266 Web服务器的功能,类似于Windows上运行的Web服务器一样。Web服务器是提供Web服务的基础,像是你正在阅读的这个博客一样,这个博客的Web服务器就是一个CentOS系统中使用Apache功能提供的(指的是myblogmaoweicao.club这个域名的博客)。博客园也有自己的Web服务器。
有了Web服务器才能使用通过一些协议定义的Web功能。
ESP8266mDNS.h
这个头文件是提供一个微型的DNS解析服务器的功能,就像正常的DNS服务器一样,但是这个功能可能只有提供 域名-IP绑定 的服务(也就是正常DNS服务的a类)。域名的概念类似于酒店里面给某个编号的房间起名字一眼,比如给化工楼313起名叫学生办公室,那么如果你到化工路的3楼,跟别人打听学生办公室是哪里,别人就会告诉你是313,这样你就能找到对应的房间了。
mDNS功能会将ESP8266本身设置成一个简单的DNS服务器,单访问特定的域名,例如:jarvis.local时,ESP8266就会将地址解析成正确的IP地址(例如自己的IP地址),在变化的环境下使用DNS服务其实更好。就类似于百度在很多地方都部署了自己的CDN(这是另外一种网络服务,用于分摊请求),但是每个CDN都有自己的IP地址,福州的CDN的IP地址能叫1.1.1.1,而百度的服务器其实叫114.114.114.114(这个不是真实的IP地址,举得例子都是DNS服务器),连接福州CDN的延时可能只要20ms,但是连接百度自己的服务器的延时药400ms,这个让人难以忍受了。但是如果有建立DNS的话,在福州这里访问www.baidu.com,DNS服务器就会自动将网址解析到延时最小的CDN服务器,也就是1.1.1.1上这样就保证了延时最小。当然这样做也会发生事故,就是如果没有建立正确的链接,那么你访问www.baidu.com可能就得不到在正确的页面。
FS.h
这个头文件是提供文件系统的相关功能,在嵌入式开发板上每个板子的文件系统都不太一样,但是通过上层抽象就有一致的接口可以访问了。
只有这个头文件被引用,才能使用SPIFFS对象,也就是ESP01上那唯一的一块硬盘。
常量(const修饰的变量)
常量 的定义 是C++特有的概念之一,由const(不变的)修饰符修饰,被const修饰的变量无法被外界再次改变。
具体的大家看C++的图书就好了,const修饰符和Java中的final修饰符(在Java中使用final(最终的)修饰一个变量来作为常量)很像。
在这个代码中,修饰的常量有五个。
LED_PIN
连接了LED的引脚,这里是指定的是ESP01上唯二引出的引脚GPIO 0。
PWM_PIN
这里制定了 PWM(载波调制)的引脚,用于输出某种模拟量(用数字信号模拟),这里指定的ESP01上唯二引出的引脚GPIO 2。
SSID
SSID是WIFI特有的概念之一,也就是路由器的“名字”,这里需要由程序指定链接哪个wifi。(除非你对其进行修改)
PASS
PASS对应的是Password(密码),这里需要输入的就是你要链接的路由器的Wifi密码,只有给出正确的密码才能连入正确的网络。
HOST
host是主机名的意思,用于指定这块ESP01的域名,这里我们指定的名字叫做“jarvis”
对象
ESP8266WebServer server(端口号)
这一句就是定义个ESP8266的WebServer对象叫做“server”,然后服务的端口号为80(也就是一般HTTP服务的端口号)。
这是个网站服务器,所以提供了一些网站服务的方法。
(HTTP对象).on(“服务器的连接”,<处理函数>)方法(在C++里面叫做函数,在Java里面叫方法,其实是一个东西)定义了一个服务器可以对哪些链接提供哪些独一无二的服务。
(HTTP对象).onNotFound(<处理函数>)方法定义了一个服务器在处理没有被on指定的服务之外的对象应该这么做。
(HTTP对象).uri()方法是返回服务器接收到的请求的URL连接,像是访问
“www.baidu.com/1.png”(假设有这个路径,实际上没有),那么这个函数会返回“1.png”这个就是URL。(URL具体定义参考:https://baike.baidu.com/item/url/110640?fr=aladdin)
(HTTP对象).handClient()就是处理客户端的请求,每个服务器都要处理对应的客户端的请求。
Serial 串口对象
Arduino内建的对象,描述的单片机经常用的串口功能。具体参考:https://www.arduino.cc/reference/en/language/functions/communication/serial/
SPIFFS SPI文件系统对象
文件系统就是我们常说的硬盘上用的那种文件系统,操作系统只有跟文件系统打交道才能跟硬盘要交流。在PC机上的Windows系统上常见的是FAT32(以前挺多的,现在少见)、NTFS(多见,目前Windows默认格式化磁盘的格式就是NTFS)、EXFAT(扩展的FAT32,支持4GB以上的对象)等。
在单片机上由于性能限制,所以只能使用一些比较简单的文件系统,像这里用的就是SPIFFS。
SPIFFS.begin()方法,用于挂载SPIFFS的文件系统,使用前必须需要begin方法进行初始化。一般操作系统在加载的时候也都初始化了。
一般还要搭配SPIFFS.format()来进行格式化,但是这里没有,因为ESP8266都安排好了。所以不用format了。
SPIFFS.open(路径, 模式)类似于C语言里面的fopen函数用来打开一个文件,返回一个File对象。
SPIFFS.exists(路径) 用来检测路径上的文件是否存在。
这里就是用来ESP8266的这个磁盘到底有没有这个文件用的。一般都要判断是否存在。
当然在这里如果你想要上传一些东西,需要在Arduino工程所在的文件夹里面在新建一个data文件夹用于上传需要的东西不过在Arduino上传你还需要安装一个另外的工具。
IPAddress IPv4地址对象(4字节)
这是一个在ESP8266里面内部定义的一个IP地址对象,这是IP v4的对象,所以是4个字节。每一个字节表示一个范围。具体参考:《计算机网络》
MDNS 组播DNS服务器对象
用于定义一个小小的DNS服务器,来完成域名到IP+端口号的转换任务。
一般是由公网服务商提供。像是1.1.1.1就是个DNS服务器。
这里是定义个组播DNS服务器,用于局域网内没有DNS服务器的情况下进行DNS的服务。
MDNS.begin(主机名,IP地址)初始化MDNS服务器。
MDS.setInstanceName(“别名”)这个就是设置MDNS服务器的别名。
MDNS.addService(“http”, “tcp”, 80) 添加服务,为特定的类型添加服务器,这里为http协议设置成TCP格式的内容并且添加了对应服务的端口号为80
特殊语法
在这个例子里面你们可能看到跟平时都看不到的语法也就是 [](){} 这种格式其实是C++ 11里面新增的语法叫做 lambda表达式。用于代替一些需要使用匿名函数的场合。(匿名函数,即没有名字的函数,程序员有时候觉得有些时候为一些简单功能的函数起个名字太麻烦了,所以就有了匿名函数的功能)
具体的格式是:
[捕获列表](形式参数列表) 返回类型 {函数体}
与正常的函数相同他也有自己的形参和返回类型,这的返回类型是可选项。
捕获列表 是一个特殊的函数,仅在函数体内可用。一般在全局环境下不可用。(因为编译器不知道你要捕获什么玩意),捕获列表可以写一些变量或者对象的名字,这样你就能在lambda表达式内部使用这些东西而不用进行参数传递。(也就是对象地址传递),具体的参考《C++ Primer》里面有详细介绍(可以买第五版的中文版,*哥校对的【逼乎上一个老司机,在微软工作,真名叫 陈梓瀚】)
结束
下面上传代码,并且上传SPIFFS文件系统里面的东西,那么你在对应的IP地址应该可以看到这样的东西。