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

带以太网 Shield 的简单 Arduino Web 服务器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.33/5 (8投票s)

2017年5月20日

CPOL

5分钟阅读

viewsIcon

34126

downloadIcon

554

为带有 Wiznet 5100 以太网 Shield 的 Arduino 实现基本的 HTTP 请求处理。

引言

这是一个用于 Arduino 和 Wiznet 5100 以太网盾的极其简单的 Web 服务器。加载此草图后,您将立即开始接收请求!

  • 支持 IPv4。本示例使用 TCP/IP。
  • 在端口 80 上监听,这是未经加密的 HTTP 流量的标准 TCP 端口。设备将在 http://192.168.1.23/ 响应。
  • 默认网关默认禁用。这意味着设备仅响应来自本地/私有网络的请求。
  • 如果出于某种原因需要它响应 Internet 请求,您可以配置路由器进行端口映射,并将默认网关设置为 192.168.1.1。然后设备将通过路由器响应。有关更多信息,请搜索“端口地址转换”。
  • 客户端请求和响应代码以 115200 波特率写入控制台。
  • 此 Web 服务器仅适用于纯文本,因此任何图像都必须托管在真正的 Web 服务器上(或使用更强大的 Arduino)。
  • 请保持现实的期望。如果您的项目超出了基本自动化要求,请考虑完整的嵌入式系统或服务器操作系统。

作者:Mark Scammacca

你需要的东西

(总成本约 10 美元)

  1. Arduino Uno - 随处可见。在 eBay 上花几美元就能买到一个。
    重要提示:选择带有微型 USB 连接器的型号,这样 Wiznet 5100 以太网盾才能轻松安装在其上方。
  2. Arduino Wiznet 5100 以太网盾 - eBay 上也有很多。

    可选:选择支持 POE(以太网供电)的型号。POE 板必须单独购买并焊接,然后您的 POE 交换机就可以直接通过以太网线为所有 Arduino 设备供电。

    重要提示:还有其他类型的以太网盾,此代码仅适用于 Wiznet 兼容型号。但对于任何以太网盾,基本原理都可以通过少量修改后工作。

  3. Arduino 开发软件(免费) - 完全免费安装。此项目除了开发环境已有的内容外,不需要任何其他依赖项。不需要芯片编程器 - 编程通过 USB 完成。安装 Arduino 软件后,USB 端口还充当串行端口,因此您可以使用 Putty 或 Arduino 软件内置的串行控制台监控此代码的控制台输出。

以太网供电

Wiznet 5100 盾牌有支持以太网供电的版本,但这需要一个单独购买的附加板。有些型号有焊接点可以安装 POE 附加板,有些则没有。请仔细检查。我看到它们被列为“POE 以太网盾”,但它们的意思是“支持 POE,单独出售”。然后该设备可以由 POE 交换机或 POE 注入器供电。

如果您对 POE 感兴趣并想选择合适的交换机类型,有三种主要的标准需要了解

  1. IEEE 802.3af-2003
  2. IEEE 802.3at-2009(功率更大,应向后兼容 af 设备)
  3. 专有 - 通常最便宜,有时与 802.3af 或 802.3at 设备不兼容,会导致不可预测的结果。有时包含匹配的终端。

还可以考虑 POE 分割器。这些分割器通过提供 barrel plug 输出,简化了将非 POE 设备连接到 POE 交换机。POE 分割器从线路中提取电源,并通过 barrel plug 提供给设备。它们接受 48V POE 电源并将其降低到 5VDC、12VDC 等。我用它们的效果很好。

安全注意事项

此代码的安全性未经审查 - 因此直接将其连接到 Internet 可能是一个糟糕的主意。出于此原因,默认网关设置为 0.0.0.0。如果您选择将其连接到路由器,请将默认网关设置为 192.168.1.1(或您的路由器的 IP 地址)并搜索“端口地址转换”。

在仔细保护设备免受黑客攻击之前,请勿在实际产品中使用此代码。一旦人们认为攻击物联网设备有价值,他们普遍会攻击物联网设备。

Using the Code

安装 Arduino 软件后,只需连接 USB 并将此草图加载到您的 Arduino 设备中。设备和以太网盾可以通过 USB 供电。

请求在 void respond() 函数中处理。

默认页面只是一个带有点击计数器的启动页面:http://192.168.1.23/

测试页面显示了如何传入参数并将它们写回响应:http://192.168.1.23/test.html

每次请求都必须完整处理,然后 Arduino 才能响应下一个请求,因此请保持请求简短。

(只需突出显示代码并复制/粘贴。请勿使用“复制代码”按钮,否则粘贴时 HTML 元素将损坏。)

/*
  Board: Duemilanove or Diecimila
  Processor: ATmega328
  Programmer: AVR-ISP mkII

  UNO 5V BOARD
  and Wiznet 5100 Ethernet Shield
  
  X0 - RX - FTDI
  X1 - TX - FTDI
  2  DIG02 - 
  3  DIG03 - 
  4  DIG04 - SD CARD             / RESERVED
  5  TIMR1 - 
  6  DIG06 -
  7  DIG07 - 
  8  DIG08 - 
  9  DIG09 - 
  10 DIG10 - SPI ETHERNET SS     / RESERVED
  11 DIG11 - SPI ETHERNET MOSI   / RESERVED
  12 DIG12 - SPI ETHERNET MISO   / RESERVED
  13 DIG13 - SPI ETHERNET SCK    / RESERVED (LED FLICKERS DURING SPI COMM)
  22 ADC00 - 
  23 ADC01 - 
  24 ADC02 - 
  25 ADC03 - 
  26 ADC04 - 
  27 ADC05 - 
  18? ADC06 - 
  21? ADC07 - 
*/

#include <SPI.h>
#include <Ethernet.h>

/*
  MACRO for string handling from PROGMEM
  https://todbot.com/blog/2008/06/19/how-to-do-big-strings-in-arduino/
  max 149 chars at once ...
*/
char p_buffer[150];
#define P(str) (strcpy_P(p_buffer, PSTR(str)), p_buffer)

// Ethernet Interface Settings
byte mac[] =      { 0xAD, 0xDE, 0xEF, 0xBB, 0xFD, 0xDD };
IPAddress ip      (192, 168, 1, 23);  // Private static IP address
IPAddress myDns   (192, 168, 1, 1);   // DNS is not needed for this example
IPAddress gateway (0, 0, 0, 0);       // Default gateway disabled for security reasons
IPAddress subnet  (255, 255, 255, 0); // Class C subnet; typical

// HTTP lives on TCP port 80
EthernetServer server(80);

void setup()
{
  // start the Ethernet connection and the server:
  Ethernet.begin(mac, ip);
  server.begin();
  Serial.begin(115200);
}

// string buffers for receiving URL and arguments
char bufferUrl[256];
char bufferArgs[512];
int urlChars = 0;
int argChars = 0;

// number of characters read on the current line
int lineChars = 0;

// total # requests serviced
long requests = 0;

// connection state while receiving a request
int state = 0;

/*
  Typical request: GET /<request goes here>?firstArg=1&anotherArg=2 HTTP/1.1
  State 0 - connection opened
  State 1 - receiving URL
  State 2 - receiving Arguments
  State 3 - arguments and/or URL finished
  State 4 - client has ended request, waiting for server to respond
  State 5 - server has responded
  
  Example of what the server receives:
  
  GET /test.html HTTP/1.1
  Host: 192.168.1.23
  Connection: keep-alive
  Cache-Control: max-age=0
  Upgrade-Insecure-Requests: 1
  User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 
  (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36
  Accept-Encoding: gzip, deflate, sdch
  Accept-Language: en-US,en;q=0.8
*/

void loop()
{
  // listen for incoming clients
  EthernetClient client = server.available();
  
  if (client) 
  {
    state = 0;
    urlChars = 0;
    argChars = 0;
    lineChars = 0;
    bufferUrl[0] = 0;
    bufferArgs[0] = 0;

    while (client.connected()) 
    {
      if (client.available()) 
      {
        // read and echo data received from the client
        char c = client.read();
        Serial.print(c);

        // ignore \r carriage returns, we only care about \n new lines
        if (c == '\r')
          continue;        
          
        // control what happens to the buffer:
        if (state == 0 && c == '/')
        {
          // Begin receiving URL
          state = 1; 
        }
        else if (state == 1 && c == '?')
        {
          // Begin receiving args
          state = 2;
        }
        else if ((state == 1 || state == 2) && c == ' ')
        {
          // Received full request URL and/or args
          state = 3;
        }
        else if (state == 1 && urlChars < 255)
        {
            // Receiving URL (allow up to 255 chars + null terminator)
            bufferUrl[urlChars++] = c;
            bufferUrl[urlChars] = 0;
        }
        else if (state == 2 && argChars < 511)
        {
            // Receiving Args (allow up to 511 chars + null terminator)
            bufferArgs[argChars++] = c;
            bufferArgs[argChars] = 0;
        }
        else if (state == 3 && c == '\n' && lineChars == 0)
        {
          // Received a line with no characters; 
          // this means the client has ended their request
          state = 4;
        }

        // record how many characters on the line so far:
        if (c == '\n')
          lineChars = 0;
        else
          lineChars++;

        // OK to respond
        if(state == 4)
        {
          // Response given
          state = 5; 

          // increment internally for fun purposes
          requests++;
          Serial.print(P("Request # "));
          Serial.print(requests);
          Serial.print(P(": "));
          Serial.println(bufferUrl);

          // handle the response
          respond(client);

          // exit the loop
          break;
        }
      }
    }
    
    // flush and close the connection:
    client.flush();
    client.stop();
  }
}

void respond(EthernetClient client)
{
  if (strcmp(bufferUrl, P("")) == 0)
  {
    // Requested: /  (DEFAULT PAGE)
    
    // send response header
    sendHttpResponseOk(client);

    // send html page
    // max length: ------------------------------------------------------  (149 chars)
    client.println(P("<HTML><head><title>Welcome</title></head><body><h1>Welcome, visitor"));
    client.print(requests);
    client.println(P("!</h1>Click here to visit the <a href=/test.html>Test Page</a><p>"));
    client.println(P("String output is stored in progmem to conserve RAM. 
    That's what all the P( ) stuff is about. Buffer is big enough for 149 chars at once. "));
    client.println(P("Be careful not to exceed!<p><font color=red>
    This web server is not secured for public access. Use at your own risk.</font> "));
    client.println(P("If you want to use this in an actual product, 
    at least leave the gateway IP setting disabled. 
    You should consider a design where the Arduino acts"));
    client.println(P("as the client, or maybe a design where the Arduino 
    can only be contacted by a more fully-secured server.
    <p>Requests are echoed to the console"));
    client.println(P("with baud rate of 115200. Have fun!<p>
    <img src='https://cdn.meme.am/instances/250x250/54595677.jpg'/></body></html>"));
  }
  else if (strcmp(bufferUrl, P("test.html")) == 0)
  {
    // Requested: test.html
    
    // send response header
    sendHttpResponseOk(client);

    // send html page
    client.println(P("<HTML><head><title>Test Page</title>
    </head><body><h1>Test Page</h1>"));
    client.println(P("<br><b>Resource:</b> "));
    client.println(bufferUrl);
    client.println(P("<br><b>Arguments:</b> "));
    client.println(bufferArgs);
    // max length: ------------------------------------------------  (149 chars)
    client.println(P("<br><br><form action='/test.html?' 
    method='GET'>Test arguments: <input type=text name='arg1'/> 
    <input type=submit value='GET'/></form>"));
    client.println(P("</body></html>"));
  }
  else
  {
    // All other requests - 404 not found
    
    // send 404 not found header (oops)
    sendHttp404(client);
    client.println(P("<HTML><head><title>Resource not found
    </title></head><body><h1>The requested resource was not found</h1>"));
    client.println(P("<br><b>Resource:</b> "));
    client.println(bufferUrl);
    client.println(P("<br><b>Arguments:</b> "));
    client.println(bufferArgs);
    client.println(P("</body></html>"));
  }
}

// 200 OK means the resource was located on the server and the browser 
// (or service consumer) should expect a happy response
void sendHttpResponseOk(EthernetClient client)
{
  Serial.println(P("200 OK"));
  Serial.println();
  
  // send a standard http response header
  client.println(P("HTTP/1.1 200 OK"));
  client.println(P("Content-Type: text/html"));
  client.println(P("Connnection: close")); // do not reuse connection
  client.println();
}

// 404 means it ain't here. quit asking.
void sendHttp404(EthernetClient client)
{
  Serial.println(P("404 Not Found"));
  Serial.println();
  
  client.println(P("HTTP/1.1 404 Not Found"));
  client.println(P("Content-Type: text/html"));
  client.println(P("Connnection: close")); // do not reuse connection
  client.println();
}

示例输出

就是这样!

您可以读取或操作数字 IO 引脚,或者执行任何您通常会执行的操作。

内存使用说明

由于字符串会占用宝贵的 RAM 空间,我引用了一个宏,该宏将它们存储在 PROGMEM 中并从中检索。这使您能够为如此微小的处理器创建相当大的网页(真丢人!),并将它们存储在充足的闪存中,而不会牺牲所有 RAM。

权衡是您必须记住将每行文本限制在 149 个字符以内,否则读取 PROGMEM 中字符串的缓冲区将溢出,导致您的代码出现奇怪的错误。

如果您需要更多内存,请考虑 ATmega 或更强大的处理器,或 SD 卡(有其他适用于 Arduino 的 HTTP 实现使用此功能)。

历史

  • 2017 年 5 月 20 日:初始版本
© . All rights reserved.