65.9K
CodeProject 正在变化。 阅读更多。
Home

一款 5 美元的便携式可编程微型 Web 服务器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (28投票s)

2020 年 11 月 3 日

MIT

7分钟阅读

viewsIcon

39446

downloadIcon

406

在您的网络上释放微型 ESP-01。

ESP-01

引言

未来已至!我们现在拥有价格惊人且微小的可联网32位CPU,它们正变得无处不在。在本文中,我们将深入探讨ESP-01模块,并在其上编程一个小型网络服务器。

更新:增加了对将内容嵌入闪存文件系统的支持

更新2:增加了一个用于gzipping和ungzipping目录的工具,以便与网络服务器配合使用

必备组件

您将需要一个ESP-01模块和一个基于CH340的USB转ESP8266适配器。这两者都显示在上方图片中。

您将需要Arduino IDE。您必须转到文件|首选项,并将以下内容添加到附加开发板管理器文本框中

如果已经存在一个URL,请用逗号分隔它们。

确保在工具|开发板:下显示“Generic ESP8266 Module”。

将ESP8266模块插入USB适配器,如上图所示,使ESP8266位于USB适配器上方,而不是悬挂在末端。如果您从侧面看,它应该形成一个“U”形。这适用于此适配器。如果您的适配器不同,可能会有所不同。请务必小心,否则可能会损坏您的设备。

如果您想使用后面概述的技术,即我们将一些闪存存储用作文件系统,您需要执行以下操作

首先,此处获取最新的ESP8266FS zip文件。

如果您使用的是Linux,您需要获取zip文件中的ESP8266FS文件夹,并找到您的Arduino应用程序目录。这应该在您的主目录下。我的是~/arduino-1.8.13。不要与~/Arduino混淆。在该目录下,有一个tools文件夹,这就是您希望ESP8266FS文件夹所在的位置。

我从没在Windows上做过,但方法是这样的:你需要zip文件中的ESP8266FS文件夹。找到你的程序目录。它可能类似于C:\Program Files (x86)\arduino-1.8.13。里面有一个tools文件夹。ESP8266FS文件夹需要放在那里。

无论哪种方式,您都需要重新启动Arduino IDE。如果您现在有工具|ESP8266 Sketch Data Upload选项,就说明安装成功了。

概念化这个混乱的局面

基于ESP8266的ESP-01模块包含一个WiFi收发器和一个以80MHz运行的32位处理器,由一个适度的3.3伏电源(通常是USB)供电。除了WiFi,其主要的I/O设施是一个串行UART。通常我的代码中,该串行UART将是所有调试和状态消息的发送位置。通过基于CH340的USB适配器,它将该UART公开为一个虚拟COM端口。在这种情况下,您可以使用该COM端口监视设备正在做什么。

我们将创建一个基于部分示例代码的Web服务器。该Web服务器将自动连接到配置的SSID,然后通过URL http://test.local公开自身。该Web服务器仅显示一张图片和一些文本。test.local域名使用多播DNS公开。

由于通常没有存储设备,更不用说文件系统,所以我们所有的内容都必须嵌入到源代码本身中。可以使用文件系统,当我们这样做时,我们将使用稍微不同的技术来提供页面。

编写这个混乱的程序

每当您上传代码时,都必须确保您的USB适配器设置为“program”而不是“uart”。然后,您必须拔下USB适配器,将其切换到“uart”,然后重新插入USB插槽才能运行代码。

不带嵌入式文件系统

以下是服务器的代码。请注意,为了在此处显示内容而不发生大量换行,我已截断了主页和图像数据。请勿复制粘贴此代码,因为它会因截断而无法工作。请使用文章顶部的下载链接获取完整的.ino文件,该文件作为解决方案项包含在内。

#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <ESP8266WebServer.h>
#include <ESP8266mDNS.h>

#ifndef STASSID
// BEGIN configuration properties
#define STASSID "myssid"
#define STAPSK  "mypassword"
#define STAHOSTNAME "test"
// END configuration properties
#endif

const char* ssid = STASSID;
const char* password = STAPSK;

// create the server with the port to listen on
ESP8266WebServer server(80);

// the LED pin is 13
const int led = 13;

// handles requests to /
void handleRoot() {
  digitalWrite(led, 1);
  server.send(200, "text/html", "<!DOCTYPE html>\r\n<html ...");
  digitalWrite(led, 0);
}

// handles when content is not found
void handleNotFound() {
  digitalWrite(led, 1);
  String message = "File Not Found\n\n";
  message += "URI: ";
  message += server.uri();
  message += "\nMethod: ";
  message += (server.method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += server.args();
  message += "\n";
  for (uint8_t i = 0; i < server.args(); i++) {
    message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
  }
  server.send(404, "text/plain", message);
  digitalWrite(led, 0);
}

void setup(void) {
  pinMode(led, OUTPUT);
  // turn off the LED
  digitalWrite(led, LOW);
  // initialize the serial port
  Serial.begin(115200);
  // set up the wifi
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.println("");

  // Wait for connection
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  // display the connection info
  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("Host name: ");
  Serial.print(STAHOSTNAME);
  Serial.println(".local");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  // start the multicast DNS publishing
  if (MDNS.begin(STAHOSTNAME)) {
    Serial.println("MDNS responder started");
  }

  // install the handlers
  server.on("/", handleRoot);

  server.on("/inline", []() {
    server.send(200, "text/plain", "this works as well");
  });

  server.on("/test.jpg", []() {
    static const uint8_t img[] PROGMEM = 
      { 255, 216, 255, ... }
    ;
    server.send(200, "image/jpg", img, sizeof(img));
  });

  server.onNotFound(handleNotFound);

  // start the server
  server.begin();
  Serial.println("HTTP server started");
}

void loop(void) {
  // handle the request
  server.handleClient();
  MDNS.update();
}

我包含了一个用于生成内容字符串和字节数组的工具。它叫做file2c,是一个我附加到项目中的命令行实用程序。您可以像我一样使用这个工具,无论是文本还是二进制文件,都可以生成要传递给server.send()的内容。它可以从输入文件中生成C字符串或C字节数组初始化器。这样,您就可以编辑一个HTML页面,然后在完成时将其转换为C字符串,以便将其注入到上面的代码中。运行它的方法如下

file2c mydoc.html 

或者对于像图片这样的二进制文件

file2c myimage.jpg /binary

然后你可以将生成的输出复制到你的代码中。

使用嵌入式文件系统

这样做需要一种略有不同的技术,我们使用内置闪存来存储内容并从中提供服务。用于闪存的文件系统称为SPIFFS。我们只在请求的路径没有现有处理程序时才这样做。基本上,它会在通常会转到404之前进行拦截,如果文件存在,则会提供该文件。这样,任何现有处理程序仍然可以工作。

为此,我们需要在上面介绍的Web服务器代码中添加一些支持函数。

请注意,我从此处的文章中获取了这段代码。

首先,我们需要在代码中为文件系统API添加一个头文件

#include <FS.h>

接下来,我们需要能够将MIME内容类型与文件扩展名关联起来,这就是以下方法的作用

String getContentType(String filename){
  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";
}

根据需要添加类型。您还需要做的一件事是添加一个方法来处理向客户端发送文件

// send the right file to the client (if it exists)
bool handleFileRead(String path){  
  Serial.println("handleFileRead: " + path);
  // If a folder is requested, send the index file
  if(path.endsWith("/")) path += "index.html";           
  // Get the MIME type
  String contentType = getContentType(path);             
  String pathWithGz = path + ".gz";
  // If the file exists, either as a compressed archive, or normal
  if(SPIFFS.exists(pathWithGz) || SPIFFS.exists(path)){  
    // If there's a compressed version available
    // Use the compressed version           
    if(SPIFFS.exists(pathWithGz))                          
      path += ".gz";                                  
    // Open the file
    File file = SPIFFS.open(path, "r");                    
    // Send it to the client    
    size_t sent = server.streamFile(file, contentType);    
    // Close the file
    file.close();                                          
    Serial.println(String("\tSent file: ") + path);
    return true;
  }
  Serial.println(String("\tFile Not Found: ") + path);
  // If the file doesn't exist, return false
  return false;                                          
}

请注意我们对.gz文件进行了特殊处理。这样做是为了我们可以对内容进行gzip压缩,以节省宝贵的闪存空间和少量带宽。基本上,我们将内容存储为foo.html.gzbar.jpg.gz并以这种方式提供服务。浏览器将知道如何显示它。

接下来,我们需要更新我们的处理程序,通常我们只会发送404。用这个稍微不同的代码替换handleNotFound()方法

// handles when content is not found
void handleNotFound() {
  digitalWrite(led, 1);
  // If the client requests any URI
  if (handleFileRead(server.uri())) // send it if it exists
     return;
  String message = "File Not Found\n\n";
  message += "URI: ";
  message += server.uri();
  message += "\nMethod: ";
  message += (server.method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += server.args();
  message += "\n";
  for (uint8_t i = 0; i < server.args(); i++) {
    message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
  }
  server.send(404, "text/plain", message);
  digitalWrite(led, 0);
}

我用粗体字标出了更改。

现在我们需要从现有代码中删除以下行

// install the handlers
server.on("/", handleRoot);

server.on("/inline", []() {
 server.send(200, "text/plain", "this works as well");
});

server.on("/test.jpg", []() {
  static const uint8_t img[] PROGMEM = 
    { 255, 216, 255, ... }
  ;
  server.send(200, "image/jpg", img, sizeof(img));
});

并将其替换为

SPIFFS.begin(); 

以初始化文件系统。

现在移除handleRoot()方法,因为我们不再需要它。

接下来,您必须将所有要提供的内容放在您的草图文件夹中一个名为“data”的文件夹下,例如,如果您的草图目录是~/projects/myweb,那么您的内容需要放在~/projects/myweb/data下。

此时,你可能会考虑对每个文件进行gzip压缩以节省空间和一些带宽,但主要是空间,因为空间不多且珍贵。你可以使用gzdir工具来完成。例如,如果我们在你的草图目录下,并且gzdir在你的PATH中,我们会这样做

gzdir data

这应该会压缩“data”目录中的每个文件并删除原始文件。您可以使用“/decompress”选项反向操作,如下所示

gzdir data /decompress

最后,一旦您完成代码上传到ESP8266,将ESP8266组件从USB插座中拔出,然后重新插入以重置它,以便它可以进行另一次闪存。然后您必须使用工具|ESP8266 Sketch Data Upload将您的文件闪存到设备。请注意错误。内置的1MB闪存很容易耗尽空间。

部署

一旦你完成了你的小型网络服务器的编程,如果你愿意,你可以不使用USB棒为其供电。它只需要3.3伏直流电连接到VCC,同样连接到EN(即CHG),然后GND连接到地线或负极端子。

未来方向

您可以使用串口与另一块板子通信,比如说您有一块Arduino或其他通过串口连接的I/O板。您可以使用与这篇文章中概述的方法几乎相同的技术来获取I/O。这样,您就可以将传感器连接到它,并例如使用动态网页报告它们的状态。另一个方向可能是将SD卡连接到SPI接口,以便您可以拥有一个文件系统并使其更像一个真正的网络服务器。

ESP-01上也有一个用于通信的SPI接口,但我对如何在软件中与其接口一无所知——目前还不知道!

历史

  • 2020年11月3日 - 首次提交
  • 2020年11月4日 - 更新以包含微型文件系统
  • 2020年11月4日 - 增加文件gzip工具
© . All rights reserved.