9. 应用层协议

9.1 TCP Client

TCP Client主要是用来访问服务器,很多可以通过外网访问的物联网设备主要就是工作在TCP Client下。设备主动去访问外部的服务器,与服务器建立连接,用户的app也是去访问这个服务器,这样变相实现了用户对设备的访问。

9.1.1 使用说明

TCP Client按如下方式使用:

  1. 引用相关库#include <WiFi.h>。

  2. 连上网 。

  3. 声明WiFiClient对象用于连接服务器。

  4. 使用connect方法连接服务器。

  5. 进行数据读写通讯。

9.1.2 API 参考

connect() - 建立连接

int connect(IPAddress ip, uint16_t port)

int connect(IPAddress ip, uint16_t port, int32_t timeout)

int connect(const char *host, uint16_t port)

int connect(const char *host, uint16_t port, int32_t timeout)

用于和服务器建立连接,可以连接指定IP地址或是指定域名,连接成功返回1,失败返回0;

语法

WiFiClient client;
if (!client.connect(host, port)) {
    Serial.println("Connection failed.");
    Serial.println("Waiting 5 seconds before retrying...");
    delay(5000);
    return;
}

参数

传入值 说明 值范围
IPAddress ip IP地址
const char *host 指定域名
uint16_t port 端口号
int32_t timeout 超时,单位秒

返回

返回值 说明 值范围
int 连接成功返回1,失败返回0 1、0

connected() - 返回当前客户端是否与服务器建立连接

uint8_t connected()

语法

if(client.connected()){
    Serial.println("connected.");
}

返回

返回值 说明 值范围
uint8_t 服务器建立连接返回true true、false

write() - 发送数据

size_t write(uint8_t data)
size_t write(const uint8_t *buf, size_t size)
size_t write_P(PGM_P buf, size_t size)
size_t write(Stream &stream)

发送数据,发送成功返回发送字节数,失败返回0 除了用 write() 方法外也可以用print()等方法发送数据

语法

client.write();

参数

传入值 说明 值范围
uint8_t data 发送的数据
size_t size 数据长度

返回

返回值 说明 值范围
size_t 发送成功返回发送字节数,失败返回0

print() - 将数据打印到客户端连接到的服务器

println() - 打印数据,后跟回车符和换行符

client.print(data)
client.print(data, BASE)

将数据打印到客户端连接到的服务器。将数字打印为数字序列,每个数字为一个ASCII字符(例如,数字123作为三个字符“ 1”,“ 2”,“ 3”发送)。

语法

client.print("GET /index.html HTTP/1.1\n\n");

参数

传入值 说明 值范围
data 要打印的数据(char,byte,int,long或string)
BASE(可选) DEC代表十进制(基数10),OCT代表八进制(基数8),HEX代表十六进制(基数16)

返回

返回值 说明 值范围
返回的字节数写的,可选

available() - 返回可读取数据长度

int available()

返回可读取数据长度,如果没有数据可读取则返回0

语法

while (client.available()) {    
      // 读取一行服务器响应内容,并通过串口打印出来
      // read back one line from the server
      String line = client.readStringUntil('\r');
      Serial.print(line);
    }

返回

返回值 说明 值范围
int 返回可读取数据长度,如果没有数据可读取则返回0

read() - 从接收缓存读取数据并返回读取到的数据字节数

int read()
int read(uint8_t *buf, size_t size)

从接收缓存读取数据并返回读取到的数据字节数,如果返回-1则表示读取失败,读取过的数据会从接收缓存删除。

语法

client.read();

参数

传入值 说明 值范围
uint8_t *buf 读取缓存
size_t size 读取字节数

返回

返回值 说明 值范围
int 读取到的数据字节数,如果返回-1则表示读取失败

readStringUntil() - 将来自缓冲区的字符读取到数组中

stream.readStringUntil(terminator)
readStringUntil()

从流中读取字符到字符串。如果检测到终止符字符或该终止符超时,则该函数终止(请参见setTimeout())。

该函数是Stream类的一部分,并且可以从该类继承的任何类(Wire,Serial等)调用。有关更多信息,请参见Stream类主页。

语法

String line = client.readStringUntil('\r');

参数

传入值 说明 值范围
terminator 要搜索的字符。允许的数据类型:char。

返回

返回值 说明 值范围
String 从流中读取整个String,直至终止符 true、false

注意和警告 终止符将从流中丢弃。

stop() - 关闭客户端,释放资源

void stop()

语法

client.stop();

remoteIP() - 返回服务器IP地址

IPAddress remoteIP() const
IPAddress remoteIP(int fd) const

语法

Serial.println(client.remoteIP());

返回

返回值 说明 值范围
IPAddress 服务器IP地址

remotePort() - 返回服务器端口号

uint16_t remotePort() const
uint16_t remotePort(int fd) const

语法

Serial.println(client.remotePort());

返回

返回值 说明 值范围
uint16_t 服务器端口号

localIP() - 返回本地IP地址

IPAddress localIP() const
IPAddress localIP(int fd) const

语法

Serial.println(client.localIP());

返回

返回值 说明 值范围
IPAddress 本地IP地址

localPort() - 返回本地端口号

uint16_t localPort() const
uint16_t localPort(int fd) const

语法

Serial.println(client.localPort());

返回

返回值 说明 值范围
uint16_t 本地端口号

其他Stream类的方法 可参考串口部分的介绍

readString() - 读取字符串

peek() - 读取首字节数据,但并不从接收缓存中删除它

flush() - 清空当前接收缓存

parseInt() - 从流中查找第一个有效的整型数据

parseFloat() - 从缓冲区返回第一个有效的float型数据。

find() - 从缓冲区读取数据,直到读取到指定的字符串

findUntil() - 从缓冲区读取数据,直到读取到指定的字符串或指定的停止符

setTimeout() - 设置超时时间

例程:TCP Client 连接Server通信

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板;修改主板建立 TCP Client 去连接 PC 软件建立的一个 server ,如果连接成功 每5秒发送一次 “Hello World!” 字符串。

#include <WiFi.h>

const char *ssid = "your_ssid";
const char *password = "your_password";

const IPAddress serverIP(192,168,1,96); //欲访问的地址
uint16_t serverPort = 777;         //服务器端口号

WiFiClient client; //声明一个客户端对象,用于与服务器进行连接

void setup()
{
    Serial.begin(115200);
    Serial.println();

    WiFi.mode(WIFI_STA);
    WiFi.setSleep(false); //关闭STA模式下wifi休眠,提高响应速度
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED)
    {
        delay(500);
        Serial.print(".");
    }
    Serial.println("Connected");
    Serial.print("IP Address:");
    Serial.println(WiFi.localIP());
}

void loop()
{
    Serial.println("Trying to access the server"); // 尝试访问服务器
    if (client.connect(serverIP, serverPort)) //尝试访问目标地址
    {
        Serial.println("Access successful");	// 访问成功

        client.print("Hello world!");                    //向服务器发送数据
        while (client.connected() || client.available()) //如果已连接或有收到的未读取的数据
        {
            if (client.available()) //如果有数据可读取
            {
                String line = client.readStringUntil('\n'); //读取数据到换行符
                Serial.print("Read data:");	// 读取到数据
                Serial.println(line);
                client.write(line.c_str()); //将收到的数据回发
            }
        }
        Serial.println("Close current connection");  // 关闭当前连接
        client.stop(); //关闭客户端
    }
    else
    {
        Serial.println("Access loss");	// 访问失
        client.stop(); //关闭客户端
    }
    delay(5000);
}

下载免费的 Packet Sender 软件

打开软件 点击 File -> Settings ,点击 Enable TCP Servers 使能TCP服务器,TCP Server Port 设置一个固定端口号,点击右下方 OK按钮保存后退出设置。

image-20211220150814214

此时在软件界面可以看到 IP 地址和 TCP:777。

image-20211220151217493

将例程中的IP地址和端口号改为 Packet Sender 软件建立的 TCP Servers 的 IP 和 端口号,烧写代码到 Edge101WE 主板。

const IPAddress serverIP(192,168,1,96); //欲访问的地址
uint16_t serverPort = 777;         //服务器端口号

此时主板串口打印连接成功,主板的 IP地址为 192.168.0.221

.....Connected
IP Address:192.168.0.221
Trying to access the server
Access successful
Close current connection

此时可在 Packet Sender 软件上看到主板发送的数据。在发送对话框选中 Persistent TCP (持久TCP连接)

image-20211220160839286

此时收到 Edge101WE 主板发送的数据 “Hello world!”。

在对话框中输入 “Hello Edge101WE” 点击 Send按钮。

image-20211220161150353

此时在串口接收的数据如下,说明主板收到 Packet Sender 软件发送的字符串。

Trying to access the server
Access successful
Read data:Hello Edge101WE

例程:作为WEB TCP Client 向一个TCP服务器发送信息

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->WiFi\examples\WiFiClientBasic)

/*
    This sketch sends a message to a TCP server

*/

#include <WiFi.h>
#include <WiFiMulti.h>

WiFiMulti WiFiMulti;

void setup()
{
  Serial.begin(115200);
  delay(10);

  // We start by connecting to a WiFi network
  WiFiMulti.addAP("your_ssid", "your_password");

  Serial.println();
  Serial.println();
  Serial.print("Waiting for WiFi... ");

  while (WiFiMulti.run() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
  }

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());

  delay(500);
}


void loop()
{
  //    const uint16_t port = 80;
  //    const char * host = "192.168.1.1"; // ip or dns
  const uint16_t port = 80;  // 一般网站默认开放端口为80
  const char * host = "www.baidu.com"; // ip or dns,访问百度网站

  Serial.print("Connecting to ");
  Serial.println(host);
  // 用WiFiClient类创建TCP连接
  // Use WiFiClient class to create TCP connections
  WiFiClient client;
    
  // 连接服务器	
  if (!client.connect(host, port)) {
    Serial.println("Connection failed.");
    Serial.println("Waiting 5 seconds before retrying...");
    delay(5000);
    return;
  }
  // 将会向服务器发送请求    
  // This will send a request to the server
    
  // 取消下行注释,即可将任意字符串发送到服务器
  // uncomment this line to send an arbitrary string to the server
  // client.print("Send this data to the server");
  
  // 取消下行注释,发送一个基本的文档请求到服务器
  // uncomment this line to send a basic document request to the server
  client.print("GET /index.html HTTP/1.1\n\n");

  int maxloops = 0;
  // 等待服务器响应
  //wait for the server's reply to become available
  while (!client.available() && maxloops < 1000)
  {
    maxloops++;
    delay(1); //delay 1 msec
  }
  if (client.available() > 0)
  { 
    // 检测接收缓存区是否有内容
    while (client.available()) {    
      // 读取一行服务器响应内容,并通过串口打印出来
      // read back one line from the server
      String line = client.readStringUntil('\r');
      Serial.print(line);
    }
  }
  else
  {
    Serial.println("client.available() timed out ");
  }

  Serial.println("Closing connection.");
  client.stop();

  Serial.println("Waiting 5 seconds before restarting...");
  delay(5000);
}

串口终端打印结果

image-20210328104901131

9.1.3 WiFiClientSecure 客户端加密

WiFiClientSecure类使用TLS(SSL)实现对安全连接的支持。它继承自WiFiClient,因此实现了该类的接口的超集。使用WiFiClientSecure类建立安全连接的方法有三种:使用根证书颁发机构(CA)证书,使用根CA证书加上客户端证书和密钥以及使用预共享密钥(PSK)。

使用根证书颁发机构证书

此方法对服务器进行身份验证并协商加密的连接。连接到HTTPS站点时,它与在Web浏览器中实现的功能相同。

如果要访问自己的服务器:

  • 为您自己的证书颁发机构生成根证书

  • 使用您的服务器的根证书(“自签名证书”)生成证书和私钥

如果要访问公共服务器:

  • 获取签署该服务器证书的公共CA的证书,然后:

  • 在WiFiClientSecure中,使用setCACert(或适当的连接方法)来设置您的CA或公共CA的根证书。

  • 当WiFiClientSecure连接到目标服务器时,它使用CA证书来验证服务器提供的证书,然后协商连接的加密

请参阅WiFiClientSecure示例。

使用根CA证书和客户端证书/密钥

此方法对服务器进行身份验证,此外还向服务器对客户端进行身份验证,然后协商加密连接。

  • 请按照上述步骤

  • 使用您的根CA为您的客户端生成证书/密钥

  • 在您将要访问的服务器上注册密钥,以便服务器可以对您的客户端进行身份验证

  • 在WiFiClientSecure中,使用setCACert(或适当的连接方法)设置您的CA或公共CA的根证书,这用于对服务器进行身份验证

  • 在WiFiClientSecure中,使用setCertificate和setPrivateKey(或适当的connect方法)设置客户端的证书和密钥,这将用于向服务器验证客户端的身份

  • 当WiFiClientSecure连接到目标服务器时,它将使用CA证书来验证服务器提供的证书,它将使用证书/密钥来向服务器验证您的客户端,然后将协商连接的加密。

使用预共享密钥(PSK)

TLS支持使用预共享密钥(即,客户端和服务器都知道的密钥)作为HTTPS上网络上常用的公共密钥加密的替代方法来进行身份验证和加密。PSK已开始用于MQTT(例如在mosquitto中),以简化设置并避免必须经历整个CA,证书和私钥过程。

预共享密钥是最多32个字节的二进制字符串,通常以十六进制形式表示。除了密钥之外,客户端还可以提供ID,通常,服务器允许将不同的密钥与每个客户端ID关联。实际上,这与用户名和密码对非常相似,除了与密码不同,密钥不会直接传输到服务器,因此与恶意服务器的连接不会泄露密码。另外,服务器还向客户端进行了身份验证。

要使用PSK,请执行以下操作:

  • 生成随机的十六进制字符串(为某些文件生成MD5或SHA是这样做的一种方法)

  • 为您的客户端提供一个字符串ID,并将您的服务器配置为接受ID /密钥对

  • 在WiFiClientSecure中,使用setPreSharedKey(或适当的连接方法)来设置ID /密钥组合

  • 当WiFiClientSecure连接到目标服务器时,它使用id / key组合对服务器进行身份验证(必须证明它也具有密钥),对客户端进行身份验证,然后协商连接的加密

API参考

connect() - 与指定主机的端口建立TCP连接
#include <WiFiClientSecure.h>

int WiFiClientSecure::connect(IPAddress ip, uint16_t port);
int WiFiClientSecure::connect(IPAddress ip, uint16_t port, int32_t timeout);
int WiFiClientSecure::connect(const char *host, uint16_t port);
int WiFiClientSecure::connect(const char *host, uint16_t port, int32_t timeout);
int WiFiClientSecure::connect(IPAddress ip, uint16_t port, const char *CA_cert, const char *cert, const char *private_key);
int WiFiClientSecure::connect(const char *host, uint16_t port, const char *CA_cert, const char *cert, const char *private_key);
int WiFiClientSecure::connect(IPAddress ip, uint16_t port, const char *pskIdent, const char *psKey);
int WiFiClientSecure::connect(const char *host, uint16_t port, const char *pskIdent, const char *psKey);

语法

// 连接到指定主机上的指定端口。connect() 成功返回1
  if (!client.connect(server, 443))
    Serial.println("Connection failed!");
  else {
    Serial.println("Connected to server!");

参数

传入值 说明 值范围
IPAddress ip IP地址
uint16_t port 端口号
int32_t timeout 超时时间(单位:毫秒)
const char *host 主机名
const char *CA_cert 根证书
const char *cert 客户端证书
const char *private_key 客户端证书的私钥
char *pskIdent PSK身份
const char *psKey 预共享密钥

返回

返回值 说明 值范围
int 如果连接成功,则为1;如果连接失败,则为0。 0,1
setInsecure() - 通过HTTPS连接时,请勿执行证书验证

语法

#include <WiFiClientSecure.h>

void WiFiClientSecure::setInsecure();

参数

返回

setCACert() - 设置访问目标主机的根证书

语法

#include <WiFiClientSecure.h>

void WiFiClientSecure::setCACert (const char *rootCA);

参数

传入值 说明 值范围
const char *rootCA 根证书

返回

connected() - 返回与服务器的连接状态

语法

#include <WiFiClientSecure.h>

uint8_t WiFiClientSecure::connected();

参数

返回

返回值 说明 值范围
uint8_t 连接状态。连接时为true,断开连接时为false。 true、false
available() - 获取流中可用的字节数

语法

#include <WiFiClientSecure.h>

int WiFiClientSecure::available();

参数

返回

返回值 说明 值范围
int 可以读取的字节数
read() - 从流中读取数据

对于未设置参数的调用,将读取一个字符。如果发生错误,请调用stop()断开与主机的TCP连接。

语法

#include <WiFiClientSecure.h>

int WiFiClientSecure::read();

int WiFiClientSecure::read(uint8_t *buf, size_t size);

参数

传入值 说明 值范围
uint8_t *buf 读取缓冲区
size_t size 缓冲区大小

返回

返回值 说明 值范围
int 读取的字节数。发生错误时为负数。
stop() - 断开与主机的TCP连接

语法

#include <WiFiClientSecure.h>

void WiFiClientSecure::stop();

参数

返回

例程:WiFiClientSecure 证书认证

实现 Edge101WE 主板通过HTTPS协议与www.howsmyssl.com网站服务器进行通讯(该网站专门用于HTTPS通讯测试)。服务器响应信息将会通过串口监视器显示以便我们查阅。例程使用证书认证可以提高HTTPS通讯安全性。

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->WiFiClientSecure\examples\WiFiClientSecure)

/*
  Wifi secure connection example for ESP32
  Running on TLS 1.2 using mbedTLS
  Suporting the following chipersuites:
  "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384","TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384","TLS_DHE_RSA_WITH_AES_256_GCM_SHA384","TLS_ECDHE_ECDSA_WITH_AES_256_CCM","TLS_DHE_RSA_WITH_AES_256_CCM","TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384","TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384","TLS_DHE_RSA_WITH_AES_256_CBC_SHA256","TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA","TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA","TLS_DHE_RSA_WITH_AES_256_CBC_SHA","TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8","TLS_DHE_RSA_WITH_AES_256_CCM_8","TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_GCM_SHA384","TLS_ECDHE_RSA_WITH_CAMELLIA_256_GCM_SHA384","TLS_DHE_RSA_WITH_CAMELLIA_256_GCM_SHA384","TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384","TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384","TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256","TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA","TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256","TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256","TLS_DHE_RSA_WITH_AES_128_GCM_SHA256","TLS_ECDHE_ECDSA_WITH_AES_128_CCM","TLS_DHE_RSA_WITH_AES_128_CCM","TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256","TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256","TLS_DHE_RSA_WITH_AES_128_CBC_SHA256","TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA","TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA","TLS_DHE_RSA_WITH_AES_128_CBC_SHA","TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8","TLS_DHE_RSA_WITH_AES_128_CCM_8","TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_GCM_SHA256","TLS_ECDHE_RSA_WITH_CAMELLIA_128_GCM_SHA256","TLS_DHE_RSA_WITH_CAMELLIA_128_GCM_SHA256","TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_CBC_SHA256","TLS_ECDHE_RSA_WITH_CAMELLIA_128_CBC_SHA256","TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256","TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA","TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA","TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA","TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA","TLS_DHE_PSK_WITH_AES_256_GCM_SHA384","TLS_DHE_PSK_WITH_AES_256_CCM","TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384","TLS_DHE_PSK_WITH_AES_256_CBC_SHA384","TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA","TLS_DHE_PSK_WITH_AES_256_CBC_SHA","TLS_DHE_PSK_WITH_CAMELLIA_256_GCM_SHA384","TLS_ECDHE_PSK_WITH_CAMELLIA_256_CBC_SHA384","TLS_DHE_PSK_WITH_CAMELLIA_256_CBC_SHA384","TLS_PSK_DHE_WITH_AES_256_CCM_8","TLS_DHE_PSK_WITH_AES_128_GCM_SHA256","TLS_DHE_PSK_WITH_AES_128_CCM","TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256","TLS_DHE_PSK_WITH_AES_128_CBC_SHA256","TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA","TLS_DHE_PSK_WITH_AES_128_CBC_SHA","TLS_DHE_PSK_WITH_CAMELLIA_128_GCM_SHA256","TLS_DHE_PSK_WITH_CAMELLIA_128_CBC_SHA256","TLS_ECDHE_PSK_WITH_CAMELLIA_128_CBC_SHA256","TLS_PSK_DHE_WITH_AES_128_CCM_8","TLS_ECDHE_PSK_WITH_3DES_EDE_CBC_SHA","TLS_DHE_PSK_WITH_3DES_EDE_CBC_SHA","TLS_RSA_WITH_AES_256_GCM_SHA384","TLS_RSA_WITH_AES_256_CCM","TLS_RSA_WITH_AES_256_CBC_SHA256","TLS_RSA_WITH_AES_256_CBC_SHA","TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384","TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384","TLS_ECDH_RSA_WITH_AES_256_CBC_SHA","TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384","TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384","TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA","TLS_RSA_WITH_AES_256_CCM_8","TLS_RSA_WITH_CAMELLIA_256_GCM_SHA384","TLS_RSA_WITH_CAMELLIA_256_CBC_SHA256","TLS_RSA_WITH_CAMELLIA_256_CBC_SHA","TLS_ECDH_RSA_WITH_CAMELLIA_256_GCM_SHA384","TLS_ECDH_RSA_WITH_CAMELLIA_256_CBC_SHA384","TLS_ECDH_ECDSA_WITH_CAMELLIA_256_GCM_SHA384","TLS_ECDH_ECDSA_WITH_CAMELLIA_256_CBC_SHA384","TLS_RSA_WITH_AES_128_GCM_SHA256","TLS_RSA_WITH_AES_128_CCM","TLS_RSA_WITH_AES_128_CBC_SHA256","TLS_RSA_WITH_AES_128_CBC_SHA","TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256","TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256","TLS_ECDH_RSA_WITH_AES_128_CBC_SHA","TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256","TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256","TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA","TLS_RSA_WITH_AES_128_CCM_8","TLS_RSA_WITH_CAMELLIA_128_GCM_SHA256","TLS_RSA_WITH_CAMELLIA_128_CBC_SHA256","TLS_RSA_WITH_CAMELLIA_128_CBC_SHA","TLS_ECDH_RSA_WITH_CAMELLIA_128_GCM_SHA256","TLS_ECDH_RSA_WITH_CAMELLIA_128_CBC_SHA256","TLS_ECDH_ECDSA_WITH_CAMELLIA_128_GCM_SHA256","TLS_ECDH_ECDSA_WITH_CAMELLIA_128_CBC_SHA256","TLS_RSA_WITH_3DES_EDE_CBC_SHA","TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA","TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA","TLS_RSA_PSK_WITH_AES_256_GCM_SHA384","TLS_RSA_PSK_WITH_AES_256_CBC_SHA384","TLS_RSA_PSK_WITH_AES_256_CBC_SHA","TLS_RSA_PSK_WITH_CAMELLIA_256_GCM_SHA384","TLS_RSA_PSK_WITH_CAMELLIA_256_CBC_SHA384","TLS_RSA_PSK_WITH_AES_128_GCM_SHA256","TLS_RSA_PSK_WITH_AES_128_CBC_SHA256","TLS_RSA_PSK_WITH_AES_128_CBC_SHA","TLS_RSA_PSK_WITH_CAMELLIA_128_GCM_SHA256","TLS_RSA_PSK_WITH_CAMELLIA_128_CBC_SHA256","TLS_RSA_PSK_WITH_3DES_EDE_CBC_SHA","TLS_PSK_WITH_AES_256_GCM_SHA384","TLS_PSK_WITH_AES_256_CCM","TLS_PSK_WITH_AES_256_CBC_SHA384","TLS_PSK_WITH_AES_256_CBC_SHA","TLS_PSK_WITH_CAMELLIA_256_GCM_SHA384","TLS_PSK_WITH_CAMELLIA_256_CBC_SHA384","TLS_PSK_WITH_AES_256_CCM_8","TLS_PSK_WITH_AES_128_GCM_SHA256","TLS_PSK_WITH_AES_128_CCM","TLS_PSK_WITH_AES_128_CBC_SHA256","TLS_PSK_WITH_AES_128_CBC_SHA","TLS_PSK_WITH_CAMELLIA_128_GCM_SHA256","TLS_PSK_WITH_CAMELLIA_128_CBC_SHA256","TLS_PSK_WITH_AES_128_CCM_8","TLS_PSK_WITH_3DES_EDE_CBC_SHA","TLS_EMPTY_RENEGOTIATION_INFO_SCSV"]
  2017 - Evandro Copercini - Apache 2.0 License.
*/

#include <WiFiClientSecure.h>

const char* ssid     = "your_ssid";     // your network SSID (name of wifi network)
const char* password = "your_password"; // your network password

const char*  server = "www.howsmyssl.com";  // Server URL

// www.howsmyssl.com root certificate authority, to verify the server
// change it to your server root CA
// SHA1 fingerprint is broken now!

// Web服务器的根证书
// 注意:出于安全原因,CA会定期更新根证书信息。因此本程序中的证书可能已经过期。
// 请使用浏览器获取最新的网站根证书并复制粘贴到程序中相应位置。如需了解如何执行这一操作,“查看网站根证书”页面。
const char* test_root_ca= \
     "-----BEGIN CERTIFICATE-----\n" \
     "MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/\n" \
     "MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT\n" \
     "DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow\n" \
     "PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD\n" \
     "Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\n" \
     "AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O\n" \
     "rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq\n" \
     "OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b\n" \
     "xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw\n" \
     "7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD\n" \
     "aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV\n" \
     "HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG\n" \
     "SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69\n" \
     "ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr\n" \
     "AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz\n" \
     "R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5\n" \
     "JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo\n" \
     "Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ\n" \
     "-----END CERTIFICATE-----\n";

// You can use x.509 client certificates if you want
//const char* test_client_key = "";   //to verify the client
//const char* test_client_cert = "";  //to verify the client


WiFiClientSecure client;  // client 是 WiFiClientSecure 类型变量,通过HTTPS连接时使用。

void setup() {
  //Initialize serial and wait for port to open:
  Serial.begin(115200);
  delay(100);

  Serial.print("Attempting to connect to SSID: ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);  // 连接到ssid指定的访问点。此时的密码是密码。WiFi是WiFi.cpp中定义的变量

  // attempt to connect to Wifi network:
  // 返回当前的连接状态。连接到接入点时,将返回WL_CONNECTED  
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    // wait 1 second for re-trying
    delay(1000);
  }

  Serial.print("Connected to ");
  Serial.println(ssid);

  client.setCACert(test_root_ca);  // 设置Web服务器的根证书
  //client.setCertificate(test_client_cert); // for client verification
  //client.setPrivateKey(test_client_key);	// for client verification

  Serial.println("\nStarting connection to server...");
  // 连接到指定主机上的指定端口。connect() 成功返回1
  if (!client.connect(server, 443))
    Serial.println("Connection failed!");
  else {
    Serial.println("Connected to server!");
    // Make a HTTP request:
    // 编写一个HTTP请求。由于WiFiClientSecure继承了Print类,因此您可以使用诸如Serial之类的println() 等  
    client.println("GET https://www.howsmyssl.com/a/check HTTP/1.0");
    client.println("Host: www.howsmyssl.com");
    client.println("Connection: close");
    client.println();
	// 返回与服务器的连接状态。连接后,在while语句中执行
    while (client.connected()) {
      String line = client.readStringUntil('\n');  // WiFiClientSecure也继承自Stream类,因此您可以使用readStringUntil
      if (line == "\r") {
        Serial.println("headers received");
        break;
      }
    }
    // if there are incoming bytes available
    // from the server, read them and print them:
    // 返回可以读取的字节数
    while (client.available()) {
      char c = client.read();  //  从服务器读取一个字符
      Serial.write(c);
    }

    client.stop(); // 与服务器断开连接
  }
}

void loop() {
  // do nothing
}

串口返回

Attempting to connect to SSID: dfrobotOffice
.....Connected to dfrobotOffice

Starting connection to server...
Connected to server!
headers received
{"given_cipher_suites":["TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384","TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384","TLS_DHE_RSA_WITH_AES_256_GCM_SHA384","TLS_ECDHE_ECDSA_WITH_AES_256_CCM","TLS_DHE_RSA_WITH_AES_256_CCM","TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384","TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384","TLS_DHE_RSA_WITH_AES_256_CBC_SHA256","TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA","TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA","TLS_DHE_RSA_WITH_AES_256_CBC_SHA","TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8","TLS_DHE_RSA_WITH_AES_256_CCM_8","TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256","TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256","TLS_DHE_RSA_WITH_AES_128_GCM_SHA256","TLS_ECDHE_ECDSA_WITH_AES_128_CCM","TLS_DHE_RSA_WITH_AES_128_CCM","TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256","TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256","TLS_DHE_RSA_WITH_AES_128_CBC_SHA256","TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA","TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA","TLS_DHE_RSA_WITH_AES_128_CBC_SHA","TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8","TLS_DHE_RSA_WITH_AES_128_CCM_8","TLS_DHE_PSK_WITH_AES_256_GCM_SHA384","TLS_DHE_PSK_WITH_AES_256_CCM","TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384","TLS_DHE_PSK_WITH_AES_256_CBC_SHA384","TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA","TLS_DHE_PSK_WITH_AES_256_CBC_SHA","TLS_PSK_DHE_WITH_AES_256_CCM_8","TLS_DHE_PSK_WITH_AES_128_GCM_SHA256","TLS_DHE_PSK_WITH_AES_128_CCM","TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256","TLS_DHE_PSK_WITH_AES_128_CBC_SHA256","TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA","TLS_DHE_PSK_WITH_AES_128_CBC_SHA","TLS_PSK_DHE_WITH_AES_128_CCM_8","TLS_RSA_WITH_AES_256_GCM_SHA384","TLS_RSA_WITH_AES_256_CCM","TLS_RSA_WITH_AES_256_CBC_SHA256","TLS_RSA_WITH_AES_256_CBC_SHA","TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384","TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384","TLS_ECDH_RSA_WITH_AES_256_CBC_SHA","TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384","TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384","TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA","TLS_RSA_WITH_AES_256_CCM_8","TLS_RSA_WITH_AES_128_GCM_SHA256","TLS_RSA_WITH_AES_128_CCM","TLS_RSA_WITH_AES_128_CBC_SHA256","TLS_RSA_WITH_AES_128_CBC_SHA","TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256","TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256","TLS_ECDH_RSA_WITH_AES_128_CBC_SHA","TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256","TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256","TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA","TLS_RSA_WITH_AES_128_CCM_8","TLS_RSA_PSK_WITH_AES_256_GCM_SHA384","TLS_RSA_PSK_WITH_AES_256_CBC_SHA384","TLS_RSA_PSK_WITH_AES_256_CBC_SHA","TLS_RSA_PSK_WITH_AES_128_GCM_SHA256","TLS_RSA_PSK_WITH_AES_128_CBC_SHA256","TLS_RSA_PSK_WITH_AES_128_CBC_SHA","TLS_PSK_WITH_AES_256_GCM_SHA384","TLS_PSK_WITH_AES_256_CCM","TLS_PSK_WITH_AES_256_CBC_SHA384","TLS_PSK_WITH_AES_256_CBC_SHA","TLS_PSK_WITH_AES_256_CCM_8","TLS_PSK_WITH_AES_128_GCM_SHA256","TLS_PSK_WITH_AES_128_CCM","TLS_PSK_WITH_AES_128_CBC_SHA256","TLS_PSK_WITH_AES_128_CBC_SHA","TLS_PSK_WITH_AES_128_CCM_8","TLS_EMPTY_RENEGOTIATION_INFO_SCSV"],"ephemeral_keys_supported":true,"session_ticket_supported":true,"tls_compression_supported":false,"unknown_cipher_suite_supported":false,"beast_vuln":false,"able_to_detect_n_minus_one_splitting":false,"insecure_cipher_suites":{},"tls_version":"TLS 1.2","rating":"Probably Okay"}⸮

例程:WiFiClientInsecure 通过HTTPS连接时,跳过执行证书验证

使用 setInsecure 将会让主板不进行服务器身份认证,而直接与服务器进行通讯。注意:此方法跳过了 HTTPS 协议中的安全加密措施,因此仅可用于测试使用,而不适合传输需要保密的信息。

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->WiFiClientSecure\examples\WiFiClientInsecure)

#include <WiFiClientSecure.h>

const char* ssid     = "your-ssid";     // your network SSID (name of wifi network)
const char* password = "your-password"; // your network password

const char*  server = "www.howsmyssl.com";  // Server URL

WiFiClientSecure client;

void setup() {
  //Initialize serial and wait for port to open:
  Serial.begin(115200);
  delay(100);

  Serial.print("Attempting to connect to SSID: ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);

  // attempt to connect to Wifi network:
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    // wait 1 second for re-trying
    delay(1000);
  }

  Serial.print("Connected to ");
  Serial.println(ssid);

  Serial.println("\nStarting connection to server...");
  // 通过HTTPS连接时,请勿执行证书验证  
  client.setInsecure();//skip verification
    
  if (!client.connect(server, 443))
    Serial.println("Connection failed!");
  else {
    Serial.println("Connected to server!");
    // Make a HTTP request:
    client.println("GET https://www.howsmyssl.com/a/check HTTP/1.0");
    client.println("Host: www.howsmyssl.com");
    client.println("Connection: close");
    client.println();

    while (client.connected()) {
      String line = client.readStringUntil('\n');
      if (line == "\r") {
        Serial.println("headers received");
        break;
      }
    }
    // if there are incoming bytes available
    // from the server, read them and print them:
    while (client.available()) {
      char c = client.read();
      Serial.write(c);
    }

    client.stop();
  }
}

void loop() {
  // do nothing
}

串口输出

Attempting to connect to SSID: dfrobotOffice
...Connected to dfrobotOffice

Starting connection to server...
Connected to server!
headers received
{"given_cipher_suites":["TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384","TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384","TLS_DHE_RSA_WITH_AES_256_GCM_SHA384","TLS_ECDHE_ECDSA_WITH_AES_256_CCM","TLS_DHE_RSA_WITH_AES_256_CCM","TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384","TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384","TLS_DHE_RSA_WITH_AES_256_CBC_SHA256","TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA","TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA","TLS_DHE_RSA_WITH_AES_256_CBC_SHA","TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8","TLS_DHE_RSA_WITH_AES_256_CCM_8","TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256","TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256","TLS_DHE_RSA_WITH_AES_128_GCM_SHA256","TLS_ECDHE_ECDSA_WITH_AES_128_CCM","TLS_DHE_RSA_WITH_AES_128_CCM","TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256","TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256","TLS_DHE_RSA_WITH_AES_128_CBC_SHA256","TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA","TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA","TLS_DHE_RSA_WITH_AES_128_CBC_SHA","TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8","TLS_DHE_RSA_WITH_AES_128_CCM_8","TLS_DHE_PSK_WITH_AES_256_GCM_SHA384","TLS_DHE_PSK_WITH_AES_256_CCM","TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384","TLS_DHE_PSK_WITH_AES_256_CBC_SHA384","TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA","TLS_DHE_PSK_WITH_AES_256_CBC_SHA","TLS_PSK_DHE_WITH_AES_256_CCM_8","TLS_DHE_PSK_WITH_AES_128_GCM_SHA256","TLS_DHE_PSK_WITH_AES_128_CCM","TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256","TLS_DHE_PSK_WITH_AES_128_CBC_SHA256","TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA","TLS_DHE_PSK_WITH_AES_128_CBC_SHA","TLS_PSK_DHE_WITH_AES_128_CCM_8","TLS_RSA_WITH_AES_256_GCM_SHA384","TLS_RSA_WITH_AES_256_CCM","TLS_RSA_WITH_AES_256_CBC_SHA256","TLS_RSA_WITH_AES_256_CBC_SHA","TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384","TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384","TLS_ECDH_RSA_WITH_AES_256_CBC_SHA","TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384","TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384","TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA","TLS_RSA_WITH_AES_256_CCM_8","TLS_RSA_WITH_AES_128_GCM_SHA256","TLS_RSA_WITH_AES_128_CCM","TLS_RSA_WITH_AES_128_CBC_SHA256","TLS_RSA_WITH_AES_128_CBC_SHA","TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256","TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256","TLS_ECDH_RSA_WITH_AES_128_CBC_SHA","TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256","TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256","TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA","TLS_RSA_WITH_AES_128_CCM_8","TLS_RSA_PSK_WITH_AES_256_GCM_SHA384","TLS_RSA_PSK_WITH_AES_256_CBC_SHA384","TLS_RSA_PSK_WITH_AES_256_CBC_SHA","TLS_RSA_PSK_WITH_AES_128_GCM_SHA256","TLS_RSA_PSK_WITH_AES_128_CBC_SHA256","TLS_RSA_PSK_WITH_AES_128_CBC_SHA","TLS_PSK_WITH_AES_256_GCM_SHA384","TLS_PSK_WITH_AES_256_CCM","TLS_PSK_WITH_AES_256_CBC_SHA384","TLS_PSK_WITH_AES_256_CBC_SHA","TLS_PSK_WITH_AES_256_CCM_8","TLS_PSK_WITH_AES_128_GCM_SHA256","TLS_PSK_WITH_AES_128_CCM","TLS_PSK_WITH_AES_128_CBC_SHA256","TLS_PSK_WITH_AES_128_CBC_SHA","TLS_PSK_WITH_AES_128_CCM_8","TLS_EMPTY_RENEGOTIATION_INFO_SCSV"],"ephemeral_keys_supported":true,"session_ticket_supported":true,"tls_compression_supported":false,"unknown_cipher_suite_supported":false,"beast_vuln":false,"able_to_detect_n_minus_one_splitting":false,"insecure_cipher_suites":{},"tls_version":"TLS 1.2","rating":"Probably Okay"}⸮

9.2 TCP Server

9.2.1 使用说明

TCP Server按如下方式使用:

  1. 引用相关库#include <WiFi.h>

  2. 声明WiFiServer对象

  3. 使用begin方法启动监听

  4. 监听客户端连接并处理数据通讯;

9.2.2 常用方法

WiFiServer() - 声明WiFiServer对象

WiFiServer(uint16_t port=80, uint8_t max_clients=4)

在声明WiFiServer对象可以选择输入要监听的端口号和最大接入客户数量。

语法

#include <WiFi.h>
WiFiServer server; // 声明服务器对象

参数

传入值 说明 值范围
port 服务器使用的端口号
max_clients 最大客户端数,默认值 4。 1~4

返回

begin() - 服务器启动监听

void begin(uint16_t port=0)

语法

server.begin(55555); // 服务器启动监听端口号55555

参数

传入值 说明 值范围
服务器使用的端口号。如果省略,则使用构造函数中指定的值。
指定重用本地地址的权限。

返回

available() - 尝试建立客户对象

WiFiClient available()
WiFiClient accept(){return available();}

语法

WiFiClient client = server.available(); // 尝试建立客户对象
if (client) // 如果当前客户可用
{
    
}

参数

返回

返回值 说明 值范围
返回与客户端的连接的 WiFi 客户端类型信息

setNoDelay() - 设置是否延时发送

void setNoDelay(bool nodelay)

此函数用于设置 server 是否使用Nagle算法来将发送的信息先拆分成小包再发送。

使用Nagle算法来拆包发送的优点是可以将较大的数据信息拆分,从而让信息传输的网络利用率更加优化。但缺点是比起不拆包发送的模式来说,拆包发送的速度要慢一些。

语法

server.setNoDelay(true);  // 停止小包合并发送

参数

传入值 说明 值范围
nodelay true: 不使用Nagle算法来将发送的信息先拆分成小包再发送。
false: 使用Nagle算法来将发送的信息先拆分成小包再发送。

返回

getNoDelay() - 返回是否延时发送

bool getNoDelay()

语法

status = client.getNoDelay()

参数

返回

返回值 说明 值范围
bool true:禁用延迟发送

hasClient() - 返回是否有客户端尝试接入

bool hasClient()

检查是否有客户端访问主板建立的网络服务器。

语法

if (server.hasClient()) {}

参数

返回

返回值 说明 值范围
bool true:有client连接server
false:无client连接server

write() - 发送数据

size_t write(const uint8_t *data, size_t len)
size_t write(uint8_t data){return write(&data, 1);}

发送数据,发送成功返回发送字节数,失败返回0。 除了用 write() 方法外也可以用 print() 等方法发送数据。

语法

uint8_t sbuf;
serverClient.wrte(sbuf,len);

参数

传入值 说明 值范围
const uint8_t *data 指向数组的指针
uint8_t data 数据
size_t len 数据长度

返回

返回值 说明 值范围
size_t 发送成功返回发送字节数,失败返回0

setTimeout() - 设置超时时间

int setTimeout(uint32_t seconds)

语法

client.setTimeout(5); 

参数

传入值 说明 值范围
uint32_t seconds 设备等待数据流的最大时间,单位

返回

返回值 说明 值范围
int

stop() - 停止当前监听

void stop() 

语法

server.stop();

参数

返回

例程:建立TCP Server 等待TCP Client连接通信

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

#include <WiFi.h>

const char *ssid = "your_ssid";
const char *password = "your_password";

WiFiServer server; // 声明服务器对象

void setup()
{
    Serial.begin(115200);
    Serial.println();

    WiFi.mode(WIFI_STA);
    WiFi.setSleep(false); //关闭STA模式下wifi休眠,提高响应速度
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED)
    {
        delay(500);
        Serial.print(".");
    }
    Serial.println("Connected");
    Serial.print("IP Address:");
    Serial.println(WiFi.localIP());

    server.begin(55555); //服务器启动监听端口号55555
}

void loop()
{
    WiFiClient client = server.available(); //尝试建立客户对象
    if (client) //如果当前客户可用
    {
        Serial.println("[Client connected]");
        String readBuff;
        while (client.connected()) //如果客户端处于连接状态
        {
            if (client.available()) //如果有可读数据
            {
                char c = client.read(); //读取一个字节
                                        //也可以用readLine()等其他方法
                readBuff += c;
                if(c == '\r') //接收到回车符
                {
                    client.print("Received: " + readBuff); //向客户端发送
                    Serial.println("Received: " + readBuff); //从串口打印
                    readBuff = "";
                }
            }
        }
        client.stop(); //结束当前连接:
        Serial.println("[Client disconnected]");
    }
}

打开串口终端,将主板复位,串口将打印出主板TCP Server的IP地址。使用 Packet Sender 软件,发送区 Address 设置为 串口终端打印出来的地址,填写程序设定的端口号 55555,协议选择TCP点击发送。

此时在串口中断可看到主板 TCP Server收到相同的数据。

...........Connected
IP Address:192.168.0.221
[Client connected]
Received: Edge101WE

在 Packet Sender 软件建立的TCP Client上也可接收到主板 TCP Server 回发的数据。

image-20211220164713279

例程:SimpleWiFiServer - 网页控制LED灯

例程通过 STA 方式联网后建立一个简单的服务端,例程将打印主板IP地址到串行监视器。 你可以在浏览器中访问该ip地址来打开和关闭LED。

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

注意:需要修改例程中的 LED 引脚为 15

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->WiFi/examples/SimpleWiFiServe)

/*
 WiFi Web Server LED Blink
 A simple web server that lets you blink an LED via the web.
 This sketch will print the IP address of your WiFi Shield (once connected)
 to the Serial monitor. From there, you can open that address in a web browser
 to turn on and off the LED on pin 15.
 If the IP address of your shield is yourAddress:
 http://yourAddress/H turns the LED on
 http://yourAddress/L turns it off
 This example is written for a network using WPA encryption. For
 WEP or WPA, change the Wifi.begin() call accordingly.
 Circuit:
 * WiFi shield attached
 * LED attached to pin 15
 created for arduino 25 Nov 2012
 by Tom Igoe
ported for sparkfun esp32 
31.01.2017 by Jan Hendrik Berlin
 
 */

#include <WiFi.h>

const char* ssid     = "your_ssid";
const char* password = "your_password";

WiFiServer server(80);

void setup()
{
    Serial.begin(115200);
    pinMode(15, OUTPUT);      // set the LED pin mode

    delay(10);

    // We start by connecting to a WiFi network

    Serial.println();
    Serial.println();
    Serial.print("Connecting to ");
    Serial.println(ssid);

    WiFi.begin(ssid, password);

    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }

    Serial.println("");
    Serial.println("WiFi connected.");
    Serial.println("IP address: ");
    Serial.println(WiFi.localIP());
    
    server.begin();

}

int value = 0;

void loop(){
    
 // 监听新连接的客户端
 WiFiClient client = server.available();   // listen for incoming clients
  if (client) {                             // if you get a client(如果有客户端连接)
    Serial.println("New Client.");           // print a message out the serial port(打印信息到串口)
    // 创建一个字符串来保存来自客户端的传入数据  
    String currentLine = "";                // make a String to hold incoming data from the client
    while (client.connected()) {            // loop while the client's connected (当客户端连接时保持循环)
      // 如果有来自客户端的数据可以读取
      if (client.available()) {             // if there's bytes to read from the client          
        char c = client.read();             // read a byte, then (读取一个字节)
        Serial.write(c);                    // print it out the serial monitor (打印到串口)
        if (c == '\n') {                    // if the byte is a newline character (如果是换行符)
          // 如果当前行为空,在一行中会有两个换行符。
          // 这是客户端HTrP请求的结束标志,所以发送一个响应:  
          // if the current line is blank, you got two newline characters in a row.
          // that's the end of the client HTTP request, so send a response:
          if (currentLine.length() == 0) {
            // HTTP 头经常以一串响应代码开始,即(e.g. HTTP/1.1 200 OK)
            // 然后是一个内容类型,这样客户就知道收到了什么,然后是一个空白行:
            // HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK)
            // and a content-type so the client knows what's coming, then a blank line:
            client.println("HTTP/1.1 200 OK");
            client.println("Content-type:text/html");
            client.println();
			// 紧接着http响应的内容是:	
            // the content of the HTTP response follows the header:
            client.print("Click <a href=\"/H\">here</a> to turn the LED on pin 0 on.<br>");
            client.print("Click <a href=\"/L\">here</a> to turn the LED on pin 0 off.<br>");
			// HTTP响应以另一个空行结束	
            // The HTTP response ends with another blank line:
            client.println();
            //跳出循环
            // break out of the while loop:
            break;
          } else {    // if you got a newline, then clear currentLine:(如果你收到了新行,则清空currentLine:)
            currentLine = "";
          }
        } else if (c != '\r') {  // if you got anything else but a carriage return character,
           
           // 如果收到除回车符以外的其他字符,将其加到currentLine的末尾
           currentLine += c;      // add it to the end of the currentLine
        }
		// 检查来自客户端的请求是否是"GET /H" 或者 "GET /L":
        // Check to see if the client request was "GET /H" or "GET /L":
        if (currentLine.endsWith("GET /H")) {
          digitalWrite(15, HIGH);               // GET /H turns the LED on
        }
        if (currentLine.endsWith("GET /L")) {
          digitalWrite(15, LOW);                // GET /L turns the LED off
        }
      }
    }
    // 关闭连接  
    // close the connection:
    client.stop();
    Serial.println("Client Disconnected.");
  }
}

打开串口终端,将主板复位,串口将打印出主板TCP Server的IP地址,设置的端口号是80端口。一般80作为网页服务器的默认访问端口。

Connecting to dfrobotOffice
.......
WiFi connected.
IP address: 
192.168.0.221
New Client.
GET / HTTP/1.1
Host: 192.168.0.221
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9

Client Disconnected.
New Client.
GET /favicon.ico HTTP/1.1
Host: 192.168.0.221
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Referer: http://192.168.0.221/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9

Client Disconnected.

使用网页浏览器访问TCP Web Server 的IP,点击页面,可开关LED.

image-20211220170814222

例程:WiFi Telnet to Serial

程序建立一个 server,并在 debug 串口打印出IP地址和端口号。PC 在同一个网段,通过 TCP Client 或 Telent 工具连接这个 IP 和端口后,即可建立无线 WiFi Telnet 和 串口2 收发数据的功能,为了便于调试可以将代码中串口收发数据改为 debug 串口。

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->WiFi/examples/WiFiTelnetToSerial)

/*
  WiFiTelnetToSerial - Example Transparent UART to Telnet Server for ESP32

  Copyright (c) 2017 Hristo Gochkov. All rights reserved.
  This file is part of the ESP32 WiFi library for Arduino environment.

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.

  This library is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with this library; if not, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/
#include <WiFi.h>
#include <WiFiMulti.h>

WiFiMulti wifiMulti;

//how many clients should be able to telnet to this ESP32
#define MAX_SRV_CLIENTS 1
const char* ssid = "**********";
const char* password = "**********";

WiFiServer server(23);
WiFiClient serverClients[MAX_SRV_CLIENTS];

void setup() {
  Serial.begin(115200);
  Serial.println("\nConnecting");

  wifiMulti.addAP(ssid, password);
  wifiMulti.addAP("ssid_from_AP_2", "your_password_for_AP_2");
  wifiMulti.addAP("ssid_from_AP_3", "your_password_for_AP_3");

  Serial.println("Connecting Wifi ");
  for (int loops = 10; loops > 0; loops--) {
    if (wifiMulti.run() == WL_CONNECTED) {
      Serial.println("");
      Serial.print("WiFi connected ");
      Serial.print("IP address: ");
      Serial.println(WiFi.localIP());
      break;
    }
    else {
      Serial.println(loops);
      delay(1000);
    }
  }
  if (wifiMulti.run() != WL_CONNECTED) {
    Serial.println("WiFi connect failed");
    delay(1000);
    ESP.restart();
  }

  //start UART and the server
  Serial1.begin(9600);
  server.begin();
  WiFi.setSleep(false);
  server.setNoDelay(true);

  Serial.print("Ready! Use 'telnet ");
  Serial.print(WiFi.localIP());
  Serial.println(" 23' to connect");
}

void loop() {
  uint8_t i;
  if (wifiMulti.run() == WL_CONNECTED) {
    //check if there are any new clients
    if (server.hasClient()){
      for(i = 0; i < MAX_SRV_CLIENTS; i++){
        //find free/disconnected spot
        if (!serverClients[i] || !serverClients[i].connected()){
          if(serverClients[i]) serverClients[i].stop();
          serverClients[i] = server.available();
          if (!serverClients[i]) Serial.println("available broken");
          Serial.print("New client: ");
          Serial.print(i); Serial.print(' ');
          Serial.println(serverClients[i].remoteIP());
          break;
        }
      }
      if (i >= MAX_SRV_CLIENTS) {
        //no free/disconnected spot so reject
        server.available().stop();
      }
    }
    //check clients for data
    for(i = 0; i < MAX_SRV_CLIENTS; i++){
      if (serverClients[i] && serverClients[i].connected()){
        if(serverClients[i].available()){
          //get data from the telnet client and push it to the UART
          while(serverClients[i].available()) Serial1.write(serverClients[i].read());
        }
      }
      else {
        if (serverClients[i]) {
          serverClients[i].stop();
        }
      }
    }
    //check UART for data
    if(Serial1.available()){
      size_t len = Serial1.available();
      uint8_t sbuf[len];
      Serial1.readBytes(sbuf, len);
      //push UART data to all connected telnet clients
      for(i = 0; i < MAX_SRV_CLIENTS; i++){
        if (serverClients[i] && serverClients[i].connected()){
          serverClients[i].write(sbuf, len);
          delay(1);
        }
      }
    }
  }
  else {
    Serial.println("WiFi not connected!");
    for(i = 0; i < MAX_SRV_CLIENTS; i++) {
      if (serverClients[i]) serverClients[i].stop();
    }
    delay(1000);
  }
}

9.3 同步UDP通信

UDP协议是User Datagram Protocol的简称,中文名是用户数据协议,是一种无连接的传输层协议。UDP协议具有较好的实时性,效率比TCP协议高,适用于对高速传输和实时性有较高要求的通信。但是UDP协议不保证绝对的可靠,需要用户通过其他方式实现可靠性。

9.3.1 UDP 几种通信方式

只有UDP有广播和多播, TCP只能进行点对点的单播, 多播的重点是高效的把同一个包尽可能多的发送到不同的,甚至可能是未知的设备。但是TCP连接是一对一明确的,只能单播。

UDP单播(Unicast)

单播是客户端与服务器之间的点到点连接。

UDP广播(broadcast)

广播UDP与单播UDP的区别就是IP地址不同,广播使用广播地址255.255.255.255,将消息发送到在同一广播网络上的每个主机。值得强调的是:本地广播信息是不会被路由器转发。当然这是十分容易理解的,因为如果路由器转发了广播信息,那么势必会引起网络瘫痪。这也是为什么IP协议的设计者故意没有定义互联网范围的广播机制。

广播地址通常用于在网络游戏中处于同一本地网络的玩家之间交流状态信息等。

其实广播顾名思义,就是想局域网内所有的人说话,但是广播还是要指明接收者的端口号的,因为不可能接受者的所有端口都来收听广播。

UDP多播(multicast)

多播,也称为“组播”,将网络中同一业务类型主机进行了逻辑上的分组,进行数据收发的时候其数据仅仅在同一分组中进行,其他的主机没有加入此分组不能收发对应的数据。

在广域网上广播的时候,其中的交换机和路由器只向需要获取数据的主机复制并转发数据。主机可以向路由器请求加入或退出某个组,网络中的路由器和交换机有选择地复制并传输数据,将数据仅仅传输给组内的主机。多播的这种功能,可以一次将数据发送到多个主机,又能保证不影响其他不需要(未加入组)的主机的其他通 信。

相对于传统的一对一的单播,多播具有如下的优点:

  • 具有同种业务的主机加入同一数据流,共享同一通道,节省了带宽和服务器的优点,具有广播的优点而又没有广播所需要的带宽。

  • 服务器的总带宽不受客户端带宽的限制。由于组播协议由接收者的需求来确定是否进行数据流的转发,所以服务器端的带宽是常量,与客户端的数量无关。

  • 与单播一样,多播是允许在广域网即Internet上进行传输的,而广播仅仅在同一局域网上才能进行。

多播的缺点:

  • 多播与单播相比没有纠错机制,当发生错误的时候难以弥补,但是可以在应用层来实现此种功能。

  • 多播的网络支持存在缺陷,需要路由器及网络协议栈的支持。

  • 多播的应用主要有网上视频、网上会议等。

地址规范

多播的地址是特定的,D类地址用于多播。D类IP地址就是多播IP地址,即224.0.0.0至239.255.255.255之间的IP地址,并被划分为局部连接多播地址、预留多播地址和管理权限多播地址3类:

  • 局部多播地址:在224.0.0.0~224.0.0.255之间,这是为路由协议和其他用途保留的地址,路由器并不转发属于此范围的IP包。

  • 预留多播地址:在224.0.1.0~238.255.255.255之间,可用于全球范围(如Internet)或网络协议。

  • 管理权限多播地址:在239.0.0.0~239.255.255.255之间,可供组织内部使用,类似于私有IP地址,不能用于Internet,可限制多播范围。

多播程序设计的框架

要进行多播的编程,需要遵从一定的编程框架。多播程序框架主要包含套接字初始化、设置多播超时时间、加入多播组、发送数据、接收数据以及从多播组中离开几个方面。其步骤如下:

(1)建立一个socket。

(2)然后设置多播的参数,例如超时时间TTL、本地回环许可LOOP等。

(3)加入多播组。

(4)发送和接收数据。

(5)从多播组离开。

9.3.1 API 参考

begin() - 启动监听某个端口

uint8_t begin(uint16_t p)
uint8_t begin(IPAddress a, uint16_t p)

启动监听某个端口,或者来自某地址发送给某端口的数据。

语法

#include <WiFiUdp.h>

WiFiUDP Udp;                         // 实例化WiFiUDP对象
unsigned int localUdpPort = 1234;    // 自定义本地监听端口

if(Udp.begin(localUdpPort)){  // 启动Udp监听服务
	Serial.println("Listening Success");
    // 打印本地的ip地址,在UDP工具中会使用到
    Serial.printf("UDP Listening on IP:%s, UDP port:%d\n", WiFi.localIP().toString().c_str(), localUdpPort);
}

参数

传入值 说明 值范围
IPAddress a UDP的IP地址
uint16_t p UDP的端口号

返回

返回值 说明 值范围
uint8_t 1:启动UDP服务成功
0:启动UDP服务失败

beginMulticast() - 多播模式启动监听

uint8_t beginMulticast(IPAddress a, uint16_t p);

语法

#include <WiFiUdp.h>

WiFiUDP Udp;                         // 实例化WiFiUDP对象
unsigned int localUdpPort = 1234;    // 自定义本地监听端口
IPAddress multicast_ip(239, 1, 2, 3);

if(Udp.beginMulticast(multicast_ip,localUdpPort)){  // 启动Udp多播监听服务
	Serial.println("Listening Success");
    // 打印本地的ip地址,在UDP工具中会使用到
    Serial.printf("UDP Listening on IP:%s, UDP port:%d\n", WiFi.localIP().toString().c_str(), localUdpPort);
}

参数

传入值 说明 值范围
IPAddress a UDP的IP地址
uint16_t p UDP的端口号

返回

返回值 说明 值范围
uint8_t 1:启动UDP服务成功
0:启动UDP服务失败

stop() - 停止监听

void stop()

停止监听,释放资源。

语法

Udp.stop(); // 关闭udp监听

参数

返回

beginPacket() - 准备发送数据包

int beginPacket()
// 准备发送数据包(仅在运行parsePacket()方法且返回值大于0时可用)   
    
int beginPacket(IPAddress ip, uint16_t port)
int beginPacket(const char *host, uint16_t port);
// 准备发送数据包,参数分别为目标IP和目标端口号

语法

Udp.beginPacket(Udp.remoteIP(), remoteUdpPort); // 配置远端ip地址和端口

参数

传入值 说明 值范围
IPAddress ip UDP数据接收设备的IP地址
uint16_t port UDP数据接收设备的监听端口号

返回

返回值 说明 值范围
int 1:配置数据接收设备的IP地址和监听端口号成功
0:配置数据接收设备的IP地址和监听端口号失败

beginMulticastPacket() - 多播模式准备发送数据包

int beginMulticastPacket();

语法

udp.beginMulticast();

参数

返回

返回值 说明 值范围
int 1:配置数据接收设备的IP地址和监听端口号成功
0:配置数据接收设备的IP地址和监听端口号失败

parsePacket() - 获取接收数据信息

int parsePacket()

获取接收数据信息,如果有数据包可用,则返回队首数据包长度,否则返回0。

语法

int packetSize = Udp.parsePacket(); // 获得解析包
  if (packetSize)  // 解析包不为空
  {
  }

参数

返回

返回值 说明 值范围
int n:数据包的大小(以字节为单位)
0:没有可用的数据包

available() - 检查是否有数据被接收

int available();

检查设备是否接收到数据。函数将会返回等待读取的数据字节数。请注意,使用本函数以前需要先调用parsePacket函数。

语法

void loop()
{
  int packetSize = Udp.parsePacket();  // 获得解析包
  Serial.printf("UDP data size: %d\n", packetSize);
  int n = Udp.available();
  Serial.printf("UDP buffer size: %d\n", n);
  delay(3000);
}

参数

返回

返回值 说明 值范围
int 等待读取的数据字节数。
返回值数据类型:int

endPacket() - 发送UDP数据包

int endPacket()

语法

char  replyPacket[] = "FireBeetle MESH\n";  //发送的消息,仅支持英文
// 向udp工具发送消息
Udp.beginPacket(Udp.remoteIP(), remoteUdpPort); // 配置远端ip地址和端口
Udp.write(replyPacket); // 把数据写入发送缓冲区
Udp.endPacket(); // 发送数

参数

返回

返回值 说明 值范围
int 1:数据发送成功
0:数据发送失败

remoteIP() - 返回远端地址

IPAddress remoteIP()

返回远端地址(仅在运行parsePacket()方法且返回值大于0时可用)。

语法

Serial.printf("received from remote IP:%s(remote port:%d)Number of packet bytes is:%d\n", Udp.remoteIP().toString().c_str(), Udp.remotePort(), packetSize);

参数

返回

返回值 说明 值范围
IPAddress 返回值是向主板发送UDP数据包的远端设备的IP地址。

remotePort() - 返回远端端口号

uint16_t remotePort()

返回远端端口号(仅在运行parsePacket()方法且返回值大于0时可用)。

语法

Serial.printf("received from remote IP:%s(remote port:%d)Number of packet bytes is:%d\n", Udp.remoteIP().toString().c_str(), Udp.remotePort(), packetSize);

参数

返回

返回值 说明 值范围
uint16_t 返回值是向主板发送UDP数据包的远端设备的端口。

write() - 复制数据到发送缓冲区

size_t write(uint8_t)
size_t write(const uint8_t *buffer, size_t size)

复制数据到发送缓冲区(同一数据包发送缓存最大1460字节)。请注意:此函数仅仅将数据写入发送缓冲区,但是数据不会发送。

语法

Udp.write(replyPacket); // 把数据写入发送缓冲区

参数

传入值 说明 值范围
uint8_t 发送的数据字节
const uint8_t *buffer 指向发送数据的指针
size_t size 数据字节数

返回

返回值 说明 值范围
size_t 写入发送缓冲区的数据大小(单位:字节)。

read() - 读取数据

int read()
// 读取首字节数据(仅在运行parsePacket()方法且返回值大于0时可用)

int read(unsigned char* buffer, size_t len)
int read(char* buffer, size_t len)
// 读取数据(仅在运行parsePacket()方法且返回值大于0时可用)

语法

if (Udp.available()) {  // 判断是否有UDP数据包
    char a = Udp.read();//连续调用read
    Serial.printf("READ: %c\n", a);
    a = Udp.read();
    Serial.printf("READ: %c\n", a);
    a = Udp.read();
    Serial.printf("READ: %c\n", a);
    a = Udp.read();
    Serial.printf("READ: %c\n", a);
    char incomingPacket[255];  // 存储Udp客户端发过来的数据
    int len = Udp.read(incomingPacket, 255);
    Serial.printf("READ: %s   len:%d\n", incomingPacket, len);
}  

参数

传入值 说明 值范围
* buffer 保存传入数据包的内存指针
size_t len 传入数据包的大小(单位是字节)

返回

返回值 说明 值范围
int 在没有使用任何参数的情况下调用此函数,函数的返回值情况如下所述:
设备没有接收到数据时,返回值为 -1
设备接收到数据时,返回值为接收到的数据包中的第1个字符的ASCII码数值。

在使用了参数buffer和len调用此函数,函数的返回值情况如下所述:
设备没有接收到数据时,返回值为 -1
设备接收到数据时,返回值为接收到的数据包的大小(单位是字节)。

peek() - 读取数据,但不清除数据

int peek()

语法

int data = Udp.peek();

参数

返回

返回值 说明 值范围
int 设备没有接收到数据时,返回值为-1
设备接收到数据时,返回值为接收到的数据流中的第1个字符。

flush() - 待发数据发送完毕前,保持等待状态

void flush();

等待所有“发送缓存”中数据都发送完毕以后,再执行后续的程序内容。

语法

Udp.flush();

参数

返回

其他读写等操作可参考 串口部分 Stream类中的函数。

例程:建立UDP Server

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

#include <WiFi.h>
#include <WiFiUdp.h>
 
#define ssid      "your_ssid"  		// 这里改成你的设备当前环境下WIFI名字
#define password  "your_password"   // 这里改成你的设备当前环境下WIFI密码
 
WiFiUDP Udp; // 实例化WiFiUDP对象
unsigned int localUdpPort = 1234;  // 自定义本地监听端口
// unsigned int remoteUdpPort = 4321;  // 自定义远程监听端口
uint8_t  incomingPacket[255];  // 保存Udp工具发过来的消息
uint8_t  replyPacket[] = "This is Edge101WE Board speaking.";  //发送的消息,仅支持英文
 
void setup()
{
  Serial.begin(115200); // 打开串口
  Serial.println();
 
  Serial.printf("connecting... %s ", ssid);
  WiFi.begin(ssid, password); // 连接到wifi
  while (WiFi.status() != WL_CONNECTED) // 等待连接
  {
    delay(500);
    Serial.print(".");
  }
  Serial.println("connected");
 
  if(Udp.begin(localUdpPort)){//启动Udp监听服务
    Serial.println("Monitoring succeeded");
    // 打印本地的ip地址,在UDP工具中会使用到
    // WiFi.localIP().toString().c_str()用于将获取的本地IP地址转化为字符串    
    Serial.printf("Now listen to IP:%s, UDP port:%d\n", WiFi.localIP().toString().c_str(), localUdpPort);
  }else{
    Serial.println("Monitoring failed");
  }
}
 
void loop()
{
  // 解析Udp数据包
  int packetSize = Udp.parsePacket(); // 获得解析包
  if (packetSize) // 解析包不为空
  {
    //收到Udp数据包
    //Udp.remoteIP().toString().c_str()用于将获取的远端IP地址转化为字符串
    Serial.printf("Received from remote IP:%s(remote port:%d)packet bytes:%d\n", Udp.remoteIP().toString().c_str(), Udp.remotePort(), packetSize);
      
    // 读取Udp数据包并存放在incomingPacket
    int len = Udp.read(incomingPacket, 255);//返回数据包字节数
    if (len > 0)
    { 
      incomingPacket[len] = 0; // 清空缓存
    }
    // 向串口打印信息
    Serial.printf("UDP data is: %s\n", incomingPacket);
 
    // 向udp工具发送消息
    Udp.beginPacket(Udp.remoteIP(), Udp.remotePort()); // 配置远端ip地址和端口
    Udp.write(replyPacket,39); // 把数据写入发送缓冲区
    Udp.endPacket(); // 发送数据
  }
}

将程序下载到 Edge101WE 主板。打开串口终端,提示已经连接到WiFi,并且打开监听,IP:192.168.0.73, UDP port:1234

connecting... dfrobotOffice .....connected
Monitoring succeeded
Now listen to IP:192.168.0.221, UDP port:1234

打开 Packet Sender 软件,通过 UDP 发送字符串到 Edge101WE 主板的 IP:192.168.0.221 和 UDP port:1234。

image-20211220172558910

串口终端将会收到发送的字符串“This is Packet Sender speaking.”。

此时可在 Packet Sender 软件 上看到 Edge101WE 主板返回的字符串”This is Edge101WE Board speaking.”。

connecting... dfrobotOffice .....connected
Monitoring succeeded
Now listen to IP:192.168.0.221, UDP port:1234
Received from remote IP:192.168.1.96(remote port:50760)packet bytes:31
UDP data is: This is Packet Sender speaking.

9.4 异步 UDP 通信

异步 UDP 理论上可以提供比一般 UDP 更加优异的性能。

API参考

AsyncUDP类:

onPacket() - 注册事件,绑定回调函数
void onPacket(AuPacketHandlerFunctionWithArg cb, void * arg=NULL);
void onPacket(AuPacketHandlerFunction cb);
listen() - 开启监听
bool listen(const ip_addr_t *addr, uint16_t port);
bool listen(const IPAddress addr, uint16_t port);
bool listen(const IPv6Address addr, uint16_t port);
bool listen(uint16_t port);
listenMulticast() - 开启多播监听
bool listenMulticast(const ip_addr_t *addr, uint16_t port, uint8_t ttl=1, tcpip_adapter_if_t tcpip_if=TCPIP_ADAPTER_IF_MAX);
bool listenMulticast(const IPAddress addr, uint16_t port, uint8_t ttl=1, tcpip_adapter_if_t tcpip_if=TCPIP_ADAPTER_IF_MAX);
bool listenMulticast(const IPv6Address addr, uint16_t port, uint8_t ttl=1, tcpip_adapter_if_t tcpip_if=TCPIP_ADAPTER_IF_MAX);
connect() - 作为客户端连接到服务器
bool connect(const ip_addr_t *addr, uint16_t port);
bool connect(const IPAddress addr, uint16_t port);
bool connect(const IPv6Address addr, uint16_t port);

作为客户端连接到服务器(连接成功后可以直接使用write、send方法,而不用writeTo、sendTo)。

close() - 关闭UDP
void close()
write() - 发送数据
size_t writeTo(const uint8_t *data, size_t len, const ip_addr_t *addr, uint16_t port, tcpip_adapter_if_t tcpip_if=TCPIP_ADAPTER_IF_MAX);
size_t writeTo(const uint8_t *data, size_t len, const IPAddress addr, uint16_t port, tcpip_adapter_if_t tcpip_if=TCPIP_ADAPTER_IF_MAX);
size_t writeTo(const uint8_t *data, size_t len, const IPv6Address addr, uint16_t port, tcpip_adapter_if_t tcpip_if=TCPIP_ADAPTER_IF_MAX);
size_t write(const uint8_t *data, size_t len);
size_t write(uint8_t data);
broadcast() - 广播数据
size_t broadcastTo(uint8_t *data, size_t len, uint16_t port, tcpip_adapter_if_t tcpip_if=TCPIP_ADAPTER_IF_MAX);
size_t broadcastTo(const char * data, uint16_t port, tcpip_adapter_if_t tcpip_if=TCPIP_ADAPTER_IF_MAX);
size_t broadcast(uint8_t *data, size_t len);
size_t broadcast(const char * data);
send() - 发送数据
size_t sendTo(AsyncUDPMessage &message, const ip_addr_t *addr, uint16_t port, tcpip_adapter_if_t tcpip_if=TCPIP_ADAPTER_IF_MAX);
size_t sendTo(AsyncUDPMessage &message, const IPAddress addr, uint16_t port, tcpip_adapter_if_t tcpip_if=TCPIP_ADAPTER_IF_MAX);
size_t sendTo(AsyncUDPMessage &message, const IPv6Address addr, uint16_t port, tcpip_adapter_if_t tcpip_if=TCPIP_ADAPTER_IF_MAX);
size_t send(AsyncUDPMessage &message);
broadcast() - 广播数据
size_t broadcastTo(AsyncUDPMessage &message, uint16_t port, tcpip_adapter_if_t tcpip_if=TCPIP_ADAPTER_IF_MAX);
size_t broadcast(AsyncUDPMessage &message);

AsyncUDPPacket类:

AsyncUDPPacket() - 构造函数
AsyncUDPPacket(AsyncUDPPacket &packet);
AsyncUDPPacket(AsyncUDP *udp, pbuf *pb, const ip_addr_t *addr, uint16_t port, struct netif * netif);
data() - 返回数据指针
uint8_t * data()
length() - 返回数据长度
size_t length()
isBroadcast() - 查询是否为广播数据
bool isBroadcast()
isMulticast() - 查询是否为组播地址
bool isMulticast()
localIP() - 返回目标(本地)IP
IPAddress localIP()

返回目标(本地)IP

localPort() - 返回目标(本地)端口号
uint16_t localPort()
remoteIP() - 返回远端IP
IPAddress remoteIP()
remotePort() - 返回远端端口号
uint16_t remotePort()
remoteMac() - 返回远端MAC地址
 void remoteMac(uint8_t * mac);
send() - 发送UDP数据
size_t send(AsyncUDPMessage &message);
available() - 查询缓存区数据
int available();
read() - 读取数据
size_t read(uint8_t *data, size_t len)  // 读取数据
    
int read();  // 读取首字节数据  
peek() - 读取首字节数据,但并不从接收缓存中删除它
int peek()
flush() - 待发数据发送完毕前,保持等待状态
void flush()

等待所有“发送缓存”中数据都发送完毕以后,再执行后续的程序内容。

语法

Udp.flush();

参数

返回

write() - 向远端发数据
size_t write(const uint8_t *data, size_t len)
size_t write(uint8_t data)

AsyncUDPMessage类:

AsyncUDPMessage() - 构造函数
AsyncUDPMessage(size_t size=CONFIG_TCP_MSS)

构造函数(相当于一个缓存,把要发送的数据放到这里,然后通过相应方法发送)

write() - 将数据写到AsyncUDPMessage对象
size_t write(const uint8_t *data, size_t len)
size_t write(uint8_t data)
space() - 返回AsyncUDPMessage对象剩余可用空间
size_t space()
data() - 返回AsyncUDPMessage对象中数据首指针
uint8_t * data()
length() - 返回AsyncUDPMessage对象当前已用长度
size_t length()
flush() - 待发AsyncUDPMessage对象中的数据发送完毕前,保持等待状态
void flush()

参数

返回

例程:AsyncUDPServer

建立一个异步UDP服务器,监听端口1234。将收到的数据从串口打印出来。同时每秒钟发送一次广播。

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->AsyncUDP\examples\AsyncUDPServer)

#include "WiFi.h"
#include "AsyncUDP.h"

const char * ssid = "***********";
const char * password = "***********";

AsyncUDP udp;

void setup()
{
    Serial.begin(115200);
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);
    if (WiFi.waitForConnectResult() != WL_CONNECTED) {
        Serial.println("WiFi Failed");
        while(1) {
            delay(1000);
        }
    }
    if(udp.listen(1234)) {
        Serial.print("UDP Listening on IP: ");
        Serial.println(WiFi.localIP());
        udp.onPacket([](AsyncUDPPacket packet) {
            Serial.print("UDP Packet Type: ");
            Serial.print(packet.isBroadcast()?"Broadcast":packet.isMulticast()?"Multicast":"Unicast");
            Serial.print(", From: ");
            Serial.print(packet.remoteIP());
            Serial.print(":");
            Serial.print(packet.remotePort());
            Serial.print(", To: ");
            Serial.print(packet.localIP());
            Serial.print(":");
            Serial.print(packet.localPort());
            Serial.print(", Length: ");
            Serial.print(packet.length());
            Serial.print(", Data: ");
            Serial.write(packet.data(), packet.length());
            Serial.println();
            //reply to the client
            packet.printf("Got %u bytes of data", packet.length());
        });
    }
}

void loop()
{
    delay(1000);
    //Send broadcast
    udp.broadcast("Anyone here?");
}

打开 Packet Sender 软件,发送数据到 Edge101WE 主板建立的 UDP server。

image-20211220173613936

Edge101WE 主板串口终端打印,接收到 Packet Sender 软件发出的字符串 “This is Packet Sender speaking.”

同时 Edge101WE 主板将发送获取的字符串长度信息给 Packet Sender 软件。

UDP Listening on IP: 192.168.0.221
UDP Packet Type: Unicast, From: 192.168.1.96:63983, To: 192.168.0.221:1234, Length: 31, Data: This is Packet Sender speaking.

例程:UDP多播服务器

多播(组播)的概念 多播,也称为“组播”,将网络中同一业务类型主机进行了逻辑上的分组,进行数据收发的时候其数据仅仅在同一分组中进行,其他的主机没有加入此分组不能收发对应的数据。

在广域网上广播的时候,其中的交换机和路由器只向需要获取数据的主机复制并转发数据。主机可以向路由器请求加入或退出某个组,网络中的路由器和交换机有选择地复制并传输数据,将数据仅仅传输给组内的主机。多播的这种功能,可以一次将数据发送到多个主机,又能保证不影响其他不需要(未加入组)的主机的其他通信。

相对于传统的一对一的单播,多播具有如下的优点:

  • 具有同种业务的主机加入同一数据流,共享同一通道,节省了带宽和服务器的优点,具有广播的优点而又没有广播所需要的带宽。

  • 服务器的总带宽不受客户端带宽的限制。由于组播协议由接收者的需求来确定是否进行数据流的转发,所以服务器端的带宽是常量,与客户端的数量无关。

  • 与单播一样,多播是允许在广域网即 Internet 上进行传输的,而广播仅仅在同一局域网上才能进行。

组播的缺点:

  • 多播与单播相比没有纠错机制,当发生错误的时候难以弥补,但是可以在应用层来实现此种功能。

  • 多播的网络支持存在缺陷,需要路由器及网络协议栈的支持。

  • 多播的应用主要有网上视频、网上会议等。

广域网的多播 多播的地址是特定的,D类地址用于多播。D类IP地址就是多播IP地址,即 224.0.0.0至239.255.255.255 之间的 IP 地址,并被划分为局部连接多播地址、预留多播地址和管理权限多播地址3类:

  • 局部多播地址:在 224.0.0.0~224.0.0.255 之间,这是为路由协议和其他用途保留的地址,路由器并不转发属于此范围的IP包。

  • 预留多播地址:在 224.0.1.0~238.255.255.255 之间,可用于全球范围(如 Internet)或网络协议。

  • 管理权限多播地址:在 239.0.0.0~239.255.255.255 之间,可供组织内部使用,类似于私有IP地址,不能用于Internet,可限制多播范围。

在一块 Edge101WE 主板A 烧写如下程序:

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->AsyncUDP\examples\AsyncUDPMulticastServer)

#include "WiFi.h"
#include "AsyncUDP.h"

const char * ssid = "***********";
const char * password = "***********";

AsyncUDP udp;

void setup()
{
    Serial.begin(115200);
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);
    if (WiFi.waitForConnectResult() != WL_CONNECTED) {
        Serial.println("WiFi Failed");
        while(1) {
            delay(1000);
        }
    }
    if(udp.listenMulticast(IPAddress(239,1,2,3), 1234)) {
        Serial.print("UDP Listening on IP: ");
        Serial.println(WiFi.localIP());
        udp.onPacket([](AsyncUDPPacket packet) {
            Serial.print("UDP Packet Type: ");
            Serial.print(packet.isBroadcast()?"Broadcast":packet.isMulticast()?"Multicast":"Unicast");
            Serial.print(", From: ");
            Serial.print(packet.remoteIP());
            Serial.print(":");
            Serial.print(packet.remotePort());
            Serial.print(", To: ");
            Serial.print(packet.localIP());
            Serial.print(":");
            Serial.print(packet.localPort());
            Serial.print(", Length: ");
            Serial.print(packet.length());
            Serial.print(", Data: ");
            Serial.write(packet.data(), packet.length());
            Serial.println();
            //reply to the client
            packet.printf("Got %u bytes of data", packet.length());
        });
        //Send multicast
        udp.print("Hello!");
    }
}

void loop()
{
    delay(1000);
    //Send multicast
    udp.print("Anyone here?");
}

在另外一块 Edge101WE 主板B 烧写如下程序。为了防止重复的应答,去掉了应答和广播的代码。

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码

#include "WiFi.h"
#include "AsyncUDP.h"

const char * ssid = "***********";
const char * password = "***********";

AsyncUDP udp;

void setup()
{
    Serial.begin(115200);
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);
    if (WiFi.waitForConnectResult() != WL_CONNECTED) {
        Serial.println("WiFi Failed");
        while(1) {
            delay(1000);
        }
    }
    if(udp.listenMulticast(IPAddress(239,1,2,3), 1234)) {
        Serial.print("UDP Listening on IP: ");
        Serial.println(WiFi.localIP());
        udp.onPacket([](AsyncUDPPacket packet) {
            Serial.print("UDP Packet Type: ");
            Serial.print(packet.isBroadcast()?"Broadcast":packet.isMulticast()?"Multicast":"Unicast");
            Serial.print(", From: ");
            Serial.print(packet.remoteIP());
            Serial.print(":");
            Serial.print(packet.remotePort());
            Serial.print(", To: ");
            Serial.print(packet.localIP());
            Serial.print(":");
            Serial.print(packet.localPort());
            Serial.print(", Length: ");
            Serial.print(packet.length());
            Serial.print(", Data: ");
            Serial.write(packet.data(), packet.length());
            Serial.println();
            //reply to the client
            //packet.printf("Got %u bytes of data", packet.length());
        });
        //Send multicast
        //udp.print("Hello!");
    }
}

void loop()
{
    delay(1000);
    //Send multicast
    //udp.print("Anyone here?");
}

此时在 软件上 建立一个 多播,发送信息,点击发送后会弹出多播的设置页面,将 239.1.2.3 加入。

然后点击 Send 按钮,在两个 Edge101WE 主板都将收到相同的信息。

image-20211221104336798

主板A串口终端打印

UDP Listening on IP: 192.168.0.221
UDP Packet Type: Multicast, From: 192.168.1.96:51296, To: 239.1.2.3:1234, Length: 31, Data: This is Packet Sender speaking.
UDP Packet Type: Multicast, From: 192.168.1.96:51296, To: 239.1.2.3:1234, Length: 31, Data: This is Packet Sender speaking.

主板B串口终端打印

UDP Listening on IP: 192.168.0.218
UDP Packet Type: Multicast, From: 192.168.0.221:1234, To: 239.1.2.3:1234, Length: 12, Data: Anyone here?
UDP Packet Type: Multicast, From: 192.168.1.96:51296, To: 239.1.2.3:1234, Length: 31, Data: This is Packet Sender speaking.
UDP Packet Type: Multicast, From: 192.168.0.221:1234, To: 239.1.2.3:1234, Length: 12, Data: Anyone here?
UDP Packet Type: Multicast, From: 192.168.1.96:51296, To: 239.1.2.3:1234, Length: 31, Data: This is Packet Sender speaking

9.4 Web Server

Web Server 就是提供 Web 服务的服务器。Web服务器的主要功能是:存储、处理和传递网页给客户。Web服务器可以解析(handles)HTTP协议。当Web服务器接收到一个HTTP请求(request),会返回一个HTTP响应(response),例如送回一个HTML页面。为了处理一个请求(request),Web服务器可以响应(response)一个静态页面或图片,进行页面跳转(redirect),或者把动态响应(dynamic response)的产生委托(delegate)给一些其它的程序例如CGI脚本、JSP(JavaServer Pages)脚本、servlets、ASP(Active Server Pages)脚本、服务器端(server-side) JavaScript,或者一些其它的服务器端(server-side)技术。无论脚本的目的如何,这些服务器端(server-side)的程序通常产生一个HTML的响应(response)来让浏览器可以浏览。

Edge101WE 主板可作为存储网页的 Web 服务器,有人通过局域网请求一个网页时,Edge101WE 主板将为该网页提供服务。对于开发来说主要处理的就是注册链接并编写用户访问该链接时需要执行的操作。

使用步骤如下:

  • 引入相应库#include <WebServer.h>。

  • 声明WebServer对象并设置端口号,一般WebServer端口号使用80。

  • 使用on()方法注册链接与回调函数。

  • 使用begin()方法启动服务器进行请求监听。

  • 使用handleClient()处理来自客户端的请求。

9.4.1 API 参考

WebServer() - 构造方法

WebServer(int port = 80)
WebServer(IPAddress addr, int port = 80);

语法

WebServer server(80);

参数

传入值 说明 值范围
IPAddress addr 设置服务器IP地址。
int port 设置服务端口号,默认为80端口。

返回

begin() - 服务器启动监听

void begin()
void begin(uint16_t port)

语法

WebServer server(80);
server.begin();

参数

传入值 说明 值范围
uint16_t port 服务端口号

返回

close() - 停止当前监听

stop() - 停止当前监听

void close()
void stop()

语法

server.close();
server.stop();

参数

返回

on() - 注册链接与回调函数

void on(const String &uri, THandlerFunction handler)
void on(const String &uri, HTTPMethod method, THandlerFunction fn)
void on(const String &uri, HTTPMethod method, THandlerFunction fn, THandlerFunction ufn)

设置HTTP请求回调函数。

语法

void homepage() {
  server.send(200, "text/plain", "This is homepage!");
  Serial.println("This is homepage");
}
server.on("/", homepage);

参数

传入值 说明 值范围
uri HTTP请求客户端所请求的uri
handler HTTP请求回调函数
method 向客户端发送响应信息时所使用的HTTP方法 HTTP_ANY
HTTP_GET
HTTP_POST
HTTP_PUT
HTTP_PATCH
HTTP_DELETE
HTTP_OPTIONS

返回

onNotFound() - 注册未注册链接回调函数

void onNotFound(THandlerFunction fn)

设置HTTP请求无效地址的回调函数。通过无效地址回调函数,我们可以将404页面信息发送给客户端。

语法

// 设置处理404情况的函数'handleNotFound'
void handleNotFound() { // 当浏览器请求的网络资源无法在服务器找到时将调用此函数
  server.send(404, "text/plain", "404: Not found"); 
}
server.onNotFound(handleNotFound);

参数

传入值 说明 值范围
fn 处理无效地址请求的回调函数

返回

onFileUpload() - 注册文件上传回调函数

void onFileUpload(THandlerFunction fn)

当 FireBeetle MESH 主板建立的网络服务器收到了客户端的文件上传请求时,可利用此函数来配置处理文件上传请求的回调函数。

语法

void handleonFileUpload() { // 当收到了客户端的文件上传请求时时将调用此函数
    Serial.println("FileUpload called");
}
server.onFileUpload(handleonFileUpload);

参数

传入值 说明 值范围
fn 处理文件上传请求的回调函数

返回

handleClient() - 处理来自客户端的请求

void handleClient();

检查有没有客户端设备通过网络向 FireBeetle MESH 主板建立的网络服务器发送请求。每一次 handleClient 函数被调用时,FireBeetle MESH 的网络服务器都会检查一下是否有客户端发送 HTTP 请求。因此建议将该函数放在 loop 函数中,从而确保它能经常被调用。

假如 loop 函数里有类似 delay 一类的函数延迟程序运行,那么就一定要注意了。如果 handleClient 函数长时间得不到调用,FireBeetle MESH 网络服务器会因为无法经常检查 HTTP 客户端请求而导致服务器响应变慢,严重的情况下,会导致服务器工作不稳定。

语法

void loop(void){
  server.handleClient();     // 处理 http 服务器访问
}

参数

返回

uri() - 返回客户端请求的url

String uri()  

获取客户端发送的HTTP请求行中的请求资源路径信息。

语法

void homepage() {
  Serial.println(server.uri());  // 通过串口监视器输出浏览器请求资源路径
  server.send(200, "text/plain", "This is homepage!");
}
server.on("/", homepage);

参数

返回

返回值 说明 值范围
String 客户端请求行中的请求资源路径信息。

method() - 返回客户端请求方法

HTTPMethod method()

语法

void homepage() {
  Serial.println(server.method());  // 串口输出当前客户端的请求方法
  server.send(200, "text/plain", "This is homepage!");
}
server.on("/", homepage);

参数

返回

返回值 说明 值范围
HTTPMethod HTTP请求方法代表值 HTTP_ANY 1
HTTP_GET 1
HTTP_POST 3
HTTP_PUT 4
HTTP_PATCH 5
HTTP_DELETE 6
HTTP_OPTIONS 7

collectHeaders() - 设置需要收集哪些请求头信息

void collectHeaders(const char* headerKeys[], const size_t headerKeysCount); 

使用 Edge101WE 主板实现的物联网服务器收集客户端发送的请求头信息。使用这个函数设置需要获取的HTTP请求头信息。

注意:在我们使用WebServer库中的headers、header 、headerName、hasHeader 函数来获取浏览器请求头以前,需要首先调用本函数来设置主板具体收集哪些请求头信息。

语法

#include <WiFi.h>
#include <WebServer.h>

// 设置WiFi接入信息
const char* ssid = "your_ssid";
const char* password = "your_password";

// 创建 WebServer 对象
WebServer server(80);

// 设置访问次数变量
int repeat = 0;
// 设置需要收集的请求头信息
const char *headerKeys[] = {"Content-Length", "Content-Type", "Connection", "Date"};

void homepage() {
  Serial.println(server.method());  // 串口输出当前客户端的请求方法
  server.send(200, "text/plain", "This is homepage!");
  if (server.hasHeader("Connection")) { // 判断该请求头是否存在

    // 使用示例,打印当前收集的请求头的数量
    Serial.print("Number of request headers:"); Serial.println(server.headers());

    // 打印当前请求中所收集的请求头指定项的值
    Serial.print("Value of request headers Connection:"); Serial.println(server.header("Connection"));

    // 打印当前请求中所收集的Host
    Serial.print("request headers Host :"); Serial.println(server.hostHeader());

    // 分隔空行
    Serial.println("\r\n");
  }
}

void setup(void) {
  // 初始化串口
  Serial.begin(115200);
  Serial.println("");

  // 初始化网络
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());

  //初始化WebServer
  server.begin();
  server.on("/", homepage);
  server.collectHeaders(headerKeys, sizeof(headerKeys) / sizeof(headerKeys[0]));
}

void loop(void) {
  // 监听客户请求并处理
  server.handleClient();
}

参数

传入值 说明 值范围
headerKeys[] 请求头名数组。此数组中的元素即是需要主板处理或收集的请求头信息。
headerKeysCount 需要获取/处理的请求头个数

返回

headers() - 获得请求头数量

int headers();

语法

// 使用示例,打印当前收集的请求头的数量
Serial.print("Number of request headers:"); Serial.println(server.headers());

参数

返回

返回值 说明 值范围
int 获取的响应头的数量

headerName() - 获取指定请求头的名称

String headerName(int i);

语法

// 打印当前请求中所收集的请求头的值信息,请求头名称+请求头值
Serial.print(server.headerName(1)); 
Serial.print(" = "); 
Serial.println(server.header(1));

参数

传入值 说明 值范围
int i 请求头序号

返回

返回值 说明 值范围
String 指定请求头名称

hasHeader() - 确定是否包含指定请求头

bool hasHeader(String name);

语法

// 判断该请求头是否存在Connection信息
if (server.hasHeader("Connection")) {
    Serial.println("Include Connection"); 
} else {
    Serial.println("Not included Connection"); 
} 

参数

传入值 说明 值范围
String name 需要确认客户端请求中是否存在的请求头信息名

返回

返回值 说明 值范围
bool true:请求头中是否存在指定请求头信息存在。
false:请求头中是否存在指定请求头信息不存在

arg() - 请求中指定参数的数值

String arg(String name);        // get request argument value by name
String arg(int i);              // get request argument value by number

语法

void homepage() {
	server.send(200, "text/plain", "This is homepage!");
 	Serial.print("parameter a:"); Serial.println(server.arg("a"));
  	Serial.print("second parameter:"); Serial.println(server.arg(2));	   
}

server.on("/", homepage);

参数

传入值 说明 值范围
String name 请求参数的名称
int i 请求参数序列号

返回

返回值 说明 值范围
String 请求参数的值。如果没有指定的请求参数号/请求参数名称对应的请求参数,则返回空字符串(“”)。

args() - 请求包含的参数数量

int args(); 

语法

void homepage() {
	server.send(200, "text/plain", "This is homepage!");
	Serial.print("Number of parameters:"); Serial.println(server.args());  
}

server.on("/", homepage);

参数

返回

返回值 说明 值范围
int 请求参数的数量

argName() - 请求中参数名

String argName(int i);

语法

void homepage() {
	server.send(200, "text/plain", "This is homepage!");
  	Serial.print("The name of parameter 2:");
  	Serial.println(server.argName(2));
}

server.on("/", homepage);

参数

传入值 说明 值范围
int i 请求体中的参数序列号

返回

返回值 说明 值范围
String 请求信息中指定参数的名称

hasArg () - 判断请求中是否包含某个参数名

bool hasArg(String name);

语法


参数

传入值 说明 值范围
name 需要确认的请求体中的参数名

返回

返回值 说明 值范围
bool 返回是否存在指定参数

send() - 向客户端(浏览器)发送数据

void send(int code, const char* content_type = NULL, const String& content = String(""))
void send(int code, char* content_type, const String& content)
void send(int code, const String& content_type, const String& content)

语法

void homepage() {
  server.send(200, "text/plain", "This is homepage!");
  Serial.println("This is homepage");
}
server.on("/", homepage);

参数响应内容类型

传入值 说明 值范围
code 响应状态码
content_type 响应内容类型
content 响应内容

返回

sendHeader() - 发送响应头

void sendHeader(const String& name, const String& value, bool first = false)

语法

void homepage() {
  // 设置自定义响应头内容
  server.sendHeader("device", "FireBeetle_MESH");
    
  server.send(200, "text/plain", "This is homepage!");
  Serial.println("This is homepage");
}
server.on("/", homepage);

参数

传入值 说明 值范围
name 自定义的响应头信息的名称,可使用字符串类型。
value 自定义的响应头值,可使用字符串类型。
first 设置该响应头是否需要放在第一行,默认为false。

返回

sendContent() - 发送响应正文内容

void sendContent(const String& content);
void sendContent(const char* content, size_t contentLength);

向客户端发送响应信息有以下三种方式:

sendContent:

sendContent函数所发送的信息通常是程序中的一个字符串。该函数的优点是直接调用程序内字符串,这个操作比起两外两种方法来说更加简单直接。但是其缺点是,由于存储发送信息的字符串是在程序中的,这会占用主板的动态内存空间。因此,使用sendContent函数时,发送信息的大小受到了限制。

sendContent_P:

当我们使用sendContent_P时,发送的响应信息必须存储在程序存储空间。这一特点大大优化了程序内存占用。因此sendContent_P对于我们在发送较大的响应信息时非常有帮助。但是sendContent_P的信息仍是写在程序中,如果发送的信息需要分为多个文件存储,使用sendContent_P函数是无法胜任的。

streamFile:

使用streamFile函数来发送响应信息是最推荐的操作方法。因为streamFile利用了主板的闪存文件系统来存储发送的信息内容。可以说streamFile函数既可以节省程序内存空间,又允许我们将需要发送的信息分为多个文件进行保存。但是使用streamFile时需要我们使用Arduino IDE的闪存文件上传工具预先将文件上传到闪存中。

语法

void homepage() {
  // 设置响应体内容以及响应体长度
  server.sendContent("sendContent_test_OK");     
  server.send(200, "text/plain", "This is homepage!");
  Serial.println("This is homepage");
}
server.on("/", homepage);

参数

传入值 说明 值范围
content 响应体信息,可使用字符串格式
contentLength 响应体信息长度

返回

sendContent_P() - 发送响应正文内容

void sendContent_P(PGM_P content);
void sendContent_P(PGM_P content, size_t size);

语法

// 建立保存在程序存储空间中的字符串
const char reponseContent[] PROGMEM = "FireBeetle MESH IIoT mainboard";
void homepage() {
  // 设置响应体内容
  server.sendContent_P(reponseContent);    
  server.send(200, "text/plain", "This is homepage!");
  Serial.println("This is homepage");
}
server.on("/", homepage);

参数

传入值 说明 值范围
content 响应体信息,该信息必须存储在程序存储空间的字符数组。
size_t size 响应体信息长度

返回

streamFile() - 发送文件,返回发送的字节数

template<typename T>
size_t streamFile(T &file, const String& contentType)

语法

if (SPIFFS.exists(path)) {                     // 如果访问的文件可以在SPIFFS中找到
    File file = SPIFFS.open(path, "r");          // 则尝试打开该文件
    server.streamFile(file, contentType);// 并且将该文件返回给浏览器

参数

传入值 说明 值范围
file 存储有响应信息的闪存文件对象
contentType 响应信息系类型

返回

返回值 说明 值范围
size_t 发送的文件大小

setContentLength() - 设置响应体长度

void setContentLength(const size_t contentLength);

语法

void homepage() {
    // 设置响应体内容以及响应体长度
    server.sendContent("sendContent_test_OK");
    // 不确定响应体信息长度,使用 CONTENT_LENGTH_UNKNOWN 关键词作为参数    	
 	server.setContentLength(CONTENT_LENGTH_UNKNOWN); 
    
    server.send(200, "text/plain", "This is homepage!");
  	Serial.println("This is homepage");
}
server.on("/", homepage);

参数

传入值 说明 值范围
contentLength 响应体的长度。
不确定响应体信息长度,使用 CONTENT_LENGTH_UNKNOWN 关键词作为参数 。

返回

upload() - 处理文件上传

HTTPUpload& upload() { return *_currentUpload; }

语法

// 处理上传文件函数
void handleFileUpload(){  
  
  HTTPUpload& upload = server.upload();

参数

传入值 说明 值范围
server.upload()

返回

authenticate() - 请求认证校验

bool authenticate(const char * username, const char * password)

通过本API建立加密网页,设置用户名和密码。用户必须正确输入访问用户名和密码方可访问物联网服务器所建立的页面内容。

语法

//校验用户登录账号和密码,(使用Basic方式),若输入错误则继续返回认证界面
if (!server.authenticate("FireBeetleMESH", "123456")) return server.requestAuthentication();

参数

传入值 说明 值范围
username 客户端访问物联网服务器加密页面时的认证用户名
password 客户端访问物联网服务器加密页面时的认证密码

返回

返回值 说明 值范围
bool true:用户密码输入正确
false:用户密码输入错误

requestAuthentication() - 请求进行用户登录认证

void requestAuthentication(HTTPAuthMethod mode = BASIC_AUTH, const char* realm = NULL, const String& authFailMsg = String(""))

请求进行用户登录认证(浏览器端会打开登录窗口)。

语法

//校验用户登录账号和密码,(使用Basic方式),若输入错误则继续返回认证界面
if (!server.authenticate("FireBeetleMESH", "123456")) return server.requestAuthentication();

参数

传入值 说明 值范围
mode HTTP验证方式 BASIC_AUTH、 DIGEST_AUTH(默认为BASIC_AUTH)
realm 认证范围
authFailMsg 认证失败提示消息

返回

HTTP状态码说明

上面的200、404等是HTTP状态码。用户在请求访问某个地址的时候,WebServer需要进行响应,发送响应头,响应头中第一行一般像是这样的HTTP/1.1 200 OK,其中200就是状态码。常见的状态码如下:

200服务器成功返回网页; 404请求的网页不存在; 301本网页被永久性转移到另一个URL; 503服务器目前不可用; 401请求未经授权,需要登录认证; ……

Content-Type说明

上面的text/plain、text/html这些是Content-Type,具体说明如下:

Content-Type(MediaType),即是Internet Media Type,互联网媒体类型,也叫做MIME类型。在互联网中有成百上千中不同的数据类型,HTTP在传输数据对象时会为他们打上称为MIME的数据格式标签,用于区分数据类型。最初MIME是用于电子邮件系统的,后来HTTP也采用了这一方案。 在HTTP协议消息头中,使用Content-Type来表示请求和响应中的媒体类型信息。它用来告诉服务端如何处理请求的数据,以及告诉客户端(一般是浏览器)如何解析响应的数据,比如显示图片,解析并展示html等等。

Content-Type举例:

text/plain纯文本文件; text/htmlhtml文件; application/jsonjson形式数据文件; application/xmlxml形式数据文件; image/pngpng格式图片; ……

9.4.2 同步 Web Server 例程

例程:最简单的 Web Server 例子

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

#include <WiFi.h>
#include <WebServer.h> // 引入相应库

// 连接WiFi网络的SSID和密码
const char *ssid = "your_ssid";	
const char *password = "your_password";

WebServer server(80); // 声明WebServer对象

void handleRoot() // 回调函数
{
  server.send(200, "text/plain", "This is the root directory");
}

void handleP1() // 回调函数
{
  server.send(200, "text/plain", "This is the Page 1");
}

void setup()
{
  Serial.begin(115200);
  Serial.println();

  WiFi.mode(WIFI_STA);
  WiFi.setSleep(false);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }
  Serial.println("Connected");
  Serial.print("IP Address:");
  Serial.println(WiFi.localIP());  // 从串口打印出 Web Server的IP,用于访问

  server.on("/", handleRoot); // 注册链接"/"与对应回调函数

  server.on("/p1", handleP1); // 注册链接"/p1"与对应回调函数

  server.on("/p2", []() { // 注册链接"/p2",对应回调函数通过内联函数声明
    server.send(200, "text/plain", "This is the Page 2");
  });

  server.begin(); // 启动服务器
  Serial.println("Web server started");
}

void loop()
{
  server.handleClient(); // 处理来自客户端的请求
  delay(2);// 允许cpu切换到其他任务 allow the cpu to switch to other tasks  
}

下载程序后将会从串口打印出Web Server的IP。

image-20210420104952508

使用网页浏览器访问 http://192.168.1.9/ 可看到 This is the root directory

image-20210420105805663

访问 http://192.168.1.9/p1 可看到 This is the Page 1

image-20210420105932129

访问 http://192.168.1.9/p2 可看到 This is the Page 2

image-20210420110121071

例程:设置未注册页面响应

如果用户访问了未注册的的链接时我们最好能给个提示,比如我们在上网时经常能见到的“网页不存在”、“404 Not Found”等。在这里我们可以用onNotFound()方法来给出这样的提示,用户在访问不存在的链接时会跳转到该方法所绑定的回调函数上。

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

#include <WiFi.h>
#include <WebServer.h> //引入相应库

// 连接WiFi网络的SSID和密码
const char *ssid = "your_ssid";	
const char *password = "your_password";

WebServer server(80); //声明WebServer对象

void handleRoot() //回调函数
{
  server.send(200, "text/plain", "This is the root directory");
}

void handleNotFound() //未注册链接回调函数
{
  server.send(404, "text/plain", "Sorry,this page cannot be found.");
}

void setup()
{
  Serial.begin(115200);
  Serial.println();

  WiFi.mode(WIFI_STA);
  WiFi.setSleep(false);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }
  Serial.println("Connected");
  Serial.print("IP Address:");
  Serial.println(WiFi.localIP());

  server.on("/", handleRoot);    //注册链接"/"与对应回调函数
  server.onNotFound(handleNotFound); //未注册链接回调函数注册

  server.begin(); //启动服务器
  Serial.println("Web server started");
}

void loop()
{
  server.handleClient(); //处理来自客户端的请求
  delay(2);// 允许cpu切换到其他任务 allow the cpu to switch to other tasks
}

当访问串口打印出来的 Web Server IP 可打开根目录网页

image-20210420105805663

当访问其他网页 例如 http://192.168.1.9/p1 时提醒找不到这个网页

image-20210420135006797

例程:用户认证

用户认证可以提供一定的安全性,这里提供了BASIC_AUTHDIGEST_AUTH两种方式,一般来说DIGEST_AUTH方式安全性稍高些。

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

#include <WiFi.h>
#include <WebServer.h> //引入相应库

// 连接WiFi网络的SSID和密码
const char *ssid = "your_ssid";	
const char *password = "your_password";

WebServer server(80); //声明WebServer对象


const char *username = "admin";     //用户名
const char *userpassword = "admin"; //用户密码

void handleRoot() //回调函数
{
  if (!server.authenticate(username, userpassword)) //校验用户是否登录
  {
    return server.requestAuthentication(); //请求进行用户登录认证
  }
  server.send(200, "text/plain", "Login successful."); //登录成功则显示真正的内容
}

void setup()
{
  Serial.begin(115200);
  Serial.println();

  WiFi.mode(WIFI_STA);
  WiFi.setSleep(false);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }
  Serial.println("Connected");
  Serial.print("IP Address:");
  Serial.println(WiFi.localIP());

  server.on("/", handleRoot); //注册链接和回调函数

  server.begin(); //启动服务器
  Serial.println("Web server started");
}

void loop()
{
  server.handleClient(); //处理来自客户端的请求
  delay(2);// 允许cpu切换到其他任务 allow the cpu to switch to other tasks
}

访问串口打印出来的 Web Server IP 要求输入用户名和密码,此时输入程序中的用户名和密码,点击登录。

image-20210420143521767

登录后显示登录成功。

image-20210420143634840

例程:获取访问服务器的客户端信息

客户端请求链接服务器,也能够获取客户端请求的一些信息。

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

#include <WiFi.h>
#include <WebServer.h> //引入相应库

// 连接WiFi网络的SSID和密码
const char *ssid = "your_ssid";
const char *password = "your_password";

WebServer server(80); //声明WebServer对象

void handleRoot() //回调函数
{
  server.send(200, "text/plain", "This is the root directory");
}

void handleClientMessage() //回调函数
{
  String message = "Client information: ";
  message += "\nClient IP: ";
  IPAddress addr = server.client().remoteIP(); //客户端ip
  message += String(addr[0]) + "." + String(addr[1]) + "." + String(addr[2]) + "." + String(addr[3]);
  message += "\nURI: ";
  message += server.uri(); //打印当前url
  message += "\nMethod: ";
  message += (server.method() == HTTP_GET) ? "GET" : "POST"; //判断http请求方法
  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(200, "text/plain", message);
}

void setup()
{
  Serial.begin(115200);
  Serial.println();

  WiFi.mode(WIFI_STA);
  WiFi.setSleep(false);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }
  Serial.println("Connected");
  Serial.print("IP Address:");
  Serial.println(WiFi.localIP());

  server.on("/", handleRoot); //注册链接"/"与对应回调函数
  server.on("/msg", HTTP_GET, handleClientMessage); //注册链接和回调函数

  server.begin(); //启动服务器
  Serial.println("Web server started");
}

void loop()
{
  server.handleClient(); //处理来自客户端的请求
  delay(2);// 允许cpu切换到其他任务 allow the cpu to switch to other tasks    
}

当访问串口打印出来的 Web Server IP 时可显示客户端的信息

image-20210420145715367

例程:HelloServer mDNS方式的Web Server

注意:如果您使用的是Windows电脑需要设置mDNS环境,详情见 9.5.2 mDNS 通过STA方式。

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->WebServer\examples\HelloServer)

#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <ESPmDNS.h>

const char *ssid = "your_ssid";
const char *password = "your_password";

WebServer server(80);

const int led = 15;

void handleRoot() {
  digitalWrite(led, 1);
  server.send(200, "text/plain", "hello from Edge101WE!");
  digitalWrite(led, 0);
}

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);
  digitalWrite(led, 0);
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.println("");

  // Wait for connection
  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());

  if (MDNS.begin("edge101we")) {
    Serial.println("MDNS responder started");
  }

  server.on("/", handleRoot);

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

  server.onNotFound(handleNotFound);

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

void loop(void) {
  server.handleClient();
  delay(2);// 允许cpu切换到其他任务 allow the cpu to switch to other tasks
}

访问 http://edge101we.local/ 可显示 “hello from Edge101WE!”,同时每次刷新网页可看到用户指示灯闪烁一次。

例程:高级Web Server

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->WebServer\examples\AdvancedWebServer)

/*
   Copyright (c) 2015, Majenko Technologies
   All rights reserved.

   Redistribution and use in source and binary forms, with or without modification,
   are permitted provided that the following conditions are met:

 * * Redistributions of source code must retain the above copyright notice, this
     list of conditions and the following disclaimer.

 * * Redistributions in binary form must reproduce the above copyright notice, this
     list of conditions and the following disclaimer in the documentation and/or
     other materials provided with the distribution.

 * * Neither the name of Majenko Technologies nor the names of its
     contributors may be used to endorse or promote products derived from
     this software without specific prior written permission.

   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
   ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
   ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
   (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
   LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
   ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
   (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
   SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/


#include <WiFiClient.h>
#include <WebServer.h>
#include <ESPmDNS.h>


//#define ETH_MODE   //Using Ethernet
#ifdef ETH_MODE
#include <ETH.h>
static bool eth_connected = false;
void ETHEvent(WiFiEvent_t event)
{
  switch (event) {
    case ARDUINO_EVENT_ETH_START:
      //Serial.println("ETH Started");
      //set eth hostname here
      ETH.setHostname("Edge101WE");
      break;
    case ARDUINO_EVENT_ETH_CONNECTED:
      //Serial.println("ETH Connected");
      break;
    case ARDUINO_EVENT_ETH_GOT_IP:
      eth_connected = true;
      break;
    case ARDUINO_EVENT_ETH_DISCONNECTED:
      Serial.println("ETH Disconnected");
      eth_connected = false;
      break;
    case ARDUINO_EVENT_ETH_STOP:
      Serial.println("ETH Stopped");
      eth_connected = false;
      break;
    default:
      break;
  }
}
#else
#include <WiFi.h>
const char *ssid = "your_ssid";
const char *password = "your_password";
#endif
WebServer server(80);

const int led = 15;

void handleRoot() {
  digitalWrite(led, 1);
  char temp[400];
  int sec = millis() / 1000;
  int min = sec / 60;
  int hr = min / 60;

  snprintf(temp, 400,

           "<html>\
  <head>\
    <meta http-equiv='refresh' content='5'/>\
    <title>Edge101WE Demo</title>\
    <style>\
      body { background-color: #cccccc; font-family: Arial, Helvetica, Sans-Serif; Color: #000088; }\
    </style>\
  </head>\
  <body>\
    <h1>Hello from Edge101WE!</h1>\
    <p>Uptime: %02d:%02d:%02d</p>\
    <img src=\"/test.svg\" />\
  </body>\
</html>",

           hr, min % 60, sec % 60
          );
  server.send(200, "text/html", temp);
  digitalWrite(led, 0);
}

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);
  digitalWrite(led, 0);
  Serial.begin(115200);
#ifdef ETH_MODE
  WiFi.onEvent(ETHEvent);
  ETH.begin();
  Serial.print("Please wait for Ethernet connection");
  while(!eth_connected){
    delay(1000);
    Serial.print(".");
  }
  Serial.println("succeed");
  Serial.print("ETH MAC: ");
  Serial.print(ETH.macAddress());
  Serial.print(", IPv4: ");
  Serial.print(ETH.localIP());
  if (ETH.fullDuplex()) {
    Serial.print(", FULL_DUPLEX");
  }
  Serial.print(", ");
  Serial.print(ETH.linkSpeed());
  Serial.println("Mbps");
#else
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.println("");

  // Wait for connection
  Serial.print("Please wait for WiFi connection");
  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());
#endif
  if (MDNS.begin("edge101we")) {
    Serial.println("MDNS responder started");
  }

  server.on("/", handleRoot);
  server.on("/test.svg", drawGraph);
  server.on("/inline", []() {
    server.send(200, "text/plain", "this works as well");
  });
  server.onNotFound(handleNotFound);
  server.begin();
  Serial.println("HTTP server started");
}

void loop(void) {
  server.handleClient();
  delay(2);//allow the cpu to switch to other tasks
}

void drawGraph() {
  String out = "";
  char temp[100];
  out += "<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" width=\"400\" height=\"150\">\n";
  out += "<rect width=\"400\" height=\"150\" fill=\"rgb(250, 230, 210)\" stroke-width=\"1\" stroke=\"rgb(0, 0, 0)\" />\n";
  out += "<g stroke=\"black\">\n";
  int y = rand() % 130;
  for (int x = 10; x < 390; x += 10) {
    int y2 = rand() % 130;
    sprintf(temp, "<line x1=\"%d\" y1=\"%d\" x2=\"%d\" y2=\"%d\" stroke-width=\"1\" />\n", x, 140 - y, x + 10, 140 - y2);
    out += temp;
    y = y2;
  }
  out += "</g>\n</svg>\n";

  server.send(200, "image/svg+xml", out);
}

访问串口打印出来的 Web Server IP ,或访问 http://edge101we.local/ 。每10秒钟主动更新一次网页显示

image-20211221112156647

例程:网页控制LED

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

/*********
  Rui Santos
  Complete project details at https://randomnerdtutorials.com  
*********/

// Load Wi-Fi library
#include <WiFi.h>

// Replace with your network credentials
const char *ssid = "your_ssid";
const char *password = "your_password";

// Set web server port number to 80
WiFiServer server(80);

// Variable to store the HTTP request
String header;

// Auxiliar variables to store the current output state
String output15State = "off";
String output33State = "off";

// Assign output variables to GPIO pins
const int output15 = 15;
const int output33 = 33;

// Current time
unsigned long currentTime = millis();
// Previous time
unsigned long previousTime = 0; 
// Define timeout time in milliseconds (example: 2000ms = 2s)
const long timeoutTime = 2000;

void setup() {
  Serial.begin(115200);
  // Initialize the output variables as outputs
  pinMode(output15, OUTPUT);
  pinMode(output33, OUTPUT);
  // Set outputs to LOW
  digitalWrite(output15, LOW);
  digitalWrite(output33, LOW);

  // Connect to Wi-Fi network with SSID and password
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  // Print local IP address and start web server
  Serial.println("");
  Serial.println("WiFi connected.");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
  server.begin();
}

void loop(){
  WiFiClient client = server.available();   // Listen for incoming clients

  if (client) {                             // If a new client connects,
    currentTime = millis();
    previousTime = currentTime;
    Serial.println("New Client.");          // print a message out in the serial port
    String currentLine = "";                // make a String to hold incoming data from the client
    while (client.connected() && currentTime - previousTime <= timeoutTime) {  // loop while the client's connected
      currentTime = millis();
      if (client.available()) {             // if there's bytes to read from the client,
        char c = client.read();             // read a byte, then
        Serial.write(c);                    // print it out the serial monitor
        header += c;
        if (c == '\n') {                    // if the byte is a newline character
          // if the current line is blank, you got two newline characters in a row.
          // that's the end of the client HTTP request, so send a response:
          if (currentLine.length() == 0) {
            // HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK)
            // and a content-type so the client knows what's coming, then a blank line:
            client.println("HTTP/1.1 200 OK");
            client.println("Content-type:text/html");
            client.println("Connection: close");
            client.println();
            
            // turns the GPIOs on and off
            if (header.indexOf("GET /15/on") >= 0) {
              Serial.println("GPIO 15 on");
              output15State = "on";
              digitalWrite(output15, HIGH);
            } else if (header.indexOf("GET /15/off") >= 0) {
              Serial.println("GPIO 15 off");
              output15State = "off";
              digitalWrite(output15, LOW);
            } else if (header.indexOf("GET /33/on") >= 0) {
              Serial.println("GPIO 33 on");
              output33State = "on";
              digitalWrite(output33, HIGH);
            } else if (header.indexOf("GET /33/off") >= 0) {
              Serial.println("GPIO 33 off");
              output33State = "off";
              digitalWrite(output33, LOW);
            }
            
            // Display the HTML web page
            client.println("<!DOCTYPE html><html>");
            client.println("<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
            client.println("<link rel=\"icon\" href=\"data:,\">");
            // CSS to style the on/off buttons 
            // Feel free to change the background-color and font-size attributes to fit your preferences
            client.println("<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}");
            client.println(".button { background-color: #4CAF50; border: none; color: white; padding: 16px 40px;");
            client.println("text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;}");
            client.println(".button2 {background-color: #555555;}</style></head>");
            
            // Web Page Heading
            client.println("<body><h1>Edge101WE Web Server</h1>");
            
            // Display current state, and ON/OFF buttons for GPIO 15  
            client.println("<p>GPIO 15 - State " + output15State + "</p>");
            // If the output15State is off, it displays the ON button       
            if (output15State=="off") {
              client.println("<p><a href=\"/15/on\"><button class=\"button\">ON</button></a></p>");
            } else {
              client.println("<p><a href=\"/15/off\"><button class=\"button button2\">OFF</button></a></p>");
            } 
               
            // Display current state, and ON/OFF buttons for GPIO 33  
            client.println("<p>GPIO 33 - State " + output33State + "</p>");
            // If the output33State is off, it displays the ON button       
            if (output33State=="off") {
              client.println("<p><a href=\"/33/on\"><button class=\"button\">ON</button></a></p>");
            } else {
              client.println("<p><a href=\"/33/off\"><button class=\"button button2\">OFF</button></a></p>");
            }
            client.println("</body></html>");
            
            // The HTTP response ends with another blank line
            client.println();
            // Break out of the while loop
            break;
          } else { // if you got a newline, then clear currentLine
            currentLine = "";
          }
        } else if (c != '\r') {  // if you got anything else but a carriage return character,
          currentLine += c;      // add it to the end of the currentLine
        }
      }
    }
    // Clear the header variable
    header = "";
    // Close the connection
    client.stop();
    Serial.println("Client disconnected.");
    Serial.println("");
  }
}

访问串口打印出来的 Web Server IP 可点击按钮控制两个GPIO状态,控制板载 LED 灯亮灭。

image-20211221113902834

例程:FSBrowser 文件浏览器

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->WebServer\examples\FSBrowser)

/*
  FSWebServer - Example WebServer with FS backend for esp8266/esp32
  Copyright (c) 2015 Hristo Gochkov. All rights reserved.
  This file is part of the WebServer library for Arduino environment.

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.
  This library is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.
  You should have received a copy of the GNU Lesser General Public
  License along with this library; if not, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

  upload the contents of the data folder with MkSPIFFS Tool ("ESP32 Sketch Data Upload" in Tools menu in Arduino IDE)
  or you can upload the contents of a folder if you CD in that folder and run the following command:
  for file in `ls -A1`; do curl -F "file=@$PWD/$file" esp32fs.local/edit; done

  access the sample web page at http://esp32fs.local
  edit the page by going to http://esp32fs.local/edit
*/
#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <ESPmDNS.h>

#define FILESYSTEM SPIFFS
// You only need to format the filesystem once
#define FORMAT_FILESYSTEM false
#define DBG_OUTPUT_PORT Serial
#define A0 34
#if FILESYSTEM == FFat
#include <FFat.h>
#endif
#if FILESYSTEM == SPIFFS
#include <SPIFFS.h>
#endif

const char* ssid = "wifi-ssid";
const char* password = "wifi-password";
const char* host = "esp32fs";
WebServer server(80);
//holds the current upload
File fsUploadFile;

//format bytes
String formatBytes(size_t bytes) {
  if (bytes < 1024) {
    return String(bytes) + "B";
  } else if (bytes < (1024 * 1024)) {
    return String(bytes / 1024.0) + "KB";
  } else if (bytes < (1024 * 1024 * 1024)) {
    return String(bytes / 1024.0 / 1024.0) + "MB";
  } else {
    return String(bytes / 1024.0 / 1024.0 / 1024.0) + "GB";
  }
}

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 exists(String path){
  bool yes = false;
  File file = FILESYSTEM.open(path, "r");
  if(!file.isDirectory()){
    yes = true;
  }
  file.close();
  return yes;
}

bool handleFileRead(String path) {
  DBG_OUTPUT_PORT.println("handleFileRead: " + path);
  if (path.endsWith("/")) {
    path += "index.htm";
  }
  String contentType = getContentType(path);
  String pathWithGz = path + ".gz";
  if (exists(pathWithGz) || exists(path)) {
    if (exists(pathWithGz)) {
      path += ".gz";
    }
    File file = FILESYSTEM.open(path, "r");
    server.streamFile(file, contentType);
    file.close();
    return true;
  }
  return false;
}

void handleFileUpload() {
  if (server.uri() != "/edit") {
    return;
  }
  HTTPUpload& upload = server.upload();
  if (upload.status == UPLOAD_FILE_START) {
    String filename = upload.filename;
    if (!filename.startsWith("/")) {
      filename = "/" + filename;
    }
    DBG_OUTPUT_PORT.print("handleFileUpload Name: "); DBG_OUTPUT_PORT.println(filename);
    fsUploadFile = FILESYSTEM.open(filename, "w");
    filename = String();
  } else if (upload.status == UPLOAD_FILE_WRITE) {
    //DBG_OUTPUT_PORT.print("handleFileUpload Data: "); DBG_OUTPUT_PORT.println(upload.currentSize);
    if (fsUploadFile) {
      fsUploadFile.write(upload.buf, upload.currentSize);
    }
  } else if (upload.status == UPLOAD_FILE_END) {
    if (fsUploadFile) {
      fsUploadFile.close();
    }
    DBG_OUTPUT_PORT.print("handleFileUpload Size: "); DBG_OUTPUT_PORT.println(upload.totalSize);
  }
}

void handleFileDelete() {
  if (server.args() == 0) {
    return server.send(500, "text/plain", "BAD ARGS");
  }
  String path = server.arg(0);
  DBG_OUTPUT_PORT.println("handleFileDelete: " + path);
  if (path == "/") {
    return server.send(500, "text/plain", "BAD PATH");
  }
  if (!exists(path)) {
    return server.send(404, "text/plain", "FileNotFound");
  }
  FILESYSTEM.remove(path);
  server.send(200, "text/plain", "");
  path = String();
}

void handleFileCreate() {
  if (server.args() == 0) {
    return server.send(500, "text/plain", "BAD ARGS");
  }
  String path = server.arg(0);
  DBG_OUTPUT_PORT.println("handleFileCreate: " + path);
  if (path == "/") {
    return server.send(500, "text/plain", "BAD PATH");
  }
  if (exists(path)) {
    return server.send(500, "text/plain", "FILE EXISTS");
  }
  File file = FILESYSTEM.open(path, "w");
  if (file) {
    file.close();
  } else {
    return server.send(500, "text/plain", "CREATE FAILED");
  }
  server.send(200, "text/plain", "");
  path = String();
}

void handleFileList() {
  if (!server.hasArg("dir")) {
    server.send(500, "text/plain", "BAD ARGS");
    return;
  }

  String path = server.arg("dir");
  DBG_OUTPUT_PORT.println("handleFileList: " + path);


  File root = FILESYSTEM.open(path);
  path = String();

  String output = "[";
  if(root.isDirectory()){
      File file = root.openNextFile();
      while(file){
          if (output != "[") {
            output += ',';
          }
          output += "{\"type\":\"";
          output += (file.isDirectory()) ? "dir" : "file";
          output += "\",\"name\":\"";
          output += String(file.path()).substring(1);
          output += "\"}";
          file = root.openNextFile();
      }
  }
  output += "]";
  server.send(200, "text/json", output);
}

void setup(void) {
  DBG_OUTPUT_PORT.begin(115200);
  DBG_OUTPUT_PORT.print("\n");
  DBG_OUTPUT_PORT.setDebugOutput(true);
  if (FORMAT_FILESYSTEM) FILESYSTEM.format();
  FILESYSTEM.begin();
  {
      File root = FILESYSTEM.open("/");
      File file = root.openNextFile();
      while(file){
          String fileName = file.name();
          size_t fileSize = file.size();
          DBG_OUTPUT_PORT.printf("FS File: %s, size: %s\n", fileName.c_str(), formatBytes(fileSize).c_str());
          file = root.openNextFile();
      }
      DBG_OUTPUT_PORT.printf("\n");
  }


  //WIFI INIT
  DBG_OUTPUT_PORT.printf("Connecting to %s\n", ssid);
  if (String(WiFi.SSID()) != String(ssid)) {
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);
  }

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    DBG_OUTPUT_PORT.print(".");
  }
  DBG_OUTPUT_PORT.println("");
  DBG_OUTPUT_PORT.print("Connected! IP address: ");
  DBG_OUTPUT_PORT.println(WiFi.localIP());

  MDNS.begin(host);
  DBG_OUTPUT_PORT.print("Open http://");
  DBG_OUTPUT_PORT.print(host);
  DBG_OUTPUT_PORT.println(".local/edit to see the file browser");


  //SERVER INIT
  //list directory
  server.on("/list", HTTP_GET, handleFileList);
  //load editor
  server.on("/edit", HTTP_GET, []() {
    if (!handleFileRead("/edit.htm")) {
      server.send(404, "text/plain", "FileNotFound");
    }
  });
  //create file
  server.on("/edit", HTTP_PUT, handleFileCreate);
  //delete file
  server.on("/edit", HTTP_DELETE, handleFileDelete);
  //first callback is called after the request has ended with all parsed arguments
  //second callback handles file uploads at that location
  server.on("/edit", HTTP_POST, []() {
    server.send(200, "text/plain", "");
  }, handleFileUpload);

  //called when the url is not defined here
  //use it to load content from FILESYSTEM
  server.onNotFound([]() {
    if (!handleFileRead(server.uri())) {
      server.send(404, "text/plain", "FileNotFound");
    }
  });

  //get heap status, analog input value and all GPIO statuses in one json call
  server.on("/all", HTTP_GET, []() {
    String json = "{";
    json += "\"heap\":" + String(ESP.getFreeHeap());
    json += ", \"analog\":" + String(analogRead(A0));
    json += ", \"gpio\":" + String((uint32_t)(0));
    json += "}";
    server.send(200, "text/json", json);
    json = String();
  });
  server.begin();
  DBG_OUTPUT_PORT.println("HTTP server started");

}

void loop(void) {
  server.handleClient();
  delay(2);//allow the cpu to switch to other tasks
}
安装 ESP32 Sketch Data Upload

要使用FSBrowser,首先需要在Arduino IDE上安装一个文件上传的小工具。

  • 确保使用受支持的Arduino IDE版本之一,并已安装ESP32内核。

  • 最新版本下载esp32fs.zip压缩工具

    image-20210513173004079

  • 在您的Arduino sketchbook目录中,创建一个tools目录(如果尚不存在)。

  • 将工具解压缩到“ Arduino安装目录-> Arduino” / tools目录中。例如:<home_dir>/Arduino/tools/ESP32FS/tool/esp32fs.jar

    image-20210513180126454

    或在OSX上/Applications/Arduino.app/Contents/Java/tools/ESP32FS

  • 确保在esp32核心安装文件夹中有mklittlefs [.exe]mkfatfs [.exe]。在**\ AppData \ Local \ Arduino15 …内部**或在zip IDE安装上查看,请参阅“Setup->sketchbook location”hardware\espressif\esp32\tools

  • 作为参考,请参阅以前的发行版,以获取有关已归档二进制文件的副本。

  • 您还可以使用提供的package_esp32_index.template.json来运行get.py并下载缺少的二进制文件

  • 重新启动Arduino IDE。

使用方法
  • 确保已选择board,port,分区方案并关闭了串行监视器。打开FSBrowser.ino,Board信息栏,Flash Size : 4MB,Partition Scheme : Default 4MB with spiffs(1.2MB APP/1.5MB SPIFFS)

    image-20210420192549225

  • 将 FSBrowser.ino 代码下载到 Edge101WE 主板。

  • 选择Tools > ESP32 Sketch Data Upload菜单项。

    image-20210420192705241

  • 在下拉列表中,从/ data文件夹中选择要创建的SPIFFS,LittleFS或FatFS。这里选择SPIFFS。

image-20210513181239051

  • 单击 “确定” 开始将 这个FSBrowser Sketch 目录下 data 文件夹中的文件上传到ESP32 Flash文件系统中。

  • 最后还有一个 !Erase Flash!选项允许在必要时清除整个flash,请谨慎使用。

    完成后,IDE状态栏将显示“图像已上传”消息的状态。对于大型文件系统,可能需要几分钟的时间。

上传项目中的数据。当上传完毕后回显示SPIFFS Image Uploaded。

image-20210420192822507

实际上,上传是在项目下面的一个 data 文件夹的内容。里面包含了一些htm文件、图片和js脚本。

image-20210420192951410

上传完毕后访问 http://esp32fs.local/edit 即可浏览到上传的文件。

image-20210420193035802

还可以通过 选择文件 按钮上传其他的文件,点击 Upload上传。这样我们在编写web服务器程序时就可以通过 esp32FS 工具和 FSBrowser 来制作和上传网页所需要的文件。

例程:HttpAdvancedAuth 高级用户认证

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->WebServer\examples\HttpAdvancedAuth)

/*
  HTTP Advanced Authentication example
  Created Mar 16, 2017 by Ahmed El-Sharnoby.
  This example code is in the public domain.
*/

#include <WiFi.h>
#include <ESPmDNS.h>
#include <ArduinoOTA.h>
#include <WebServer.h>

const char *ssid = "your_ssid";
const char *password = "your_password";

WebServer server(80);

const char* www_username = "admin";
const char* www_password = "esp32";
// allows you to set the realm of authentication Default:"Login Required"
const char* www_realm = "Custom Auth Realm";
// the Content of the HTML response in case of Unautherized Access Default:empty
String authFailResponse = "Authentication Failed";

void setup() {
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  if (WiFi.waitForConnectResult() != WL_CONNECTED) {
    Serial.println("WiFi Connect Failed! Rebooting...");
    delay(1000);
    ESP.restart();
  }
  ArduinoOTA.begin();

  server.on("/", []() {
    if (!server.authenticate(www_username, www_password))
      //Basic Auth Method with Custom realm and Failure Response
      //return server.requestAuthentication(BASIC_AUTH, www_realm, authFailResponse);
      //Digest Auth Method with realm="Login Required" and empty Failure Response
      //return server.requestAuthentication(DIGEST_AUTH);
      //Digest Auth Method with Custom realm and empty Failure Response
      //return server.requestAuthentication(DIGEST_AUTH, www_realm);
      //Digest Auth Method with Custom realm and Failure Response
    {
      return server.requestAuthentication(DIGEST_AUTH, www_realm, authFailResponse);
    }
    server.send(200, "text/plain", "Login OK");
  });
  server.begin();

  Serial.print("Open http://");
  Serial.print(WiFi.localIP());
  Serial.println("/ in your browser to see it working");
}

void loop() {
  ArduinoOTA.handle();
  server.handleClient();
  delay(2);//allow the cpu to switch to other tasks
}

用户认证有以下两种方式:

Basic 认证

Basic 认证是一种较为简单的HTTP认证方式,客户端通过明文(Base64 编码格式)传输用户名和密码到服务端进行认证,通常需要配合HTTPS来保证信息传输的安全。输入正确的用户名和密码,认证成功后,浏览器会将凭据信息缓存起来,那么以后再进入时,无需重复手动输入用户名和密码。

Basic 认证是通过将“用户名:密码”格式的字符串经过的Base64编码得到的。而 Base64 不属于加密范畴,可以被逆向解码,等同于明文,因此Basic传输认证信息是不安全的。

Digest 认证(摘要认证)

Digest 认证是为了修复基本认证协议的严重缺陷而设计的,秉承 “绝不通过明文在网络发送密码” 的原则,通过 “密码摘要” 进行认证,大大提高了安全性。Digest 认证同样使用质询 / 响应的方式(challenge/response),但不会像 Basic 认证那样直接发送明文密码。所谓质询响应方式是指,一开始一方会先发送认证要求给另一方,接着使用从另一方那接收到的质询码计算生成响应码。最后将响应码返回给对方进行认证的方式。

相对于Basic 认证,Digest 认证主要有如下改进:

  • 绝不通过明文在网络上发送密码

  • 可以有效防止恶意用户进行重放攻击

  • 可以有选择的防止对报文内容的篡改

需要注意的是,摘要认证除了能够保护密码之外,并不能保护其他内容,与HTTPS配合使用仍是一个良好的选择。

Digest 认证涉及到的参数的含义:

Digest 认证的参数 含义
WWW-Authentication 用来定义使用何种方式(Basic、Digest、Bearer等)去进行认证以获取受保护的资源
realm 表示Web服务器中受保护文档的安全域(比如公司财务信息域和公司员工信息域),用来指示需要哪个域的用户名和密码
qop 保护质量,包含auth(默认的)和auth-int(增加了报文完整性检测)两种策略,(可以为空,但是)不推荐为空值
nonce 服务端向客户端发送质询时附带的一个随机数,这个数会经常发生变化。客户端计算密码摘要时将其附加上去,使得多次生成同一用户的密码摘要各不相同,用来防止重放攻击
nc nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量。例如,在响应的第一个请求中,客户端将发送“nc=00000001”。这个指示值的目的是让服务器保持这个计数器的一个副本,以便检测重复的请求
cnonce 客户端随机数,这是一个不透明的字符串值,由客户端提供,并且客户端和服务器都会使用,以避免用明文文本。这使得双方都可以查验对方的身份,并对消息的完整性提供一些保护
response 这是由用户代理软件计算出的一个字符串,以证明用户知道口令
Authorization-Info 用于返回一些与授权会话相关的附加信息
nextnonce 下一个服务端随机数,使客户端可以预先发送正确的摘要
rspauth 响应摘要,用于客户端对服务端进行认证
stale 当密码摘要使用的随机数过期时,服务器可以返回一个附带有新随机数的401响应,并指定stale=true,表示服务器在告知客户端用新的随机数来重试,而不再要求用户重新输入用户名和密码了

加入认证机制后,浏览器访问串口打印出来的 IP 地址,提示需要输入用户名和密码,用户名输入 admin ,密码输入 esp32。

登录后 页面显示 Login OK 。

例程:SimpleAuthentification 鉴权

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->WebServer\examples\SimpleAuthentification)

#include <WiFiClient.h>
#include <WebServer.h>

//#define ETH_MODE   //Using Ethernet
#ifdef ETH_MODE
#include <ETH.h>
static bool eth_connected = false;
void ETHEvent(WiFiEvent_t event)
{
  switch (event) {
    case ARDUINO_EVENT_ETH_START:
      //Serial.println("ETH Started");
      //set eth hostname here
      ETH.setHostname("esp32-ethernet");
      break;
    case ARDUINO_EVENT_ETH_CONNECTED:
      //Serial.println("ETH Connected");
      break;
    case ARDUINO_EVENT_ETH_GOT_IP:
      eth_connected = true;
      break;
    case ARDUINO_EVENT_ETH_DISCONNECTED:
      Serial.println("ETH Disconnected");
      eth_connected = false;
      break;
    case ARDUINO_EVENT_ETH_STOP:
      Serial.println("ETH Stopped");
      eth_connected = false;
      break;
    default:
      break;
  }
}
#else
#include <WiFi.h>
const char *ssid = "your_ssid";
const char *password = "your_password";
#endif
WebServer server(80);

//Check if header is present and correct
bool is_authentified() {
  Serial.println("Enter is_authentified");
  if (server.hasHeader("Cookie")) {
    Serial.print("Found cookie: ");
    String cookie = server.header("Cookie");
    Serial.println(cookie);
    if (cookie.indexOf("ESPSESSIONID=1") != -1) {
      Serial.println("Authentification Successful");
      return true;
    }
  }
  Serial.println("Authentification Failed");
  return false;
}

//login page, also called for disconnect
void handleLogin() {
  String msg;
  if (server.hasHeader("Cookie")) {
    Serial.print("Found cookie: ");
    String cookie = server.header("Cookie");
    Serial.println(cookie);
  }
  if (server.hasArg("DISCONNECT")) {
    Serial.println("Disconnection");
    server.sendHeader("Location", "/login");
    server.sendHeader("Cache-Control", "no-cache");
    server.sendHeader("Set-Cookie", "ESPSESSIONID=0");
    server.send(301);
    return;
  }
  if (server.hasArg("USERNAME") && server.hasArg("PASSWORD")) {
    if (server.arg("USERNAME") == "admin" &&  server.arg("PASSWORD") == "admin") {
      server.sendHeader("Location", "/");
      server.sendHeader("Cache-Control", "no-cache");
      server.sendHeader("Set-Cookie", "ESPSESSIONID=1");
      server.send(301);
      Serial.println("Log in Successful");
      return;
    }
    msg = "Wrong username/password! try again.";
    Serial.println("Log in Failed");
  }
  String content = "<html><body><form action='/login' method='POST'>To log in, please use : admin/admin<br>";
  content += "User:<input type='text' name='USERNAME' placeholder='user name'><br>";
  content += "Password:<input type='password' name='PASSWORD' placeholder='password'><br>";
  content += "<input type='submit' name='SUBMIT' value='Submit'></form>" + msg + "<br>";
  content += "You also can go <a href='/inline'>here</a></body></html>";
  server.send(200, "text/html", content);
}

//root page can be accessed only if authentification is ok
void handleRoot() {
  Serial.println("Enter handleRoot");
  String header;
  if (!is_authentified()) {
    server.sendHeader("Location", "/login");
    server.sendHeader("Cache-Control", "no-cache");
    server.send(301);
    return;
  }
  String content = "<html><body><H2>hello, you successfully connected to esp32!</H2><br>";
  if (server.hasHeader("User-Agent")) {
    content += "the user agent used is : " + server.header("User-Agent") + "<br><br>";
  }
  content += "You can access this page until you <a href=\"/login?DISCONNECT=YES\">disconnect</a></body></html>";
  server.send(200, "text/html", content);
}

//no need authentification
void handleNotFound() {
  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);
}

void setup(void) {
  Serial.begin(115200);

#ifdef ETH_MODE
  WiFi.onEvent(ETHEvent);
  ETH.begin();
  Serial.print("Please wait for Ethernet connection");
  while(!eth_connected){
    delay(1000);
    Serial.print(".");
  }
  Serial.println("succeed");
  Serial.print("ETH MAC: ");
  Serial.print(ETH.macAddress());
  Serial.print(", IPv4: ");
  Serial.print(ETH.localIP());
  if (ETH.fullDuplex()) {
    Serial.print(", FULL_DUPLEX");
  }
  Serial.print(", ");
  Serial.print(ETH.linkSpeed());
  Serial.println("Mbps");
#else
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.println("");

  // Wait for connection
  Serial.print("Please wait for WiFi connection");
  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());
#endif
  server.on("/", handleRoot);
  server.on("/login", handleLogin);
  server.on("/inline", []() {
    server.send(200, "text/plain", "this works without need of authentification");
  });

  server.onNotFound(handleNotFound);
  //here the list of headers to be recorded
  const char * headerkeys[] = {"User-Agent", "Cookie"} ;
  size_t headerkeyssize = sizeof(headerkeys) / sizeof(char*);
  //ask server to track these headers
  server.collectHeaders(headerkeys, headerkeyssize);
  server.begin();
  Serial.println("HTTP server started");
}

void loop(void) {
  server.handleClient();
  delay(2);//allow the cpu to switch to other tasks
}

访问 串口打印出来的IP地址,显示登录界面,只有输入正确的密码才能跳转到 主页。

image-20210421151202898

9.4.3 异步 Web Server

使用异步网络的优点:

  • 使用异步网络意味着您可以同时处理多个连接。

  • 一旦请求准备就绪并解析完毕,您就会被叫回。

  • 当您发送响应时,您将立即准备处理其他连接,而服务器将在后台处理响应,当您发送响应时,您立即就可以处理其他连接,而服务器则负责在后台发送响应。

  • 速度快。

  • 易于使用的API,HTTP基本和摘要MD5身份验证(默认),ChunkedResponse易于使用的API,HTTP基本和摘要MD5身份验证(可选),ChunkedResponse。

  • 易于扩展以处理任何类型的内容很容易扩展以处理任何类型的内容。

  • 支持继续100支持继续100。

  • 异步WebSocket插件无需其他服务器或端口即可提供不同的位置。异步WebSocket插件提供不同的位置,没有额外的服务器或端口。

  • Async EventSource(服务器发送事件)插件可将事件发送到浏览器Asynceventsource(服务器发送事件)插件,用于向浏览器发送事件

  • URL重写插件的条件和永久的URL重写重写URL插件的条件和永久的URL重写

  • ServeStatic插件,支持缓存,Last-Modified,默认索引以及更多ServeStatic插件,支持缓存,最后修改,替代索引和更多

  • 用于处理模板的简单模板处理引擎

需要注意的事情:

  • 这是完全异步的服务器,因此不会在循环线程上运行。

  • 您不能在回调中使用yield或delay或使用它们的任何函数。

  • 服务器足够智能,可以知道何时关闭连接并释放资源。

  • 您不能对一个请求发送多个回复。

详细介绍可查看下面的github介绍。

异步 Web Server需要安装以下两个库:

要下载这两个库,只需单击GitHub页面顶部的“ Clone or download ”按钮。

image-20210421185340915

然后,选择“ Download ZIP ”选项,文件应被下载到您的计算机上。只需打开**.zip**文件并将文件夹解压缩到Arduino库文件夹即可。

通常,用于Arduino安装的库文件夹位于 C:\ Users \ UserName \ Documents \ Arduino \ libraries文件夹中。

请注意,提取的文件夹名称末尾带有**-master**。只需删除此附加的**-master**并保留其余名称即可。

重新打开Arduino IDE ,这些库应该可以在Arduino环境中使用。

例程:simple_server

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->ESPAsyncWebServer\examples\simple_server)

//
// A simple server implementation showing how to:
//  * serve static messages
//  * read GET and POST parameters
//  * handle missing pages / 404s
//

#include <Arduino.h>
#ifdef ESP32
#include <WiFi.h>
#include <AsyncTCP.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#endif
#include <ESPAsyncWebServer.h>

AsyncWebServer server(80);

const char* ssid = "YOUR_SSID";
const char* password = "YOUR_PASSWORD";

const char* PARAM_MESSAGE = "message";

void notFound(AsyncWebServerRequest *request) {
    request->send(404, "text/plain", "Not found");
}

void setup() {

    Serial.begin(115200);
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);
    if (WiFi.waitForConnectResult() != WL_CONNECTED) {
        Serial.printf("WiFi Failed!\n");
        return;
    }

    Serial.print("IP Address: ");
    Serial.println(WiFi.localIP());

    server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
        request->send(200, "text/plain", "Hello, world");
    });

    // Send a GET request to <IP>/get?message=<message>
    server.on("/get", HTTP_GET, [] (AsyncWebServerRequest *request) {
        String message;
        if (request->hasParam(PARAM_MESSAGE)) {
            message = request->getParam(PARAM_MESSAGE)->value();
        } else {
            message = "No message sent";
        }
        request->send(200, "text/plain", "Hello, GET: " + message);
    });

    // Send a POST request to <IP>/post with a form field message set to <message>
    server.on("/post", HTTP_POST, [](AsyncWebServerRequest *request){
        String message;
        if (request->hasParam(PARAM_MESSAGE, true)) {
            message = request->getParam(PARAM_MESSAGE, true)->value();
        } else {
            message = "No message sent";
        }
        request->send(200, "text/plain", "Hello, POST: " + message);
    });

    server.onNotFound(notFound);

    server.begin();
}

void loop() {
}

程序将从串口打印出Web Server的IP。使用网页浏览器访问 这个IP。可显示网络主页的信息。也可访问 /get 或 /post 。

例程:通过滑条来控制LED亮度

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

/*********
  Rui Santos
  Complete project details at https://RandomNerdTutorials.com/esp32-web-server-slider-pwm/
  
  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files.
  
  The above copyright notice and this permission notice shall be included in all
  copies or substantial portions of the Software.
*********/

// Import required libraries
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>

// Replace with your network credentials
const char *ssid = "your_ssid";
const char *password = "your_password";

const int output = 15;

String sliderValue = "0";

// setting PWM properties
const int freq = 5000;
const int ledChannel = 0;
const int resolution = 8;

const char* PARAM_INPUT = "value";

// Create AsyncWebServer object on port 80
AsyncWebServer server(80);

const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Edge101WE Web Server</title>
  <style>
    html {font-family: Arial; display: inline-block; text-align: center;}
    h2 {font-size: 2.3rem;}
    p {font-size: 1.9rem;}
    body {max-width: 400px; margin:0px auto; padding-bottom: 25px;}
    .slider { -webkit-appearance: none; margin: 14px; width: 360px; height: 25px; background: #FFD65C;
      outline: none; -webkit-transition: .2s; transition: opacity .2s;}
    .slider::-webkit-slider-thumb {-webkit-appearance: none; appearance: none; width: 35px; height: 35px; background: #003249; cursor: pointer;}
    .slider::-moz-range-thumb { width: 35px; height: 35px; background: #003249; cursor: pointer; } 
  </style>
</head>
<body>
  <h2>Edge101WE Web Server</h2>
  <p><span id="textSliderValue">%SLIDERVALUE%</span></p>
  <p><input type="range" onchange="updateSliderPWM(this)" id="pwmSlider" min="0" max="255" value="%SLIDERVALUE%" step="1" class="slider"></p>
<script>
function updateSliderPWM(element) {
  var sliderValue = document.getElementById("pwmSlider").value;
  document.getElementById("textSliderValue").innerHTML = sliderValue;
  console.log(sliderValue);
  var xhr = new XMLHttpRequest();
  xhr.open("GET", "/slider?value="+sliderValue, true);
  xhr.send();
}
</script>
</body>
</html>
)rawliteral";

// Replaces placeholder with button section in your web page
String processor(const String& var){
  //Serial.println(var);
  if (var == "SLIDERVALUE"){
    return sliderValue;
  }
  return String();
}

void setup(){
  // Serial port for debugging purposes
  Serial.begin(115200);
  
  // configure LED PWM functionalitites
  ledcSetup(ledChannel, freq, resolution);
  
  // attach the channel to the GPIO to be controlled
  ledcAttachPin(output, ledChannel);
  
  ledcWrite(ledChannel, sliderValue.toInt());

  // Connect to Wi-Fi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi..");
  }

  // Print ESP Local IP Address
  Serial.println(WiFi.localIP());

  // Route for root / web page
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/html", index_html, processor);
  });

  // Send a GET request to <ESP_IP>/slider?value=<inputMessage>
  server.on("/slider", HTTP_GET, [] (AsyncWebServerRequest *request) {
    String inputMessage;
    // GET input1 value on <ESP_IP>/slider?value=<inputMessage>
    if (request->hasParam(PARAM_INPUT)) {
      inputMessage = request->getParam(PARAM_INPUT)->value();
      sliderValue = inputMessage;
      ledcWrite(ledChannel, sliderValue.toInt());
    }
    else {
      inputMessage = "No message sent";
    }
    Serial.println(inputMessage);
    request->send(200, "text/plain", "OK");
  });
  
  // Start server
  server.begin();
}
  
void loop() {
  
}

访问 串口打印出来的 Web Server IP 可拖动滚动条控制LED灯亮度。

image-20211221141642818

其他异步Web Server例程

例程: 网络串口 Web Serial

此例程可生成基于 Web 的串行监视器,可用于远程的调试。

例程:客户端显示自动更新

本教程展示了如何在使用 Arduino IDE 编程的 ESP32 Web 服务器中使用服务器发送事件 (SSE)。SSE 允许浏览器通过 HTTP 连接从服务器接收自动更新。例如,这对于将更新的传感器读数发送到浏览器很有用。每当有新的读数可用时,ESP32 就会将其发送给客户端,网页可以自动更新,而无需发出额外请求。

例程:ESP32 Web Server using SPIFFS

在本教程中,展示如何构建一个 Web 服务器,为存储在 ESP32 文件系统上的 HTML 和 CSS 文件提供服务。不必将 HTML 和 CSS 文本写入 Arduino 草图,我们将创建分离的 HTML 和 CSS 文件。

9.5 DNSServer

9.5.1 DNSServer

前面讲 WebServer 的时候都是通过 IP 地址去访问的,如果想像一般上网那样输入域名(www.baidu.com、www.taobao.com等)访问的话就需要用到DNSServer了。本文对 Arduino core for the ESP32中DNSServer 使用进行简单介绍。

使用 DNSServer 必须使设备处于AP 模式下,在非 AP 模式下想实现同样功能的话请参考 mDNS。 mDNS 可以在非 A P模式下使用但也有局限,局域网中其它设备也必须开启 mDNS 服务互相间才能通过域名访问。

DNSServer使用步骤如下:

  1. 引入相应库 #include <DNSServer.h>;

  2. 声明 DNSServer 对象;

  3. 使用 start() 方法启动DNS服务器;

  4. 使用 processNextRequest() 方法处理来自客户端的请求;

API 参考

start() - 启动DNSServer
bool start(const uint16_t &port, const String &domainName, const IPAddress &resolvedIP);

启动DNSServer,分别需要填入端口号、域名、IP,域名可以填写 * 表示所有域名都会被跳转至这里。

语法

const byte DNS_PORT = 53; // 默认设置端口为53 
IPAddress apIP(192, 168, 1, 1);

dnsServer.start(DNS_PORT, "www.example.com", apIP);

参数

传入值 说明 值范围
port DNS服务端口号。默认情况下设置为53。
domainName 映射的域名,也就是开启服务后可以直接访问的用于代替IP地址的域名。
resolvedIP 映射的IP地址

返回

返回值 说明 值范围
bool true:启动DNS服务成功
false:启动DNS服务失败
stop() - 停止DNSServer
void stop()

语法

dnsServer.stop();

参数

返回

返回值 说明 值范围
bool true:关闭DNS服务成功
false:关闭DNS服务失败
processNextRequest() - 处理来自客户端的请求
void processNextRequest()

语法

void loop() {
  dnsServer.processNextRequest(); // 处理DNS请求服务
  webServer.handleClient();
}

参数

返回

setErrorReplyCode() - 设置错误响应码
void setErrorReplyCode(const DNSReplyCode &replyCode);

当客户端发送的查询域名不在主板建立的DNS服务器检索中,则返回setErrorReplyCode函数设置的错误代码。

语法

dnsServer.setErrorReplyCode(DNSReplyCode::ServerFailure);

参数

传入值 说明 值范围
DNSReplyCode DNS响应错误码 NoError = 0, //DNS查询成功完成
FormError = 1, //DNS查询格式错误
ServerFailure = 2, //服务器无法完成DNS请求
NonExistentDomain = 3, //域名不存在
NotImplemented = 4, //未定义
Refused = 5, //服务器拒绝回答查询
YXDomain = 6, //名称不应该存在,但该名称确实存在
YXRRSet = 7, //资源记录集不存在
NXRRSet = 8 //服务器对该区域无权

返回

setTTL() - 设置TTL
void setTTL(const uint32_t &ttl);

通过增大TTL值,可以减少DNS递归查询过程,达到提升域名解析速度的效果。反之,通过缩小 TTL 值,以减少更换空间IP地址时造成的不可访问时间,减小空间IP地址造成的访问空窗期。根据不同应用场景和网络需求可以选择合适的TTL值达成更好的访问体验。

语法

dnsServer.setTTL(300);

参数

传入值 说明 值范围
ttl 设置TTL数值(单位:秒)

返回

例程:CaptivePortal

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->DNSServer\examples\CaptivePortal)

#include <WiFi.h>
#include <DNSServer.h>

const byte DNS_PORT = 53;
IPAddress apIP(192,168,4,1); // The default android DNS
DNSServer dnsServer;
WiFiServer server(80);

String responseHTML = ""
  "<!DOCTYPE html><html><head><title>CaptivePortal</title></head><body>"
  "<h1>Hello World!</h1><p>This is a captive portal example. All requests will "
  "be redirected here.</p></body></html>";

void setup() { 
  WiFi.disconnect();
  WiFi.mode(WIFI_AP);
  WiFi.softAP("Edge101WE_DNSServer");
  WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0));

  // if DNSServer is started with "*" for domain name, it will reply with
  // provided IP to all DNS request
  dnsServer.start(DNS_PORT, "edge.com", apIP);

  server.begin();
}

void loop() {
  dnsServer.processNextRequest();
  WiFiClient client = server.available();   // listen for incoming clients

  if (client) {
    String currentLine = "";
    while (client.connected()) {
      if (client.available()) {
        char c = client.read();
        if (c == '\n') {
          if (currentLine.length() == 0) {
            client.println("HTTP/1.1 200 OK");
            client.println("Content-type:text/html");
            client.println();
            client.print(responseHTML);
            break;
          } else {
            currentLine = "";
          }
        } else if (c != '\r') {
          currentLine += c;
        }
      }
    }
    client.stop();
  }
}

例程首先建立一个名字为 “Edge101WE_DNSServer” 的 WiFi AP ,然后启动DNSServer,设置一个域名 “edge.com” 。当电脑连接到这个 AP后,输入域名即可访问。

image-20211221144250481

9.5.2 mDNS 通过STA方式

mDNS 即多播DNS(Multicast DNS),mDNS主要实现了在没有传统 DNS 服务器的情况下使局域网内的主机实现相互发现和通信,使用的端口为 5353,遵从DNS协议,使用现有的 DNS 信息结构、名语法和资源记录类型。并且没有指定新的操作代码或响应代码。在局域网中,设备和设备之前相互通信需要知道对方的ip地址的,大多数情况,设备的 IP 不是静态IP地址,而是通过 DHCP 协议动态分配的 IP 地址,如何设备发现呢,就是要 mDNS 大显身手,例如:现在物联网设备和APP之间的通信,要么APP通过广播,要么通过组播,发一些特定信息,感兴趣设备应答,实现局域网设备的发现,当然 mDNS 比这强大。

mDNS 的工作原理 首先,在 IP 协议里规定了一些保留地址,其中有一个是 224.0.0.251,对应的 IPv6 地址是 [FF02::FB]。

mDNS 协议规定了一个端口,5353。mDNS 基于 UDP 协议。

每个进入局域网的主机,如果开启了mDNS服务的话,都会向局域网内的所有主机组播一个消息,我是谁,和我的IP地址是多少。然后其他也有该服务的主机就会响应,也会告诉你,它是谁,它的IP地址是多少。

比如,A主机进入局域网,开启了 mDNS 服务,并向 mDNS 服务注册以下信息:我提供 FTP 服务,我的IP是 192.168.1.101,端口是 21。当B主机进入局域网,并向 B 主机的 mDNS 服务请求,我要找局域网内 FTP 服务器,B主机的 mDNS 就会去局域网内向其他的 mDNS 询问,并且最终告诉你,有一个IP地址为 192.168.1.101,端口号是 21 的主机,也就是 A 主机提供 FTP 服务,所以 B 主机就知道了 A 主机的 IP 地址和端口号了。

在Apple 的设备上(电脑,笔记本,iphone,ipad等设备)都提供了这个服务。很多Linux设备也提供这个服务。Windows的设备可能没有提供,但是如果安装了iTunes之类的软件的话,也提供了这个服务。**注意安卓系统不支持 mDNS协议 **。

这样就可以利用这个服务开发一些局域网内的自动发现,然后提供一些局域网内交互的应用了。

在 Windows PC 需要安装 Apple 的 Bonjour Print Services for Windows

安装好以后可以在 命令提示符界面输入dns-sd,如果安装成功会输出功能列表。

image-20210407110034549

API参考

begin() - 开启mDNS服务
bool begin(const char* hostName);

语法

#include <ESPmDNS.h>
const char* host = "esp32";

if (!MDNS.begin(host)) { //http://esp32.local
    Serial.println("Error setting up MDNS responder!");
    while (1) {
        delay(1000);
    }
}

参数

传入值 说明 值范围
hostName 要注册的主机名

返回

返回值 说明 值范围
bool true:注册成功。
false:注册不成功
end() - 关闭mDNS服务
void end();

语法

MDNS.end();

参数

返回

addService() - 注册服务
bool addService(char *service, char *proto, uint16_t port);
bool addService(const char *service, const char *proto, uint16_t port){
	return addService((char *)service, (char *)proto, port);
}
bool addService(String service, String proto, uint16_t port){
	return addService(service.c_str(), proto.c_str(), port);
}

bool addServiceTxt(char *name, char *proto, char * key, char * value);
void addServiceTxt(const char *name, const char *proto, const char *key,const char * value){
	addServiceTxt((char *)name, (char *)proto, (char *)key, (char *)value);
}
void addServiceTxt(String name, String proto, String key, String value){
	addServiceTxt(name.c_str(), proto.c_str(), key.c_str(), value.c_str());
}

语法

MDNS.addService("http", "tcp", 80);

参数

传入值 说明 值范围
service 服务名字
proto 服务协议
port 服务端口

返回

返回值 说明 值范围
bool true:注册成功
false:注册失败
queryService() - 查询服务
int queryService(char *service, char *proto);int queryService(const char *service, const char *proto){    return queryService((char *)service, (char *)proto);}int queryService(String service, String proto){    return queryService(service.c_str(), proto.c_str());}

语法

int n = MDNS.queryService(service, proto);

参数

传入值 说明 值范围
service 服务名字
proto 服务协议

返回

返回值 说明 值范围
int 符合条件的服务个数
hostname() - 获取查询服务的主机名
String hostname(int idx);

语法

Serial.print(MDNS.hostname(i));

参数

传入值 说明 值范围
idx 服务索引

返回

返回值 说明 值范围
String 服务域名
IP() - 获取查询服务的IP地址
IPAddress IP(int idx);

语法

Serial.print(MDNS.IP(i));

参数

传入值 说明 值范围
idx 服务索引

返回

返回值 说明 值范围
IPAddress 服务IP地址
port() - 获取查询服务的端口号
uint16_t port(int idx);

语法

Serial.print(MDNS.port(i));

参数

传入值 说明 值范围
idx 服务索引

返回

返回值 说明 值范围
uint16_t 服务端口号

例程:mDNS_Web_Server

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->ESPmDNS\examples\mDNS_Web_Server)

/*
  ESP32 mDNS responder sample

  This is an example of an HTTP server that is accessible
  via http://esp32.local URL thanks to mDNS responder.

  Instructions:
  - Update WiFi SSID and password as necessary.
  - Flash the sketch to the ESP32 board
  - Install host software:
    - For Linux, install Avahi (http://avahi.org/).
    - For Windows, install Bonjour (http://www.apple.com/support/bonjour/).
    - For Mac OSX and iOS support is built in through Bonjour already.
  - Point your browser to http://esp32.local, you should see a response.

 */


#include <WiFi.h>
#include <ESPmDNS.h>
#include <WiFiClient.h>

const char* ssid = "............";
const char* password = "..............";

// TCP server at port 80 will respond to HTTP requests
WiFiServer server(80);

void setup(void)
{  
    Serial.begin(115200);

    // Connect to WiFi network
    WiFi.begin(ssid, password);
    Serial.println("");

    // Wait for connection
    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());

    // Set up mDNS responder:
    // - first argument is the domain name, in this example
    //   the fully-qualified domain name is "esp32.local"
    // - second argument is the IP address to advertise
    //   we send our IP address on the WiFi network
    if (!MDNS.begin("esp32")) {
        Serial.println("Error setting up MDNS responder!");
        while(1) {
            delay(1000);
        }
    }
    Serial.println("mDNS responder started");

    // Start TCP (HTTP) server
    server.begin();
    Serial.println("TCP server started");

    // Add service to MDNS-SD
    MDNS.addService("http", "tcp", 80);
}

void loop(void)
{
    // Check if a client has connected
    WiFiClient client = server.available();
    if (!client) {
        return;
    }
    Serial.println("");
    Serial.println("New client");

    // Wait for data from client to become available
    while(client.connected() && !client.available()){
        delay(1);
    }

    // Read the first line of HTTP request
    String req = client.readStringUntil('\r');

    // First line of HTTP request looks like "GET /path HTTP/1.1"
    // Retrieve the "/path" part by finding the spaces
    int addr_start = req.indexOf(' ');
    int addr_end = req.indexOf(' ', addr_start + 1);
    if (addr_start == -1 || addr_end == -1) {
        Serial.print("Invalid request: ");
        Serial.println(req);
        return;
    }
    req = req.substring(addr_start + 1, addr_end);
    Serial.print("Request: ");
    Serial.println(req);

    String s;
    if (req == "/")
    {
        IPAddress ip = WiFi.localIP();
        String ipStr = String(ip[0]) + '.' + String(ip[1]) + '.' + String(ip[2]) + '.' + String(ip[3]);
        s = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<!DOCTYPE HTML>\r\n<html>Hello from ESP32 at ";
        s += ipStr;
        s += "</html>\r\n\r\n";
        Serial.println("Sending 200");
    }
    else
    {
        s = "HTTP/1.1 404 Not Found\r\n\r\n";
        Serial.println("Sending 404");
    }
    client.print(s);

    client.stop();
    Serial.println("Done with client");
}

访问 http://esp32.local 可显示如下界面

image-20210407110354306

例程:mDNS-SD_Extended

mDNS-SD (DNS Service Discovery)Extended,xmDNS-SD,即为扩展多播DNS服务发现,可用于在本地网络上执行对等发现,这在设计时不知道目标终结点的 IP 地址和主机名时特别有用。如果要建立mDNS,可先通过mDNS-SD服务发现寻找网络内的各个服务的名称,然后在建立mDNS,以避免名字冲突,或者通过服务发现去寻找需要连接的服务,然后建立连接。

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->ESPmDNS\examples\mDNS-SD_Extended)

/*
  ESP8266 mDNS-SD responder and query sample

  This is an example of announcing and finding services.
  
  Instructions:
  - Update WiFi SSID and password as necessary.
  - Flash the sketch to two ESP8266 boards
  - The last one powered on should now find the other.
 */

#include <WiFi.h>
#include <ESPmDNS.h>

const char* ssid = "your_ssid";
const char* password = "your_password";

void setup() {
    Serial.begin(115200);
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(250);
        Serial.print(".");
    }
    Serial.println("");
    Serial.print("Connected to ");
    Serial.println(ssid);
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());

    if (!MDNS.begin("ESP32_Browser")) {
        Serial.println("Error setting up MDNS responder!");
        while(1){
            delay(1000);
        }
    }
}

void loop() {
    browseService("http", "tcp");
    delay(1000);
    browseService("arduino", "tcp");
    delay(1000);
    browseService("workstation", "tcp");
    delay(1000);
    browseService("smb", "tcp");
    delay(1000);
    browseService("afpovertcp", "tcp");
    delay(1000);
    browseService("ftp", "tcp");
    delay(1000);
    browseService("ipp", "tcp");
    delay(1000);
    browseService("printer", "tcp");
    delay(10000);
}

void browseService(const char * service, const char * proto){
    Serial.printf("Browsing for service _%s._%s.local. ... ", service, proto);
    int n = MDNS.queryService(service, proto);
    if (n == 0) {
        Serial.println("no services found");
    } else {
        Serial.print(n);
        Serial.println(" service(s) found");
        for (int i = 0; i < n; ++i) {
            // Print details for each service found
            Serial.print("  ");
            Serial.print(i + 1);
            Serial.print(": ");
            Serial.print(MDNS.hostname(i));
            Serial.print(" (");
            Serial.print(MDNS.IP(i));
            Serial.print(":");
            Serial.print(MDNS.port(i));
            Serial.println(")");
        }
    }
    Serial.println();
}

从串口可以打印出扫描到的一些复位,以下显示的是扫描到了8个打印服务。

image-20210407113212051

在扫描到的服务名字后增加 .local 后缀,例如 http://deve4e4xx.local/ 在浏览器进行访问,即可访问到打印机的服务。

image-20210407113444873

9.6 NetBIOS

NetBIOS 为 网上基本输入输出系统(Network Basic Input/Output System)的缩写,它提供了OSI模型中的会话层服务,让在不同计算机上运行的不同程序,可以在局域网中,互相连线,以及分享数据。该协议的主要用途之一就是把计算机名称解析为相应IP地址。如果每个设备有一个固定名字,在实现了NetBIOS 的前提下,用户在浏览器里输入该设备的名字,然后通过 NetBIOS 解析,便可实现访问该设备网页的这个功能了。

在 Windows 操作系统中,默认情况下在安装 TCP/IP 协议后会自动安装 NetBIOS 协议,其他系统需要安装协议。

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->NetBIOS\examples\ESP_NBNST)

#include <WiFi.h>
#include <NetBIOS.h>
#include <WebServer.h> 

const char* ssid = "your_ssid";
const char* password = "your_password";

WebServer server(80); //声明WebServer对象

void handleRoot() //回调函数
{
  server.send(200, "text/plain", "This is the root directory");
}

void handleP1() //回调函数
{
  server.send(200, "text/plain", "This is the Page 1");
}

void setup() {
  Serial.begin(115200);

  // Connect to WiFi network
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.println("");

  // Wait for connection
  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());

  NBNS.begin("ESP");

  server.on("/", handleRoot); //注册链接"/"与对应回调函数

  server.on("/p1", handleP1); //注册链接"/p1"与对应回调函数

  server.on("/p2", []() { //注册链接"/p2",对应回调函数通过内联函数声明
    server.send(200, "text/plain", "This is the Page 2");
  });

  server.begin(); //启动服务器
  Serial.println("Web server started");

}

void loop() {
  server.handleClient(); //处理来自客户端的请求
  delay(2);// 允许cpu切换到其他任务 allow the cpu to switch to other tasks
}

通过DOS命令行发送 ping esp 命令,可获取 FireBeetle MESH 主板的IP地址

C:\Users\DFRobot-DFTV>ping esp

正在 Ping esp [192.168.0.242] 具有 32 字节的数据:
来自 192.168.0.242 的回复: 字节=32 时间=86ms TTL=255
来自 192.168.0.242 的回复: 字节=32 时间=87ms TTL=255
来自 192.168.0.242 的回复: 字节=32 时间=102ms TTL=255
来自 192.168.0.242 的回复: 字节=32 时间=114ms TTL=255

访问http://esp/ 可登录web server

image-20210511184556660

9.7 NTP TIME

网络授时协议,主板读取授时服务器的时间信息,转换为当地时间后,通过串口打印出来。程序在初始状态获取一次网络时间,然后关闭 WiFi。实时时间将在主板中继续计时。

除了 pool.ntp.org 这个NTP服务器还可以使用其他的服务器,例如 ntp1.aliyun.com、1.cn.pool.ntp.org。

API参考

configTime() - 设置NTP服务器和本地时区

设置NTP服务器和本地时区(偏移量规范)。最后,设置环境变量TZ并调用tzset()。

语法

void configTime(long gmtOffset_sec, int daylightOffset_sec, const char* server1, const char* server2, const char* server3);

参数

传入值 说明 值范围
gmtOffset_sec GMT与本地时间之间的时差(以秒为单位)。
daylightOffset_sec 夏令时提前的时间(单位为秒)。
server1, server2, server3 NTP服务器。设置至少一个。

返回

configTzTime() - 设置NTP服务器和本地时区

设置NTP服务器和本地时区(时区规范)。最后,设置环境变量TZ并调用tzset()。

语法

void configTzTime(const char* tz, const char* server1, const char* server2, const char* server3);

参数

传入值 说明 值范围
tz 时区
server1, server2, server3 NTP服务器。设置至少一个。

返回

getLocalTime() - 获取当地时间

获取当地时间。

语法

bool getLocalTime(struct tm * info, uint32_t ms);

参数

传入值 说明 值范围
info 用于存储要获取的时间信息的区域。呼叫者需要分配空间。
ms 超时时间。如果省略,则设置为5000。

返回

返回值 说明 值范围
bool 取得结果。如果成功,则为true。如果不成功则为false。当获取的时间信息的“年份”大于116(代表2016)时,返回True。

注意: 当获取的时间信息的“年”不大于116时,请等待最大以毫秒为单位的超时时间。由于它会尝试每10毫秒获取一次时间信息,因此请注意,如果未使用configTime()等预先设置时间,请谨慎操作。

例程:SimpleTime

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

#include <WiFi.h>
#include "time.h"

const char* ssid       = "YOUR_SSID";
const char* password   = "YOUR_PASS";

const char* ntpServer = "pool.ntp.org";
//GMT为格林尼治标准时间,北京时间使用8*60*60
const long  gmtOffset_sec = 8*3600;
//与具有夏令时的国家/地区有关。在其他国家/地区可以将其设置为0。中国当前没有实行夏令时。
const int   daylightOffset_sec = 0;
void printLocalTime()
{
  struct tm timeinfo;
  if(!getLocalTime(&timeinfo)){
    Serial.println("Failed to obtain time");
    return;
  }
  Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S");
}

void setup()
{
  Serial.begin(115200);
  
  //connect to WiFi
  Serial.printf("Connecting to %s ", ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
      delay(500);
      Serial.print(".");
  }
  Serial.println(" CONNECTED");
  
  //init and get the time
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
  printLocalTime();

  //disconnect WiFi as it's no longer needed
  WiFi.disconnect(true);
  WiFi.mode(WIFI_OFF);
}

void loop()
{
  delay(1000);
  printLocalTime();
}

串口每隔1秒钟打印出实时时间

Connecting to dfrobotOffice ....... CONNECTED
Tuesday, December 21 2021 15:15:28
Tuesday, December 21 2021 15:15:29

9.8 HTTP协议

9.8.1 HTTP 简介

HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(WWW:World Wide Web )服务器传输超文本到本地浏览器的传送协议。

HTTP基于TCP/IP通信协议来传递HTML 文件、图片文件、 查询结果等数据。

HTTP 工作原理

HTTP协议工作于客户端-服务端架构上。浏览器作为HTTP客户端通过URL向HTTP服务端即WEB服务器发送所有请求。

Web服务器根据接收到的请求后,向客户端发送响应信息。

HTTP默认端口号为80,但是你也可以改为8080或者其他端口。

HTTP三点注意事项:

  • HTTP是无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。

  • HTTP是媒体独立的:这意味着,只要客户端和服务器知道如何处理的数据内容,任何类型的数据都可以通过HTTP发送。客户端以及服务器指定使用适合的MIME-type内容类型。

  • HTTP是无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。

以下图表展示了HTTP协议通信流程:

image-20210422183415270

HTTP 消息结构

HTTP是基于客户端/服务端(C/S)的架构模型,通过一个可靠的链接来交换信息,是一个无状态的请求/响应协议。

一个HTTP”客户端”是一个应用程序(Web浏览器或其他任何客户端),通过连接到服务器达到向服务器发送一个或多个HTTP的请求的目的。

一个HTTP”服务器”同样也是一个应用程序(通常是一个Web服务,如Apache Web服务器或IIS服务器等),通过接收客户端的请求并向客户端发送HTTP响应数据。

HTTP使用统一资源标识符(Uniform Resource Identifiers, URI)来传输数据和建立连接。

一旦建立连接后,数据消息就通过类似Internet邮件所使用的格式[RFC5322]和多用途Internet邮件扩展(MIME)[RFC2045]来传送。

客户端请求消息

客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行(request line)、请求头部(header)、空行和请求数据四个部分组成,下图给出了请求报文的一般格式。

客户端请求报文

服务器响应消息

HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。

服务器响应


实例

下面实例是一点典型的使用GET来传递数据的实例:

客户端请求:

GET /hello.txt HTTP/1.1
User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
Host: www.example.com
Accept-Language: en, mi

服务端响应:

HTTP/1.1 200 OK
Date: Mon, 27 Jul 2009 12:28:53 GMT
Server: Apache
Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT
ETag: "34aa387-d-1568eb00"
Accept-Ranges: bytes
Content-Length: 51
Vary: Accept-Encoding
Content-Type: text/plain

输出结果:

Hello World! My payload includes a trailing CRLF.

HTTP请求方法

根据HTTP标准,HTTP请求可以使用多种请求方法。

HTTP1.0定义了三种请求方法: GET, POST 和 HEAD方法。

HTTP1.1新增了五种请求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法。

序号 方法 描述
1 GET 请求指定的页面信息,并返回实体主体。
2 HEAD 类似于get请求,只不过返回的响应中没有具体的内容,用于获取报头
3 POST 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改。
4 PUT 从客户端向服务器传送的数据取代指定的文档的内容。
5 DELETE 请求服务器删除指定的页面。
6 CONNECT HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。
7 OPTIONS 允许客户端查看服务器的性能。
8 TRACE 回显服务器收到的请求,主要用于测试或诊断。

HTTP 响应头信息

HTTP请求头提供了关于请求,响应或者其他的发送实体的信息。在本章节中我们将具体来介绍HTTP响应头信息。

应答头 说明
Allow 服务器支持哪些请求方法(如GET、POST等)。
Content-Encoding 文档的编码(Encode)方法。只有在解码之后才可以得到Content-Type头指定的内容类型。利用gzip压 缩文档能够显著地减少HTML文档的下载时间。Java的GZIPOutputStream可以很方便地进行gzip压缩,但只有Unix上的 Netscape和Windows上的IE 4、IE 5才支持它。因此,Servlet应该通过查看Accept-Encoding头(即request.getHeader("Accept- Encoding"))检查浏览器是否支持gzip,为支持gzip的浏览器返回经gzip压缩的HTML页面,为其他浏览器返回普通页面。
Content-Length 表示内容长度。只有当浏览器使用持久HTTP连接时才需要这个数据。如果你想要利用持久连接的优势,可以把输出文档写入 ByteArrayOutputStram,完成后查看其大小,然后把该值放入Content-Length头,最后通过 byteArrayStream.writeTo(response.getOutputStream()发送内容。
Content-Type 表示后面的文档属于什么MIME类型。Servlet默认为text/plain,但通常需要显式地指定为text/html。由于经常要设置Content-Type,因此HttpServletResponse提供了一个专用的方法setContentType。
Date 当前的GMT时间。你可以用setDateHeader来设置这个头以避免转换时间格式的麻烦。
Expires 应该在什么时候认为文档已经过期,从而不再缓存它?
Last-Modified 文档的最后改动时间。客户可以通过If-Modified-Since请求头提供一个日期,该请求将被视为一个条件 GET,只有改动时间迟于指定时间的文档才会返回,否则返回一个304(Not Modified)状态。Last-Modified也可用setDateHeader方法来设置。
Location 表示客户应当到哪里去提取文档。Location通常不是直接设置的,而是通过HttpServletResponse的sendRedirect方法,该方法同时设置状态代码为302。
Refresh 表示浏览器应该在多少时间之后刷新文档,以秒计。除了刷新当前文档之外,你还可以通过setHeader("Refresh", "5; URL=http://host/path")让浏览器读取指定的页面。 注 意这种功能通常是通过设置HTML页面HEAD区的<META HTTP-EQUIV="Refresh" CONTENT="5;URL=http://host/path">实现,这是因为,自动刷新或重定向对于那些不能使用CGI或Servlet的 HTML编写者十分重要。但是,对于Servlet来说,直接设置Refresh头更加方便。 注意Refresh的意义是"N秒之后刷 新本页面或访问指定页面",而不是"每隔N秒刷新本页面或访问指定页面"。因此,连续刷新要求每次都发送一个Refresh头,而发送204状态代码则可 以阻止浏览器继续刷新,不管是使用Refresh头还是<META HTTP-EQUIV="Refresh" ...>。 注意Refresh头不属于HTTP 1.1正式规范的一部分,而是一个扩展,但Netscape和IE都支持它。
Server 服务器名字。Servlet一般不设置这个值,而是由Web服务器自己设置。
Set-Cookie 设置和页面关联的Cookie。Servlet不应使用response.setHeader("Set-Cookie", ...),而是应使用HttpServletResponse提供的专用方法addCookie。参见下文有关Cookie设置的讨论。
WWW-Authenticate 客户应该在Authorization头中提供什么类型的授权信息?在包含401(Unauthorized)状态行的 应答中这个头是必需的。例如,response.setHeader("WWW-Authenticate", "BASIC realm=\"executives\"")。 注意Servlet一般不进行这方面的处理,而是让Web服务器的专门机制来控制受密码保护页面的访问(例如.htaccess)。

HTTP状态码

当浏览者访问一个网页时,浏览者的浏览器会向网页所在服务器发出请求。当浏览器接收并显示网页前,此网页所在的服务器会返回一个包含HTTP状态码的信息头(server header)用以响应浏览器的请求。

HTTP状态码的英文为HTTP Status Code。

下面是常见的HTTP状态码:

  • 200 - 请求成功

  • 301 - 资源(网页等)被永久转移到其它URL

  • 404 - 请求的资源(网页等)不存在

  • 500 - 内部服务器错误

HTTP状态码分类

HTTP状态码由三个十进制数字组成,第一个十进制数字定义了状态码的类型,后两个数字没有分类的作用。HTTP状态码共分为5种类型:

分类 分类描述
1** 信息,服务器收到请求,需要请求者继续执行操作
2** 成功,操作被成功接收并处理
3** 重定向,需要进一步的操作以完成请求
4** 客户端错误,请求包含语法错误或无法完成请求
5** 服务器错误,服务器在处理请求的过程中发生了错误

HTTP状态码列表:

状态码 状态码英文名称 中文描述
100 Continue 继续。客户端应继续其请求
101 Switching Protocols 切换协议。服务器根据客户端的请求切换协议。只能切换到更高级的协议,例如,切换到HTTP的新版本协议
200 OK 请求成功。一般用于GET与POST请求
201 Created 已创建。成功请求并创建了新的资源
202 Accepted 已接受。已经接受请求,但未处理完成
203 Non-Authoritative Information 非授权信息。请求成功。但返回的meta信息不在原始的服务器,而是一个副本
204 No Content 无内容。服务器成功处理,但未返回内容。在未更新网页的情况下,可确保浏览器继续显示当前文档
205 Reset Content 重置内容。服务器处理成功,用户终端(例如:浏览器)应重置文档视图。可通过此返回码清除浏览器的表单域
206 Partial Content 部分内容。服务器成功处理了部分GET请求
300 Multiple Choices 多种选择。请求的资源可包括多个位置,相应可返回一个资源特征与地址的列表用于用户终端(例如:浏览器)选择
301 Moved Permanently 永久移动。请求的资源已被永久的移动到新URI,返回信息会包括新的URI,浏览器会自动定向到新URI。今后任何新的请求都应使用新的URI代替
302 Found 临时移动。与301类似。但资源只是临时被移动。客户端应继续使用原有URI
303 See Other 查看其它地址。与301类似。使用GET和POST请求查看
304 Not Modified 未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源
305 Use Proxy 使用代理。所请求的资源必须通过代理访问
306 Unused 已经被废弃的HTTP状态码
307 Temporary Redirect 临时重定向。与302类似。使用GET请求重定向
400 Bad Request 客户端请求的语法错误,服务器无法理解
401 Unauthorized 请求要求用户的身份认证
402 Payment Required 保留,将来使用
403 Forbidden 服务器理解请求客户端的请求,但是拒绝执行此请求
404 Not Found 服务器无法根据客户端的请求找到资源(网页)。通过此代码,网站设计人员可设置"您所请求的资源无法找到"的个性页面
405 Method Not Allowed 客户端请求中的方法被禁止
406 Not Acceptable 服务器无法根据客户端请求的内容特性完成请求
407 Proxy Authentication Required 请求要求代理的身份认证,与401类似,但请求者应当使用代理进行授权
408 Request Time-out 服务器等待客户端发送的请求时间过长,超时
409 Conflict 服务器完成客户端的PUT请求是可能返回此代码,服务器处理请求时发生了冲突
410 Gone 客户端请求的资源已经不存在。410不同于404,如果资源以前有现在被永久删除了可使用410代码,网站设计人员可通过301代码指定资源的新位置
411 Length Required 服务器无法处理客户端发送的不带Content-Length的请求信息
412 Precondition Failed 客户端请求信息的先决条件错误
413 Request Entity Too Large 由于请求的实体过大,服务器无法处理,因此拒绝请求。为防止客户端的连续请求,服务器可能会关闭连接。如果只是服务器暂时无法处理,则会包含一个Retry-After的响应信息
414 Request-URI Too Large 请求的URI过长(URI通常为网址),服务器无法处理
415 Unsupported Media Type 服务器无法处理请求附带的媒体格式
416 Requested range not satisfiable 客户端请求的范围无效
417 Expectation Failed 服务器无法满足Expect的请求头信息
500 Internal Server Error 服务器内部错误,无法完成请求
501 Not Implemented 服务器不支持请求的功能,无法完成请求
502 Bad Gateway 充当网关或代理的服务器,从远端服务器接收到了一个无效的请求
503 Service Unavailable 由于超载或系统维护,服务器暂时的无法处理客户端的请求。延时的长度可包含在服务器的Retry-After头信息中
504 Gateway Time-out 充当网关或代理的服务器,未及时从远端服务器获取请求
505 HTTP Version not supported 服务器不支持请求的HTTP协议的版本,无法完成处理

HTTP content-type

Content-Type,内容类型,一般是指网页中存在的Content-Type,用于定义网络文件的类型和网页的编码,决定浏览器将以什么形式、什么编码读取这个文件,这就是经常看到一些Asp网页点击的结果却是下载到的一个文件或一张图片的原因。

HTTP content-type 对照表

文件扩展名 Content-Type(Mime-Type) 文件扩展名 Content-Type(Mime-Type)
.*( 二进制流,不知道下载文件类型) application/octet-stream .tif image/tiff
.001 application/x-001 .301 application/x-301
.323 text/h323 .906 application/x-906
.907 drawing/907 .a11 application/x-a11
.acp audio/x-mei-aac .ai application/postscript
.aif audio/aiff .aifc audio/aiff
.aiff audio/aiff .anv application/x-anv
.asa text/asa .asf video/x-ms-asf
.asp text/asp .asx video/x-ms-asf
.au audio/basic .avi video/avi
.awf application/vnd.adobe.workflow .biz text/xml
.bmp application/x-bmp .bot application/x-bot
.c4t application/x-c4t .c90 application/x-c90
.cal application/x-cals .cat application/vnd.ms-pki.seccat
.cdf application/x-netcdf .cdr application/x-cdr
.cel application/x-cel .cer application/x-x509-ca-cert
.cg4 application/x-g4 .cgm application/x-cgm
.cit application/x-cit .class java/*
.cml text/xml .cmp application/x-cmp
.cmx application/x-cmx .cot application/x-cot
.crl application/pkix-crl .crt application/x-x509-ca-cert
.csi application/x-csi .css text/css
.cut application/x-cut .dbf application/x-dbf
.dbm application/x-dbm .dbx application/x-dbx
.dcd text/xml .dcx application/x-dcx
.der application/x-x509-ca-cert .dgn application/x-dgn
.dib application/x-dib .dll application/x-msdownload
.doc application/msword .dot application/msword
.drw application/x-drw .dtd text/xml
.dwf Model/vnd.dwf .dwf application/x-dwf
.dwg application/x-dwg .dxb application/x-dxb
.dxf application/x-dxf .edn application/vnd.adobe.edn
.emf application/x-emf .eml message/rfc822
.ent text/xml .epi application/x-epi
.eps application/x-ps .eps application/postscript
.etd application/x-ebx .exe application/x-msdownload
.fax image/fax .fdf application/vnd.fdf
.fif application/fractals .fo text/xml
.frm application/x-frm .g4 application/x-g4
.gbr application/x-gbr . application/x-
.gif image/gif .gl2 application/x-gl2
.gp4 application/x-gp4 .hgl application/x-hgl
.hmr application/x-hmr .hpg application/x-hpgl
.hpl application/x-hpl .hqx application/mac-binhex40
.hrf application/x-hrf .hta application/hta
.htc text/x-component .htm text/html
.html text/html .htt text/webviewhtml
.htx text/html .icb application/x-icb
.ico image/x-icon .ico application/x-ico
.iff application/x-iff .ig4 application/x-g4
.igs application/x-igs .iii application/x-iphone
.img application/x-img .ins application/x-internet-signup
.isp application/x-internet-signup .IVF video/x-ivf
.java java/* .jfif image/jpeg
.jpe image/jpeg .jpe application/x-jpe
.jpeg image/jpeg .jpg image/jpeg
.jpg application/x-jpg .js application/x-javascript
.jsp text/html .la1 audio/x-liquid-file
.lar application/x-laplayer-reg .latex application/x-latex
.lavs audio/x-liquid-secure .lbm application/x-lbm
.lmsff audio/x-la-lms .ls application/x-javascript
.ltr application/x-ltr .m1v video/x-mpeg
.m2v video/x-mpeg .m3u audio/mpegurl
.m4e video/mpeg4 .mac application/x-mac
.man application/x-troff-man .math text/xml
.mdb application/msaccess .mdb application/x-mdb
.mfp application/x-shockwave-flash .mht message/rfc822
.mhtml message/rfc822 .mi application/x-mi
.mid audio/mid .midi audio/mid
.mil application/x-mil .mml text/xml
.mnd audio/x-musicnet-download .mns audio/x-musicnet-stream
.mocha application/x-javascript .movie video/x-sgi-movie
.mp1 audio/mp1 .mp2 audio/mp2
.mp2v video/mpeg .mp3 audio/mp3
.mp4 video/mpeg4 .mpa video/x-mpg
.mpd application/vnd.ms-project .mpe video/x-mpeg
.mpeg video/mpg .mpg video/mpg
.mpga audio/rn-mpeg .mpp application/vnd.ms-project
.mps video/x-mpeg .mpt application/vnd.ms-project
.mpv video/mpg .mpv2 video/mpeg
.mpw application/vnd.ms-project .mpx application/vnd.ms-project
.mtx text/xml .mxp application/x-mmxp
.net image/pnetvue .nrf application/x-nrf
.nws message/rfc822 .odc text/x-ms-odc
.out application/x-out .p10 application/pkcs10
.p12 application/x-pkcs12 .p7b application/x-pkcs7-certificates
.p7c application/pkcs7-mime .p7m application/pkcs7-mime
.p7r application/x-pkcs7-certreqresp .p7s application/pkcs7-signature
.pc5 application/x-pc5 .pci application/x-pci
.pcl application/x-pcl .pcx application/x-pcx
.pdf application/pdf .pdf application/pdf
.pdx application/vnd.adobe.pdx .pfx application/x-pkcs12
.pgl application/x-pgl .pic application/x-pic
.pko application/vnd.ms-pki.pko .pl application/x-perl
.plg text/html .pls audio/scpls
.plt application/x-plt .png image/png
.png application/x-png .pot application/vnd.ms-powerpoint
.ppa application/vnd.ms-powerpoint .ppm application/x-ppm
.pps application/vnd.ms-powerpoint .ppt application/vnd.ms-powerpoint
.ppt application/x-ppt .pr application/x-pr
.prf application/pics-rules .prn application/x-prn
.prt application/x-prt .ps application/x-ps
.ps application/postscript .ptn application/x-ptn
.pwz application/vnd.ms-powerpoint .r3t text/vnd.rn-realtext3d
.ra audio/vnd.rn-realaudio .ram audio/x-pn-realaudio
.ras application/x-ras .rat application/rat-file
.rdf text/xml .rec application/vnd.rn-recording
.red application/x-red .rgb application/x-rgb
.rjs application/vnd.rn-realsystem-rjs .rjt application/vnd.rn-realsystem-rjt
.rlc application/x-rlc .rle application/x-rle
.rm application/vnd.rn-realmedia .rmf application/vnd.adobe.rmf
.rmi audio/mid .rmj application/vnd.rn-realsystem-rmj
.rmm audio/x-pn-realaudio .rmp application/vnd.rn-rn_music_package
.rms application/vnd.rn-realmedia-secure .rmvb application/vnd.rn-realmedia-vbr
.rmx application/vnd.rn-realsystem-rmx .rnx application/vnd.rn-realplayer
.rp image/vnd.rn-realpix .rpm audio/x-pn-realaudio-plugin
.rsml application/vnd.rn-rsml .rt text/vnd.rn-realtext
.rtf application/msword .rtf application/x-rtf
.rv video/vnd.rn-realvideo .sam application/x-sam
.sat application/x-sat .sdp application/sdp
.sdw application/x-sdw .sit application/x-stuffit
.slb application/x-slb .sld application/x-sld
.slk drawing/x-slk .smi application/smil
.smil application/smil .smk application/x-smk
.snd audio/basic .sol text/plain
.sor text/plain .spc application/x-pkcs7-certificates
.spl application/futuresplash .spp text/xml
.ssm application/streamingmedia .sst application/vnd.ms-pki.certstore
.stl application/vnd.ms-pki.stl .stm text/html
.sty application/x-sty .svg text/xml
.swf application/x-shockwave-flash .tdf application/x-tdf
.tg4 application/x-tg4 .tga application/x-tga
.tif image/tiff .tif application/x-tif
.tiff image/tiff .tld text/xml
.top drawing/x-top .torrent application/x-bittorrent
.tsd text/xml .txt text/plain
.uin application/x-icq .uls text/iuls
.vcf text/x-vcard .vda application/x-vda
.vdx application/vnd.visio .vml text/xml
.vpg application/x-vpeg005 .vsd application/vnd.visio
.vsd application/x-vsd .vss application/vnd.visio
.vst application/vnd.visio .vst application/x-vst
.vsw application/vnd.visio .vsx application/vnd.visio
.vtx application/vnd.visio .vxml text/xml
.wav audio/wav .wax audio/x-ms-wax
.wb1 application/x-wb1 .wb2 application/x-wb2
.wb3 application/x-wb3 .wbmp image/vnd.wap.wbmp
.wiz application/msword .wk3 application/x-wk3
.wk4 application/x-wk4 .wkq application/x-wkq
.wks application/x-wks .wm video/x-ms-wm
.wma audio/x-ms-wma .wmd application/x-ms-wmd
.wmf application/x-wmf .wml text/vnd.wap.wml
.wmv video/x-ms-wmv .wmx video/x-ms-wmx
.wmz application/x-ms-wmz .wp6 application/x-wp6
.wpd application/x-wpd .wpg application/x-wpg
.wpl application/vnd.ms-wpl .wq1 application/x-wq1
.wr1 application/x-wr1 .wri application/x-wri
.wrk application/x-wrk .ws application/x-ws
.ws2 application/x-ws .wsc text/scriptlet
.wsdl text/xml .wvx video/x-ms-wvx
.xdp application/vnd.adobe.xdp .xdr text/xml
.xfd application/vnd.adobe.xfd .xfdf application/vnd.adobe.xfdf
.xhtml text/html .xls application/vnd.ms-excel
.xls application/x-xls .xlw application/x-xlw
.xml text/xml .xpl audio/scpls
.xq text/xml .xql text/xml
.xquery text/xml .xsd text/xml
.xsl text/xml .xslt text/xml
.xwd application/x-xwd .x_b application/x-x_b
.sis application/vnd.symbian.install .sisx application/vnd.symbian.install
.x_t application/x-x_t .ipa application/vnd.iphone
.apk application/vnd.android.package-archive .xap application/x-silverlight-app

9.8.2 HTTPClient API 参考

begin() - 注册要访问的URL

注册要访问的URL。如果使用HTTPS,则还注册根证书。

使用此API注册URL后,实际上是通过调用GET() / PATCH() / POST() / PUT来发送和接收数据的。

语法

#include <HTTPClient.h>
bool HTTPClient::begin(WiFiClient &client, String url);
bool HTTPClient::begin(WiFiClient &client, String host, uint16_t port, String uri, bool https);

如果已经定义 HTTPCLIENT_1_1_COMPATIBLE 以下方式有效

bool HTTPClient::begin(String url);
bool HTTPClient::begin(String url, const char* CAcert);
bool HTTPClient::begin(String host, uint16_t port, String uri = "/");
bool HTTPClient::begin(String host, uint16_t port, String uri, const char* CAcert);

参数

传入值 说明 值范围
client WiFiClient对象
url 访问的URL
uri 访问的URI
host 访问的主机
port 访问的主机的端口
https 协议。如果为true,则为https;如果为false,则为http。
CAcert 访问的URL的根证书。以Base64格式指定。

返回

返回值 说明 值范围
bool 注册结果。如果成功,则为true。如果不成功则为false。 true、false

end() - 断开TCP连接并终止HTTP通信

断开TCP连接并终止HTTP通信。如果使用HTTPClient :: setReuse()重用连接,则不会断开TCP连接。

语法

#include <HTTPClient.h>
void HTTPClient::end(void);

参数

返回

GET() - 将GET请求发送到在HTTPClient :: begin()中注册的URL

语法

#include <HTTPClient.h>
int HTTPClient::GET();

参数

返回

返回值 说明 值范围
int HTTP返回码。出现错误时为负数

PATCH() - 将PATCH请求发送到在HTTPClient :: begin()中注册的URL

语法

#include <HTTPClient.h>
int HTTPClient::PATCH(uint8_t * payload, size_t size);
int HTTPClient::PATCH(String payload);

参数

传入值 说明 值范围
payload 要发送的数据
size 要发送的数据大小

如果使用 HTTPClient::PATCH(String payload),数据长度为payload.length()

返回

返回值 说明 值范围
int HTTP返回码。出现错误时为负数。

POST() - 向通过HTTPClient :: begin()注册的URL发送POST请求

语法

#include <HTTPClient.h>
int HTTPClient::POST(uint8_t * payload, size_t size);
int HTTPClient::POST(String payload);

参数

传入值 说明 值范围
payload 要发送的数据
size 要发送的数据大小

如果使用 HTTPClient::POST(String payload),数据长度为payload.length()

返回

返回值 说明 值范围
int HTTP返回码。出现错误时为负数。

PUT() - 将PUT请求发送到在HTTPClient :: begin()中注册的URL

语法

#include <HTTPClient.h>
int HTTPClient::PUT(uint8_t * payload, size_t size);
int HTTPClient::PUT(String payload);

参数

传入值 说明 值范围
payload 要发送的数据
size 要发送的数据大小

如果使用 HTTPClient::PUT(String payload),数据长度为payload.length()

返回

返回值 说明 值范围
int HTTP返回码。出现错误时为负数。

setAuthorization() - 设置用于BASIC身份验证的身份验证信息

语法

#include <HTTPClient.h>
void HTTPClient::setAuthorization(const char * user, const char * password);
void HTTPClient::setAuthorization(const char * auth);

参数

传入值 说明 值范围
user 用于BASIC身份验证的用户名
password 用于BASIC身份验证的密码
auth 用于BASIC身份验证的身份验证信息(由BASE64通过将用户名和密码连接为“:”来编码的字符串)

返回

setReuse() - 指示重用HTTP连接

语法

#include <HTTPClient.h>
void HTTPClient::setReuse(bool reuse);

参数

传入值 说明 值范围
reuse 如果重新使用HTTP连接,则为true;如果未重新使用,则为false。

返回

getSize() - 返回通过HTTP请求获得的响应消息的正文部分的长度

语法

#include <HTTPClient.h>
int HTTPClient::getSize(void);

参数

返回

返回值 说明 值范围
int 通过HTTP请求获得的响应消息的正文部分的长度。

getString() - 返回通过HTTP请求获得的响应消息的正文部分

语法

#include <HTTPClient.h>
int HTTPClient::getString(void);

参数

返回

返回值 说明 值范围
int HTTP请求获得的响应消息的正文部分。如果无法保护内存,则返回一个空字符串。

getStreamPtr() - 返回指向HTTP连接的数据流的指针

语法

#include <HTTPClient.h>
WiFiClient* HTTPClient::getStreamPtr(void);

参数

返回

返回值 说明 值范围
WiFiClient* 指向HTTP会话的数据流的指针。

connected() - 检查是否连接了HTTP连接

语法

#include <HTTPClient.h>
bool HTTPClient::connected();

参数

返回

返回值 说明 值范围
bool 如果连接了HTTP会话,则为true;如果未连接,则为false。

errorToString() - 将HTTP错误代码转换为字符串

语法

#include <HTTPClient.h>
String HTTPClient::errorToString(int error);

参数

传入值 说明 值范围
error HTTP错误代码

返回

返回值 说明 值范围
String 与HTTP错误代码相对应的字符串(String对象)

writeToStream() - 将消息正文写入流中

语法

#include <HTTPClient.h>
int HTTPClient::writeToStream(Stream * stream);

参数

传入值 说明 值范围
stream 用于写出消息正文的流。

返回

返回值 说明 值范围
int 导出的字符数。出现错误时为负值。

例程:BasicHttpClient

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->HTTPClient\examples\BasicHttpClient)

/**
 * BasicHTTPClient.ino
 *
 *  Created on: 24.05.2015
 *
 */

#include <Arduino.h>

#include <WiFi.h>
#include <WiFiMulti.h>

#include <HTTPClient.h>

#define USE_SERIAL Serial  // USE_SERIAL被定义为Serial,因此前半部分是字符串到控制台的输出。

WiFiMulti wifiMulti; //定义了WiFiMulti类型的变量wifiMulti。WiFiMulti类是用于管理多个访问点的类。

void setup() {

    USE_SERIAL.begin(115200);

    USE_SERIAL.println();
    USE_SERIAL.println();
    USE_SERIAL.println();

    for(uint8_t t = 4; t > 0; t--) {
        USE_SERIAL.printf("[SETUP] WAIT %d...\n", t);
        USE_SERIAL.flush();
        delay(1000);
    }

    wifiMulti.addAP("your_ssid", "your_password"); // WiFi接入点注册。您需要更改SSID和PASSWORD以适合您的环境。

}

void loop() {
    // wait for WiFi connection
    // 如果连接成功,将返回WL_CONNECTED
    if((wifiMulti.run() == WL_CONNECTED)) {

        HTTPClient http; // HTTPClient类是实现HTTP客户端的类

        USE_SERIAL.print("[HTTP] begin...\n");
        // configure traged server and url
        // 注册您要访问的URL
        http.begin("http://example.com/index.html"); //HTTP

        USE_SERIAL.print("[HTTP] GET...\n");
        // start connection and send HTTP header
        // 使用GET请求获取在http.begin()中注册的URL。如果出现错误(例如无法连接到服务器),将返回负值。
        // 如果HTTP连接成功,则将返回HTTP状态代码。
        int httpCode = http.GET();

        // httpCode will be negative on error
        if(httpCode > 0) {
            // HTTP header has been send and Server response header has been handled
            USE_SERIAL.printf("[HTTP] GET... code: %d\n", httpCode);

            // file found at server
            if(httpCode == HTTP_CODE_OK) {
                // http.getString()是Http.GET()返回以字符串形式获取的数据。
                // 随着HTTP响应主体部分的大小增加,它将占用更多内存。
                // 根据您的目的,StreamHttpClient有一个以小增量读取数据的示例。
                String payload = http.getString();
                USE_SERIAL.println(payload);
            }
        } else {
            // 如果http.GET()调用失败,则调用http.errorToString()将错误代码转换为与错误代码对应的字符串,然后显示它。
            USE_SERIAL.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
        }
		// 断开TCP连接。也可以使用http.setReuse()重用该连接。此时,如果服务器返回保持活动状态,则连接似乎保持不变。
        http.end();
    }

    delay(5000);
}

获取example.com的网页从串口打印,

image-20210422191110538

可将串口打印出来的html文本保存为 .html 文件,用浏览器打开即可看到网页界面。

image-20210422191302095

例程:BasicHttpsClient 带认证客户端

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->HTTPClient\examples\BasicHttpsClient)

/**
   BasicHTTPSClient.ino
    Created on: 14.10.2018
*/

#include <Arduino.h>

#include <WiFi.h>
#include <WiFiMulti.h>

#include <HTTPClient.h>

#include <WiFiClientSecure.h>

// This is GandiStandardSSLCA2.pem, the root Certificate Authority that signed 
// the server certifcate for the demo server https://jigsaw.w3.org in this
// example. This certificate is valid until Sep 11 23:59:59 2024 GMT
const char* rootCACertificate = \
"-----BEGIN CERTIFICATE-----\n" \
"MIIF6TCCA9GgAwIBAgIQBeTcO5Q4qzuFl8umoZhQ4zANBgkqhkiG9w0BAQwFADCB\n" \
"iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl\n" \
"cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV\n" \
"BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTQw\n" \
"OTEyMDAwMDAwWhcNMjQwOTExMjM1OTU5WjBfMQswCQYDVQQGEwJGUjEOMAwGA1UE\n" \
"CBMFUGFyaXMxDjAMBgNVBAcTBVBhcmlzMQ4wDAYDVQQKEwVHYW5kaTEgMB4GA1UE\n" \
"AxMXR2FuZGkgU3RhbmRhcmQgU1NMIENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IB\n" \
"DwAwggEKAoIBAQCUBC2meZV0/9UAPPWu2JSxKXzAjwsLibmCg5duNyj1ohrP0pIL\n" \
"m6jTh5RzhBCf3DXLwi2SrCG5yzv8QMHBgyHwv/j2nPqcghDA0I5O5Q1MsJFckLSk\n" \
"QFEW2uSEEi0FXKEfFxkkUap66uEHG4aNAXLy59SDIzme4OFMH2sio7QQZrDtgpbX\n" \
"bmq08j+1QvzdirWrui0dOnWbMdw+naxb00ENbLAb9Tr1eeohovj0M1JLJC0epJmx\n" \
"bUi8uBL+cnB89/sCdfSN3tbawKAyGlLfOGsuRTg/PwSWAP2h9KK71RfWJ3wbWFmV\n" \
"XooS/ZyrgT5SKEhRhWvzkbKGPym1bgNi7tYFAgMBAAGjggF1MIIBcTAfBgNVHSME\n" \
"GDAWgBRTeb9aqitKz1SA4dibwJ3ysgNmyzAdBgNVHQ4EFgQUs5Cn2MmvTs1hPJ98\n" \
"rV1/Qf1pMOowDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYD\n" \
"VR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMCIGA1UdIAQbMBkwDQYLKwYBBAGy\n" \
"MQECAhowCAYGZ4EMAQIBMFAGA1UdHwRJMEcwRaBDoEGGP2h0dHA6Ly9jcmwudXNl\n" \
"cnRydXN0LmNvbS9VU0VSVHJ1c3RSU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNy\n" \
"bDB2BggrBgEFBQcBAQRqMGgwPwYIKwYBBQUHMAKGM2h0dHA6Ly9jcnQudXNlcnRy\n" \
"dXN0LmNvbS9VU0VSVHJ1c3RSU0FBZGRUcnVzdENBLmNydDAlBggrBgEFBQcwAYYZ\n" \
"aHR0cDovL29jc3AudXNlcnRydXN0LmNvbTANBgkqhkiG9w0BAQwFAAOCAgEAWGf9\n" \
"crJq13xhlhl+2UNG0SZ9yFP6ZrBrLafTqlb3OojQO3LJUP33WbKqaPWMcwO7lWUX\n" \
"zi8c3ZgTopHJ7qFAbjyY1lzzsiI8Le4bpOHeICQW8owRc5E69vrOJAKHypPstLbI\n" \
"FhfFcvwnQPYT/pOmnVHvPCvYd1ebjGU6NSU2t7WKY28HJ5OxYI2A25bUeo8tqxyI\n" \
"yW5+1mUfr13KFj8oRtygNeX56eXVlogMT8a3d2dIhCe2H7Bo26y/d7CQuKLJHDJd\n" \
"ArolQ4FCR7vY4Y8MDEZf7kYzawMUgtN+zY+vkNaOJH1AQrRqahfGlZfh8jjNp+20\n" \
"J0CT33KpuMZmYzc4ZCIwojvxuch7yPspOqsactIGEk72gtQjbz7Dk+XYtsDe3CMW\n" \
"1hMwt6CaDixVBgBwAc/qOR2A24j3pSC4W/0xJmmPLQphgzpHphNULB7j7UTKvGof\n" \
"KA5R2d4On3XNDgOVyvnFqSot/kGkoUeuDcL5OWYzSlvhhChZbH2UF3bkRYKtcCD9\n" \
"0m9jqNf6oDP6N8v3smWe2lBvP+Sn845dWDKXcCMu5/3EFZucJ48y7RetWIExKREa\n" \
"m9T8bJUox04FB6b9HbwZ4ui3uRGKLXASUoWNjDNKD/yZkuBjcNqllEdjB+dYxzFf\n" \
"BT02Vf6Dsuimrdfp5gJ0iHRc2jTbkNJtUQoj1iM=\n" \
"-----END CERTIFICATE-----\n";

// Not sure if WiFiClientSecure checks the validity date of the certificate. 
// Setting clock just to be sure...
void setClock() {
  configTime(0, 0, "pool.ntp.org", "time.nist.gov");

  Serial.print(F("Waiting for NTP time sync: "));
  time_t nowSecs = time(nullptr);
  while (nowSecs < 8 * 3600 * 2) {
    delay(500);
    Serial.print(F("."));
    yield();
    nowSecs = time(nullptr);
  }

  Serial.println();
  struct tm timeinfo;
  gmtime_r(&nowSecs, &timeinfo);
  Serial.print(F("Current time: "));
  Serial.print(asctime(&timeinfo));
}


WiFiMulti WiFiMulti;

void setup() {

  Serial.begin(115200);
  // Serial.setDebugOutput(true);

  Serial.println();
  Serial.println();
  Serial.println();

  WiFi.mode(WIFI_STA);
  WiFiMulti.addAP("SSID", "PASSWORD");

  // wait for WiFi connection
  Serial.print("Waiting for WiFi to connect...");
  while ((WiFiMulti.run() != WL_CONNECTED)) {
    Serial.print(".");
  }
  Serial.println(" connected");

  setClock();  
}

void loop() {
  WiFiClientSecure *client = new WiFiClientSecure;
  if(client) {
    client -> setCACert(rootCACertificate);

    {
      // Add a scoping block for HTTPClient https to make sure it is destroyed before WiFiClientSecure *client is 
      HTTPClient https;
  
      Serial.print("[HTTPS] begin...\n");
      if (https.begin(*client, "https://jigsaw.w3.org/HTTP/connection.html")) {  // HTTPS
        Serial.print("[HTTPS] GET...\n");
        // start connection and send HTTP header
        int httpCode = https.GET();
  
        // httpCode will be negative on error
        if (httpCode > 0) {
          // HTTP header has been send and Server response header has been handled
          Serial.printf("[HTTPS] GET... code: %d\n", httpCode);
  
          // file found at server
          if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) {
            String payload = https.getString();
            Serial.println(payload);
          }
        } else {
          Serial.printf("[HTTPS] GET... failed, error: %s\n", https.errorToString(httpCode).c_str());
        }
  
        https.end();
      } else {
        Serial.printf("[HTTPS] Unable to connect\n");
      }

      // End extra scoping block
    }
  
    delete client;
  } else {
    Serial.println("Unable to create client");
  }

  Serial.println();
  Serial.println("Waiting 10s before the next round...");
  delay(10000);
}

例程通过增加WiFiClientSecure库来实现了https网站的访问。

image-20210429194242796

例程:HTTP GET 获取温度数据

基于 BasicHttpClient 代码我们可以修改为去读取openweathermap网站发布的天气信息。

首先需要到免费的 openWeatherMap 气象数据网站 注册一个账户,注册完毕后会将API key 发送到注册的邮箱。

在这里可以找到API的介绍

获取当前成都天气的API,使用网页浏览器访问下面的网址

http://api.openweathermap.org/data/2.5/weather?q=Chengdu&appid={your API key}

返回json格式的数据如下:

image-20210506163947096

{"coord":{"lon":104.0667,"lat":30.6667},"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03d"}],"base":"stations","main":{"temp":296.15,"feels_like":296.07,"temp_min":296.15,"temp_max":296.15,"pressure":1014,"humidity":60},"visibility":10000,"wind":{"speed":2,"deg":0},"clouds":{"all":40},"dt":1620289984,"sys":{"type":1,"id":9674,"country":"CN","sunrise":1620252944,"sunset":1620301496},"timezone":28800,"id":1815286,"name":"Chengdu","cod":200}

要解析json格式数据,我们需要在Arduino IDE 安装 ArduinoJson 库

Arduino IDE库管理器中安装该库。只需转到Sketch > Include Library > Manage Libraries ,搜索库的名字 ArduinoJson,点击安装

image-20210506170659912

ArduinoJson 库详细教程

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将代码下载到 Edge101WE 主板,注意修改WiFi和openWeatherMapApiKey等信息。

#include <Arduino.h>
#include <WiFi.h>
#include <WiFiMulti.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

#define USE_SERIAL Serial

WiFiMulti wifiMulti;

// Your openWeatherMapApiKey
String openWeatherMapApiKey = "Your_openWeatherMapApiKey";

// your country code and city
String city = "Chengdu";

void setup() {

  USE_SERIAL.begin(115200);

  USE_SERIAL.println();
  USE_SERIAL.println();
  USE_SERIAL.println();

  for (uint8_t t = 4; t > 0; t--) {
    USE_SERIAL.printf("[SETUP] WAIT %d...\n", t);
    USE_SERIAL.flush();
    delay(1000);
  }

  wifiMulti.addAP("your_wifi_ssid", "your_wifi_password");

}

void loop() {
  // Allocate the JSON document
  //
  // Inside the brackets, 1000 is the capacity of the memory pool in bytes.
  // Don't forget to change this value to match your JSON document.
  // Use arduinojson.org/v6/assistant to compute the capacity.
  StaticJsonDocument<1000> doc;
  
  // wait for WiFi connection
  if ((wifiMulti.run() == WL_CONNECTED)) {
    String serverPath = "http://api.openweathermap.org/data/2.5/weather?q=" + city + "&APPID=" + openWeatherMapApiKey;
    HTTPClient http;

    USE_SERIAL.print("[HTTP] begin...\n");
    // configure traged server and url
    http.begin(serverPath); //HTTP

    USE_SERIAL.print("[HTTP] GET...\n");
    // start connection and send HTTP header
    int httpCode = http.GET();

    // httpCode will be negative on error
    if (httpCode > 0) {
      // HTTP header has been send and Server response header has been handled
      USE_SERIAL.printf("[HTTP] GET... code: %d\n", httpCode);

      // file found at server
      if (httpCode == HTTP_CODE_OK) {
        String payload = http.getString();
        USE_SERIAL.println(payload);
        DeserializationError error = deserializeJson(doc, payload);
        // Test if parsing succeeds.
        if (error) {
          Serial.print(F("deserializeJson() failed: "));
          Serial.println(error.f_str());
          return;
        }
        double  temp = doc["main"]["temp"];
        double  Pressure = doc["main"]["pressure"];
        double  humidity = doc["main"]["humidity"];
        int windSpeed = doc["wind"]["speed"];
        Serial.print("Temperature: ");        
        Serial.println(temp);
        Serial.print("Pressure: ");        
        Serial.println(Pressure);
        Serial.print("Humidity: ");
        Serial.println(humidity);
        Serial.print("Wind Speed: ");
        Serial.println(windSpeed);
      }
    } else {
      USE_SERIAL.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
    }

    http.end();
  }

  delay(5000);
}

串口将会打印出指定城市的天气情况,这些数据也可以直接用于计算。

image-20210506182553621

例程: HTTP GET 更新值

HTTP GET除了可以获取服务器数据,还可以更新服务器的数据。

在此示例中, Edge101WE 主板发出 HTTP GET 请求以更新 ThingSpeak 中的读数。

ThingSpeak 具有免费的 API,可让您使用HTTP存储和检索数据。在本教程中,您将使用 ThingSpeak API 从任何地方发布和可视化图表中的数据。例如,我们将发布随机值,但在实际应用中,您将使用真实传感器读数。

要将 ThingSpeak 与 Edge101WE 主板一起使用,您需要一个API密钥。请执行以下步骤:

  1. 转到ThingSpeak.com并创建一个免费帐户。

  2. 然后,打开“频道”标签。

  3. 创建一个新频道。名字为test,选中Field1。

    image-20210506184402589

获取到Write API Key

image-20210506185241119

在右边可看到 Write a Channel Feed API

GET https://api.thingspeak.com/update?api_key={your_APIKey}&field1=0

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将代码下载到 Edge101WE 主板

#include <Arduino.h>
#include <WiFi.h>
#include <WiFiMulti.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

#define USE_SERIAL Serial

WiFiMulti wifiMulti;

// your thingspeak.com Write API Key
String serverName = "http://api.thingspeak.com/update?api_key={your_API_Key}";

void setup() {

  USE_SERIAL.begin(115200);

  USE_SERIAL.println();
  USE_SERIAL.println();
  USE_SERIAL.println();

  for (uint8_t t = 4; t > 0; t--) {
    USE_SERIAL.printf("[SETUP] WAIT %d...\n", t);
    USE_SERIAL.flush();
    delay(1000);
  }

  wifiMulti.addAP("your_wifi_ssid", "your_wifi_password");
  
  // if analog input pin 37 is unconnected, random analog
  // noise will cause the call to randomSeed() to generate
  // different seed numbers each time the sketch runs.
  // randomSeed() will then shuffle the random function.  
  randomSeed(analogRead(37));
}

void loop() {
  
  // wait for WiFi connection
  if ((wifiMulti.run() == WL_CONNECTED)) {
    HTTPClient http;
    String randomData = String(random(100));
    USE_SERIAL.println("random = " + randomData);
    
    String serverPath = serverName + "&field1=" + randomData;
    
    USE_SERIAL.print("[HTTP] begin...\n");
    // configure traged server and url
    http.begin(serverPath); //HTTP

    USE_SERIAL.print("[HTTP] GET...\n");
    // start connection and send HTTP header
    int httpCode = http.GET();

    // httpCode will be negative on error
    if (httpCode > 0) {
      // HTTP header has been send and Server response header has been handled
      USE_SERIAL.printf("[HTTP] GET... code: %d\n", httpCode);

      // file found at server
      if (httpCode == HTTP_CODE_OK) {
        String payload = http.getString();
        USE_SERIAL.println(payload);
      }
    } else {
      USE_SERIAL.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
    }

    http.end();
  }

  delay(1000);
}

将实时显示上传的随机数的数据

image-20210506192122556

例程: HTTP POST 提交数据

在此例程中我们将通过 IFTTT.com Web API发送电子邮件通知。

IFTTT代表“ If This Than That”,它是一项免费的基于Web的服务,用于创建称为 applet 的简单条件语句链。

这意味着您可以在发生某些事情时触发事件。在此示例中,applet 在 Edge101WE 主板用户按钮按下时向您的电子邮件发送按键按下次数。

访问IFTTT网站

ifttt.com 并输入电子邮件以创建一个帐户。

创建一个 applet

点击主界面右上角 Create 按钮进入 Create 界面,点击If This add按钮。

image-20210510103635362

搜索 Webhooks,然后选择 Webhooks 图标。

image-20210507113014506

3.选择“Receive a web request 接收网络请求”触发器,然后为事件命名。在这种情况下,我输入了“ button_event ”。然后,点击“创建触发器”按钮。

image-20210507113707750

image-20210507114017506

4.单击“Then That”字样继续。现在,定义触发您定义的事件时发生的情况。搜索“email”服务并选择它。您可以保留默认选项。

image-20210507114329391

image-20210507114528407

5.按“完成”按钮创建小程序。

image-20210507114944496

设置 Email 发送内容,点击 Create action ,点击 Finish。

image-20210510105153877

测试小程序

在继续进行该项目之前,首先测试您的Applet。请按照以下步骤进行测试:

1.搜索Webhooks服务或打开此链接:https: //ifttt.com/maker_webhooks

2.单击“Documentation”按钮。

image-20210507115903170

修改 event 字段为 刚才定义的 button_event,Value1 值修改为 12 。点击 Test It 。

image-20210510110040394

此时收到一封邮件,按钮按次数为 Value1 填写的 12 。

image-20210510110239710

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将代码下载到 Edge101WE 主板。

#include <Arduino.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

const char *ssid = "your_ssdi"; //你的网络名称
const char *password = "your_password"; //你的网络密码

#define USE_SERIAL Serial
#define userButton 38 //定义用户按钮为GPIO38
volatile bool pressed = false;  //按钮按下标志
volatile int pressCounter = 0; //按钮按下计数器


// your thingspeak.com event and Write API Key
// thingspeak 定义的事件和 写API Key
String serverName = "http://maker.ifttt.com/trigger/{your_event}/with/key/{your_key}";
//类似如下格式 
//String serverName = "http://maker.ifttt.com/trigger/button_event/with/key/dAxLm420YlSYWGsaEE-wIG";

// IRAM_ATTR定义callBack中断服务函数在IRAM(指令RAM)中运行,以便在SPI Flash写、OTA更新等禁用高速缓存的同时ISR仍可以运行。
void IRAM_ATTR callBack(void)
{
  pressed = true;
  pressCounter ++;
}

void setup() {

  pinMode(userButton, INPUT_PULLUP);
  attachInterrupt(userButton, callBack, FALLING); //使能外部输入中断,按钮按下下降沿触发

  USE_SERIAL.begin(115200);
  USE_SERIAL.println();
  USE_SERIAL.println();
  USE_SERIAL.println();

  WiFi.begin(ssid, password); //连接网络

  while (WiFi.status() != WL_CONNECTED) //等待网络连接成功
  {
    delay(500);
    USE_SERIAL.print(".");
  }
  USE_SERIAL.println("Connected");
  USE_SERIAL.print("IP Address:");
  USE_SERIAL.println(WiFi.localIP());
}

void loop() {

  // wait for WiFi connection
  if ((WiFi.status() == WL_CONNECTED) && (pressed == true)) {
    USE_SERIAL.print("Button pressed!\n");
    pressed = false;

    HTTPClient http;

    USE_SERIAL.print("[HTTP] begin...\n");

    http.begin(serverName);

    // Specify content-type header
    http.addHeader("Content-Type", "application/x-www-form-urlencoded");
    // Data to send with HTTP POST
    String httpRequestData = "value1=" + String(pressCounter);

    USE_SERIAL.print("[HTTP] POST...\n");
    // Send HTTP POST request
    int httpResponseCode = http.POST(httpRequestData);
    /*
      // If you need an HTTP request with a content type: application/json, use the following:
      http.addHeader("Content-Type", "application/json");
      // JSON data to send with HTTP POST
      String httpRequestData = "{\"value1\":\"" + String(random(40)) + "\",\"value2\":\"" + String(random(40)) + "\",\"value3\":\"" + String(random(40)) + "\"}";
      // Send HTTP POST request
      int httpResponseCode = http.POST(httpRequestData);
    */

    // httpResponseCode will be negative on error
    if (httpResponseCode > 0) {
      // HTTP header has been send and Server response header has been handled
      USE_SERIAL.printf("[HTTP] POST... code: %d\n", httpResponseCode);

      // file found at server
      if (httpResponseCode == HTTP_CODE_OK) {
        String payload = http.getString();
        USE_SERIAL.println(payload);
      }
    } else {
      USE_SERIAL.printf("[HTTP] POST... failed, error: %s\n", http.errorToString(httpResponseCode).c_str());
    }

    http.end();
  }
  delay(100);
}

当主板上的用户按钮按下,主板将发送 HTTP POST 请求给 IFTTT 服务器。服务器的 webhooks 收到请求后将触发 Email 发送用户按钮按下多少次的通知邮件。

image-20210510115628195

image-20210510113108187

9.9 Websocket

由于在 HTTP 协议中,服务器不能主动向设备推送信息。设备使用轮询的方式向服务器请求数据时会消耗大量的设备运行资源与网络资源,因此 WebSocket 协议诞生。 WebSocket 协议是建立在传输层协议 TCP 上进行全双工通信的协议,可以实现设备与物联网协议之间的平等传输,即客户端可以主动向服务器发送请求,服务器也可以向客户端推送信息。WebSocket 只是借用HTTP协议完成一部分握手功能。

sequenceDiagram
    WS客户端->>WS服务端: HTTP请求建立连接
    WS服务端-->WS客户端:切换成WS协议
    WS客户端->>WS服务端:客户端发送数据
    WS服务端-->>WS客户端:服务端发送数据
    WS服务端-->>WS客户端:服务端发送数据
    WS服务端->WS客户端:关闭连接    

连接过程 —— 握手过程

  • 浏览器、服务器建立 TCP 连接,三次握手。这是通信的基础,传输控制层,若失败后续都不执行。

  • TCP 连接成功后,浏览器通过 HTTP 协议向服务器传送 WebSocket 支持的版本号等信息。(开始前的 HTTP 握手)

  • 服务器收到客户端的握手请求后,同样采用 HTTP 协议回馈数据。

  • 当收到了连接成功的消息后,通过 TCP 通道进行传输通信。

首先需要引入 WebSocket 库,这里以 Links2004/arduinoWebSockets 的 WebSocket 库为例。

库作者GitHub:arduinoWebSockets

首先打开 Ardunio IDE,点击标题栏项目 –> 加载库 –> 管理库打开库管理器

搜索 WebSockets,找到 Markus Sattler 版本的 WebSockets 库,点击安装。

WebSockets 事件类型

关键字 描述
WStype_ERROR WebSocket 连接发生错误时触发
WStype_DISCONNECTED 客户端或服务端关闭连接时触发
WStype_CONNECTED 客户端连接到服务端时触发
WStype_TEXT 接收到文本数据时触发
WStype_BIN 接收到二进制数据时触发
WStype_PING Ping 和 Pong 是 websocket 里的心跳,用来保证客户端是在线的,服务端给客户端发送 PING,客户端发送 PONG来回应
WStype_PONG

例程:使用 WebSocket 控制 LED

例程中使用两块 Edge101WE 主板 A 与 B,实现使用主板 A 上的用户按钮控制主板 B 的用户 LED 灯,同时使用主板 B 上的用户按钮控制主板 A 的用户 LED 灯。两块主板连接同一 WiFi 网络,并使用 WebSocket 协议进行网络通信。

当主板 A 用户按钮按下,主板 B 上的用户 LED 灯亮,当主板 B 用户按钮按下,主板 A 上的用户 LED 灯亮。

主板A:服务端开发板代码

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板A

#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <WebSocketsServer.h> // 引入WebSocketsServer库

#define userButton 38 // 定义主板用户按钮为GPIO38
#define userLED 15  // 定义主板用户LED为GPIO15

// 在这里填入WiFi名称
const char *ssid = "your_ssid";
// 在这里填入WiFi密码
const char *password = "your_password";

// 在这里指定WebSocket服务端IP地址
IPAddress host(192, 168, 43, 8);
// 在这里填入WiFi网关地址
IPAddress gateway(192, 168, 43, 1);
// 在这里填入WiFi子网掩码,网关地址与子网掩码可用ifconfig(mac os)或ipconfig(windows)命令获取
IPAddress netmask(255, 255, 255, 0);

// 初始化WebSocketsServer对象,并设置端口号为81
WebSocketsServer server(81);

int pressState;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
  pinMode(userButton, INPUT_PULLUP); // 
  pinMode(userLED, OUTPUT);
  
  // 设置WiFi运行模式为无线终端模式
  WiFi.mode(WIFI_STA);
  // 为当前设备配置固定IP
  if (!WiFi.config(host, gateway, netmask)) {
    Serial.println("Can't config wifi.");
  }
  Serial.println("WiFi Connecting");
  // 连接WiFi 
  WiFi.begin(ssid, password);
  // 判断是否连接成功
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print('.');
    delay(500);
  }
  Serial.println("");
  Serial.println("The server is connected to WiFi");
  Serial.print("Current ip address is ");
  Serial.println(WiFi.localIP());

  // 启动WebSocket服务器
  server.begin();
  // 指定事件处理函数
  /* param:
		num: 客户端编号,可以判断接收的消息来自哪个连接的客户端
		type: 事件类型
		payload: 事件携带数据
		length: 事件携带数据长度
  */    
  server.onEvent([](uint8_t num, WStype_t type, uint8_t * payload, size_t length){

    if (type == WStype_CONNECTED) {
      // 若为客户端连接事件,显示提示信息
      Serial.println("New connection!");
    } else if (type == WStype_DISCONNECTED) {
      // 若为连接断开事件,显示提示信息
      Serial.println("Close the connection.");
    } else if (type == WStype_TEXT) {
      // 接收来自客户端的信息(客户端FLASH按键状态),并控制LED的工作
      String data = (char*)payload;
      if(data.substring(data.indexOf("=") + 1) == "on") {
        digitalWrite(userLED, HIGH);
        Serial.println("server on board LED ON");
      } else if (data.substring(data.indexOf("=") + 1) == "off") {
        digitalWrite(userLED, LOW);
        Serial.println("server on board LED OFF");
      }
    }
  });
}

// 遍历当前连接服务端的所有客户端地址,并与需要获取编号的客户端IP地址进行比较,若找到IP地址,返回IP地址索引,否则返回-1
int8_t get_num_from_ip(IPAddress ip_needed) {
    for(uint8_t i = 0; i < WEBSOCKETS_SERVER_CLIENT_MAX; i++) {
        IPAddress ip = server.remoteIP(i);
        if(ip == ip_needed) {
           // ip found
           return i;
        }
    }
    return -1;
}

void loop() {
  // put your main code here, to run repeatedly:
  server.loop();
  // 判断当前主板用户按键状态是否与之前状态一致,若不一致则按键状态发生改变
  if (digitalRead(userButton) != pressState) {
    // 若按键状态改变,记录当前按键状态,并向客户端发送按键状态
    pressState = digitalRead(userButton);
    String data = String("ledStatus=") + (digitalRead(userButton) == HIGH ? "off": "on");
    Serial.println("set client " + data);
     /* param:
		num: int8_t类型,客户端编号
		str: String类型,要发送的数据
	*/
    server.sendTXT(get_num_from_ip(IPAddress(192, 168, 43, 9)), data);
  }
}

主板B:客户端开发板代码

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板B

#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <WebSocketsClient.h> // 引入WebSocketsClient库

#define userButton 38 // 定义主板用户按钮为GPIO38
#define userLED 15  // 定义主板用户LED为GPIO15

// 在这里填入WiFi名称
const char *ssid = "your_ssid";
// 在这里填入WiFi密码
const char *password = "your_password";

// 在这里指定WebSocket客户端IP地址
IPAddress host(192, 168, 43, 9);
// 在这里填入WiFi网关地址
IPAddress gateway(192, 168, 43, 1);
// 在这里填入WiFi子网掩码,网关地址与子网掩码可用ifconfig(mac os)或ipconfig(windows)命令获取
IPAddress netmask(255, 255, 255, 0);

// WebSocket服务器IP地址
String remoteHost = "192.168.43.8";
// 初始化WebSocketsClient对象
WebSocketsClient wsClient;

int pressState;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
  pinMode(userButton, INPUT_PULLUP);
  pinMode(userLED, OUTPUT);
  
  // 设置WiFi运行模式为无线终端模式
  WiFi.mode(WIFI_STA);
  // 为当前设备配置固定IP
  if (!WiFi.config(host, gateway, netmask)) {
    Serial.println("Can't config WiFi.");
  }
  Serial.println("WiFi Connecting");
  // 连接WiFi
  WiFi.begin(ssid, password);
  // 判断是否连接成功
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print('.');
    delay(500);
  }
  Serial.println("");
  Serial.println("The client is connected to WiFi");
  Serial.print("Current ip address is ");
  Serial.println(WiFi.localIP());

  // 启动WebSocketsClient,设定WebSocket服务器地址为remoteHost,端口号为81
  wsClient.begin(remoteHost, 81);
    
  // 指定事件处理函数
  /* param:
		type: 事件类型
		payload: 事件携带数据
		length: 事件携带数据长度
  */
  wsClient.onEvent([](WStype_t type, uint8_t * payload, size_t length) {

    if (type == WStype_TEXT) {
      // 接收来自服务端的信息(服务端用户按键状态),并控制LED的工作
      String data = (char*)payload;
      if(data.substring(data.indexOf("=") + 1) == "on") {
        digitalWrite(userLED, HIGH);
        Serial.println("client on board LED ON");
      } else if (data.substring(data.indexOf("=") + 1) == "off") {
        digitalWrite(userLED, LOW);
        Serial.println("client on board LED OFF");
      }
    }
  });
}

void loop() {
  // put your main code here, to run repeatedly:
  wsClient.loop();
  // 判断当前主板用户按键状态是否与之前状态一致,若不一致则按键状态发生改变
  if (digitalRead(userButton) != pressState) {
    // 若按键状态改变,记录当前按键状态,并向服务端发送按键状态
    pressState = digitalRead(userButton);
    String data = String("ledStatus=") + (digitalRead(userButton) == HIGH ? "off": "on");
    Serial.println("set server "+ data);
    /* param:
		str: String类型,要发送的数据
	*/
    wsClient.sendTXT(data);
  }
}

服务端串口打印信息

WiFi Connecting
.....
The server is connected to WiFi
Current ip address is 192.168.43.8
New connection!
server on board LED OFF
server on board LED ON
set client ledStatus=on
set client ledStatus=off

客户端串口打印信息

WiFi Connecting
.....
The client is connected to WiFi
Current ip address is 192.168.43.9
set server ledStatus=off
set server ledStatus=on
client on board LED ON
client on board LED OFF

9.10 ESPNOW

概述

ESP-NOW 是一种由乐鑫公司定义的无连接 Wi-Fi 通信协议。在 ESP-NOW 中,应用程序数据被封装在各个供应商的动作帧中,然后在无连接的情况下,从一个 Wi-Fi 设备传输到另一个 Wi-Fi 设备。 CTR 与 CBC-MAC 协议 (CCMP) 可用来保护动作帧的安全。ESP-NOW 广泛应用于智能照明、远程控制、传感器等领域。

使用了 ESP-NOW 通信之后,如果某一个设备突然断电,只要一旦重启,就可自动连接到对应的节点中重新进行通信。

ESP-NOW 支持特性:

  • 单播包加密或单播包不加密通信

  • 加密配对设备和非加密配对设备混合使用

  • 可携带最长为 250 字节的有效 payload 数据

  • 支持设置发送回调函数以通知应用层帧发送失败或成功

ESP-NOW 存在限制:

  • 暂时不支持广播包

  • 加密配对设备有限制,Station 模式下最多支持10 个加密配对设备;SoftAP 或 SoftAP + Station 混合模式下最多支持 6 个加密配对设备。非加密配对设备支持若干,与加密设备总数和不超过 20 个

  • 有效 payload 限制为 250 字节

帧格式

ESP-NOW 使用各个供应商的动作帧传输数据,默认比特率为 1 Mbps。各个供应商的动作帧格式为:

-----------------------------------------------------------------------------------------
|   MAC 报头   |  分类代码  |  组织标识符  |  随机值  |  供应商特定内容  |   FCS   |
-----------------------------------------------------------------------------------------
   24 字节        1 字节        3 字节      4 字节      7~255 字节       4 字节
  • 分类代码:分类代码字段可用于指示各个供应商的类别(比如 127)。

  • 组织标识符:组织标识符包含一个唯一标识符 (比如 0x18fe34),为乐鑫指定的 MAC 地址的前三个字节。

  • 随机值:防止重放攻击。

  • 供应商特定内容:供应商特定内容包含供应商特定字段,如下所示:

----------------------------------------------------------------------------------------
|  元素 ID  |  长度  |  组织标识符  |  类型  |  版本  |     正文     |
----------------------------------------------------------------------------------------
   1 字节     1 字节     3 字节      1 字节   1 字节    0~250 字节
  • 元素 ID:元素 ID 字段可用于指示特定于供应商的元素。

  • 长度:长度是组织标识符、类型、版本和正文的总长度。

  • 组织标识符:组织标识符包含一个唯一标识符 (比如 0x18fe34),为乐鑫指定的 MAC 地址的前三个字节。

  • 类型:类型字段设置为 4,代表 ESP-NOW

  • 版本:版本字段设置为 ESP-NOW 的版本。

  • 正文:正文包含 ESP-NOW 数据。

由于 ESP-NOW 是无连接的,因此 MAC 报头与标准帧略有不同。FrameControl 字段的 FromDS 和 ToDS 位均为 0。第一个地址字段用于配置目标地址。第二个地址字段用于配置源地址。第三个地址字段用于配置广播地址 (0xff:0xff:0xff:0xff:0xff:0xff)。

安全

  • ESP-NOW 采用 CCMP 方法保护供应商特定动作帧的安全,具体可参考 IEEE Std. 802.11-2012。Wi-Fi 设备维护一个初始主密钥 (PMK) 和若干本地主密钥 (LMK),长度均为 16 个字节。PMK 可使用 AES-128 算法加密 LMK。请调用 esp_now_set_pmk() 设置 PMK。如果未设置 PMK,将使用默认 PMK。

  • LMK 可通过 CCMP 方法对供应商特定的动作帧进行加密,最多拥有 6 个不同的 LMK。如果未设置配对设备的 LMK,则动作帧不进行加密。

目前,不支持加密组播供应商特定的动作帧。

初始化和反初始化

调用 esp_now_init() 初始化 ESP-NOW,调用 esp_now_deinit() 反初始化 ESP-NOW。ESP-NOW 数据必须在 Wi-Fi 启动后传输,因此建议在初始化 ESP-NOW 之前启动 Wi-Fi,并在反初始化 ESP-NOW 之后停止 Wi-Fi。 当调用 esp_now_deinit() 时,配对设备的所有信息都将被删除。

添加配对设备

在将数据发送到其他设备之前,请先调用 esp_now_add_peer() 将其添加到配对设备列表中。配对设备的最大数量是 20。如果启用了加密,则必须设置 LMK。ESP-NOW 数据可以从 Station 或 Softap 接口发送。 确保在发送 ESP-NOW 数据之前已启用该接口。在发送广播数据之前必须添加具有广播 MAC 地址的设备。配对设备的信道范围是从 0 ~14。如果信道设置为 0,数据将在当前信道上发送。否则,必须使用本地设备所在的通道。

发送 ESP-NOW 数据

调用 esp_now_send() 发送 ESP-NOW 数据,调用 esp_now_register_send_cb 注册发送回调函数。如果 MAC 层成功接收到数据,则该函数将返回 ESP_NOW_SEND_SUCCESS 事件。否则,它将返回 ESP_NOW_SEND_FAIL。ESP-NOW 数据发送失败可能有几种原因,比如目标设备不存在、设备的信道不相同、动作帧在传输过程中丢失等。应用层并不一定可以总能接收到数据。如果需要,应用层可在接收 ESP-NOW 数据时发回一个应答 (ACK) 数据。如果接收 ACK 数据超时,则将重新传输 ESP-NOW 数据。可以为 ESP-NOW 数据设置序列号,从而删除重复的数据。

如果有大量 ESP-NOW 数据要发送,则调用 esp_now_send() 一次性发送不大于 250 字节的数据。 请注意,两个 ESP-NOW 数据包的发送间隔太短可能导致回调函数返回混乱。因此,建议在等到上一次回调函数返回 ACK 后再发送下一个 ESP-NOW 数据。发送回调函数从高优先级的 Wi-Fi 任务中运行。因此,不要在回调函数中执行冗长的操作。相反,将必要的数据发布到队列,并交给优先级较低的任务处理。

接收 ESP-NOW 数据

调用 esp_now_register_recv_cb 注册接收回调函数。当接收 ESP-NOW 数据时,需要调用接收回调函数。接收回调函数也在 Wi-Fi 任务任务中运行。因此,不要在回调函数中执行冗长的操作。 相反,将必要的数据发布到队列,并交给优先级较低的任务处理。

API参考

esp_now_init(void) - 初始化ESPNOW功能

初始化ESPNOW功能。

参数

返回

返回 说明 值范围
esp_err_t ESP_OK:成功
ESP_ERR_ESPNOW_INTERNAL:内部错误

esp_now_deinit(void) - 取消初始化ESPNOW函数

取消初始化ESPNOW函数。

Return ESP_OK : succeed

参数

返回 说明 值范围
esp_err_t ESP_OK:成功

esp_now_get_version(uint32_t *version) - 获取ESPNOW的版本

获取ESPNOW的版本。

参数

传入值 说明 值范围
version ESPNOW版本

返回

返回 说明 值范围
esp_err_t ESP_OK:成功
ESP_ERR_ESPNOW_ARG:无效的参数

esp_now_register_recv_cb(esp_now_recv_cb_t cb) - 接收ESPNOW数据的回调

参数

传入值 说明 值范围
cb 接收ESPNOW数据的回调函数

返回

返回 说明 值范围
esp_err_t ESP_OK:成功
ESP_ERR_ESPNOW_NOT_INIT:ESPNOW未初始化ESP_ERR_ESPNOW_INTERNAL:内部错误

esp_now_unregister_recv_cb(void) - 注销接收ESPNOW数据的回调函数

注销接收ESPNOW数据的回调函数。

参数

返回

返回 说明 值范围
esp_err_t ESP_OK:成功
ESP_ERR_ESPNOW_NOT_INIT:ESPNOW未初始化

esp_now_register_send_cb(esp_now_send_cb_t cb) - 注册发送ESPNOW数据的回调函数

注册发送ESPNOW数据的回调函数。

参数

传入值 说明 值范围
cb 发送ESPNOW数据的回调函数

返回

返回 说明 值范围
esp_err_t ESP_OK:成功
ESP_ERR_ESPNOW_NOT_INIT:ESPNOW未初始化 ESP_ERR_ESPNOW_INTERNAL:内部错误

esp_now_unregister_send_cb(void) - 注销发送ESPNOW数据的回调函数

注销发送ESPNOW数据的回调函数。

参数

返回

返回 说明 值范围
esp_err_t ESP_OK:成功
ESP_ERR_ESPNOW_NOT_INIT:ESPNOW未初始化

esp_now_send(const uint8_t *peer_addr, const uint8_t *data, size_t len) - 发送ESPNOW数据

发送ESPNOW数据。

注意:

  • 如果peer_addr不为NULL,则将数据发送到MAC地址与peer_addr匹配的匹配方

  • 如果peer_addr为NULL,则将数据发送到添加到匹配列表中的所有匹配方

  • 数据的最大长度必须小于ESP_NOW_MAX_DATA_LEN

  • esp_now_send返回后,data参数指向的缓冲区不再有效

参数

传入值 说明 值范围
peer_addr 匹配方 MAC地址
data 要发送的数据
len 数据长度

返回

返回 说明 值范围
esp_err_t ESP_OK:成功
ESP_ERR_ESPNOW_NOT_INIT:ESPNOW未初始化 ESP_ERR_ESPNOW_ARG:无效的参数 ESP_ERR_ESPNOW_INTERNAL:内部错误 ESP_ERR_ESPNOW_NO_MEM:内存不足 ESP_ERR_ESPNOW_NOT_FOUND:找不到匹配方ESP_ERR_ESPNOW_IF:当前的WiFi接口与匹配方不匹配

esp_now_add_peer(constesp_now_peer_info_t *peer) - 添加一个 peer to peer 列表

添加一个 peer to peer 列表。

参数

传入值 说明 值范围
peer peer信息

返回

返回 说明 值范围
esp_err_t ESP_OK:成功
ESP_ERR_ESPNOW_NOT_INIT:ESPNOW未初始化 ESP_ERR_ESPNOW_ARG:无效的参数 ESP_ERR_ESPNOW_FULL:对等方列表已满 ESP_ERR_ESPNOW_NO_MEM:内存不足 ESP_ERR_ESPNOW_EXIST:peer 已经存在

esp_now_del_peer(const uint8_t *peer_addr) - 从peer列表中删除一个peer

从peer列表中删除一个peer。

参数

传入值 说明 值范围
peer_addr peer MAC地址

返回

返回 说明 值范围
esp_err_t ESP_OK:成功
ESP_ERR_ESPNOW_NOT_INIT:ESPNOW未初始化 ESP_ERR_ESPNOW_ARG:无效的参数 ESP_ERR_ESPNOW_NOT_FOUND:找不到peer

esp_now_mod_peer(constesp_now_peer_info_t *peer) - 修改 peer

修改 peer。

参数

传入值 说明 值范围
peer peer信息

返回

返回 说明 值范围
esp_err_t ESP_OK:成功
ESP_ERR_ESPNOW_NOT_INIT:ESPNOW未初始化 ESP_ERR_ESPNOW_ARG:无效的参数 ESP_ERR_ESPNOW_FULL:peer列表已满

esp_now_get_peer(const uint8_t *peer_addr, esp_now_peer_info_t *peer) - 从peer列表中获取其MAC地址与peer_addr匹配的peer

从peer列表中获取其MAC地址与peer_addr匹配的peer。

参数

传入值 说明 值范围
peer_addr peerMAC地址
peer peer信息

返回

返回 说明 值范围
esp_err_t ESP_OK:成功
ESP_ERR_ESPNOW_NOT_INIT:ESPNOW未初始化 ESP_ERR_ESPNOW_ARG:无效的参数
ESP_ERR_ESPNOW_NOT_FOUND:找不到peer

esp_now_fetch_peer(bool from_head, esp_now_peer_info_t *peer) - 从peer列表中获取peer

从peer列表中获取peer。仅返回地址为单播的peer,对于多播/广播地址,该函数将忽略并尝试在peer列表中查找下一个。

参数

传入值 说明 值范围
from_head 是否从列表的开头获取
peer peer信息

返回

返回 说明 值范围
esp_err_t ESP_OK:成功
ESP_ERR_ESPNOW_NOT_INIT:ESPNOW未初始化 ESP_ERR_ESPNOW_ARG:无效的参数
ESP_ERR_ESPNOW_NOT_FOUND:找不到peer

bool esp_now_is_peer_exist(const uint8_t *peer_addr) - 查询 Peer 是否存在

查询 Peer 是否存在。

参数

传入值 说明 值范围
peer_addr peer MAC地址

返回

返回 说明 值范围
bool true:peer 存在
false:peer 不存在

esp_now_get_peer_num(esp_now_peer_num_t *num) - 获取 peers 的数量

获取 peers 的数量。

参数

传入值 说明 值范围
num peer 数量

返回

返回 说明 值范围
esp_err_t ESP_OK:成功
ESP_ERR_ESPNOW_NOT_INIT:ESPNOW未初始化 ESP_ERR_ESPNOW_ARG:无效的参数

esp_now_set_pmk(const uint8_t *pmk) - 设置 primary master 密钥

设置 primary master 密钥。

注意:

  • primary master 密钥用于加密本地主密钥

参数

传入值 说明 值范围
pmk primary master 密钥

返回

返回 说明 值范围
esp_err_t ESP_OK:成功
ESP_ERR_ESPNOW_NOT_INIT:ESPNOW未初始化 ESP_ERR_ESPNOW_ARG:无效的参数

esp_now_set_wake_window(uint16_t window) - 为sta_disconnected电源管理设置esp_now唤醒窗口

为sta_disconnected电源管理设置esp_now唤醒窗口。

注意:

  • 仅当启用ESP_WIFI_STA_DISCONNECTED_PM_ENABLE时,此配置才能起作用

  • 此配置仅适用于station模式和断开连接状态

  • 如果有多个模块配置了其wake_window,则芯片将选择最大的模块以保持唤醒状态

  • 如果间隔和窗口之间的间隔小于5ms,则芯片将一直保持唤醒状态

  • 如果从未配置过ake_window,则一旦使用esp_now,芯片将在断开连接时保持唤醒状态

参数

传入值 说明 值范围
window 每个时间间隔内,芯片将持续唤醒多少微秒 0~65535

返回

返回 说明 值范围
esp_err_t ESP_OK:成功
ESP_ERR_ESPNOW_NOT_INIT:ESPNOW未初始化 ESP_ERR_ESPNOW_ARG:无效的参数

例程:点对点 Master 和 Slave 通信

主板 A 中作为 Master,主板 B 作为 Slave,主板 A 扫描到主板 B 后,建立通信。主板 A 发送持续变化的数据,主板 B 接收数据并打印到串口。

Master代码

/**
   ESPNOW - Basic communication - Master
   Date: 26th September 2017
   Author: Arvind Ravulavaru <https://github.com/arvindr21>
   Purpose: ESPNow Communication between a Master ESP32 and a Slave ESP32
   Description: This sketch consists of the code for the Master module.
   Resources: (A bit outdated)
   a. https://espressif.com/sites/default/files/documentation/esp-now_user_guide_en.pdf
   b. http://www.esploradores.com/practica-6-conexion-esp-now/

   << This Device Master >>

   Flow: Master
   Step 1 : ESPNow Init on Master and set it in STA mode
   Step 2 : Start scanning for Slave ESP32 (we have added a prefix of `slave` to the SSID of slave for an easy setup)
   Step 3 : Once found, add Slave as peer
   Step 4 : Register for send callback
   Step 5 : Start Transmitting data from Master to Slave

   Flow: Slave
   Step 1 : ESPNow Init on Slave
   Step 2 : Update the SSID of Slave with a prefix of `slave`
   Step 3 : Set Slave in AP mode
   Step 4 : Register for receive callback and wait for data
   Step 5 : Once data arrives, print it in the serial monitor

   Note: Master and Slave have been defined to easily understand the setup.
         Based on the ESPNOW API, there is no concept of Master and Slave.
         Any devices can act as master or salve.
*/

#include <esp_now.h>
#include <WiFi.h>

// Global copy of slave
esp_now_peer_info_t slave;
#define CHANNEL 1 // 当前只能使用0,1通道
#define PRINTSCANRESULTS 0
#define DELETEBEFOREPAIR 0

// Init ESP Now with fallback
void InitESPNow() {
  WiFi.disconnect();
   // 初始化 ESP-NOW 
  if (esp_now_init() == ESP_OK) {
    Serial.println("ESPNow Init Success");
  }
  else {
    Serial.println("ESPNow Init Failed");
    // Retry InitESPNow, add a counte and then restart?
    // InitESPNow();
    // or Simply Restart
    ESP.restart();
  }
}

// Scan for slaves in AP mode
void ScanForSlave() {
  int8_t scanResults = WiFi.scanNetworks();
  // reset on each scan
  bool slaveFound = 0;
  memset(&slave, 0, sizeof(slave));

  Serial.println("");
  if (scanResults == 0) {
    Serial.println("No WiFi devices in AP Mode found");
  } else {
    Serial.print("Found "); Serial.print(scanResults); Serial.println(" devices ");
    for (int i = 0; i < scanResults; ++i) {
      // Print SSID and RSSI for each device found
      String SSID = WiFi.SSID(i);
      int32_t RSSI = WiFi.RSSI(i);
      String BSSIDstr = WiFi.BSSIDstr(i);

      if (PRINTSCANRESULTS) {
        Serial.print(i + 1);
        Serial.print(": ");
        Serial.print(SSID);
        Serial.print(" (");
        Serial.print(RSSI);
        Serial.print(")");
        Serial.println("");
      }
      delay(10);
      // Check if the current device starts with `Slave`
      if (SSID.indexOf("Slave") == 0) {
        // SSID of interest
        Serial.println("Found a Slave.");
        Serial.print(i + 1); Serial.print(": "); Serial.print(SSID); Serial.print(" ["); Serial.print(BSSIDstr); Serial.print("]"); Serial.print(" ("); Serial.print(RSSI); Serial.print(")"); Serial.println("");
        // Get BSSID => Mac Address of the Slave
        int mac[6];
        if ( 6 == sscanf(BSSIDstr.c_str(), "%x:%x:%x:%x:%x:%x",  &mac[0], &mac[1], &mac[2], &mac[3], &mac[4], &mac[5] ) ) {
          for (int ii = 0; ii < 6; ++ii ) {
            slave.peer_addr[ii] = (uint8_t) mac[ii];
          }
        }

        slave.channel = CHANNEL; // pick a channel
        slave.encrypt = 0; // no encryption

        slaveFound = 1;
        // we are planning to have only one slave in this example;
        // Hence, break after we find one, to be a bit efficient
        break;
      }
    }
  }

  if (slaveFound) {
    Serial.println("Slave Found, processing..");
  } else {
    Serial.println("Slave Not Found, trying again.");
  }

  // clean up ram
  WiFi.scanDelete();
}

// Check if the slave is already paired with the master.
// If not, pair the slave with master
bool manageSlave() {
  if (slave.channel == CHANNEL) {
    if (DELETEBEFOREPAIR) {
      deletePeer();
    }

    Serial.print("Slave Status: ");
    // check if the peer exists
    bool exists = esp_now_is_peer_exist(slave.peer_addr);
    if ( exists) {
      // Slave already paired.
      Serial.println("Already Paired");
      return true;
    } else {
      // Slave not paired, attempt pair
      esp_err_t addStatus = esp_now_add_peer(&slave);
      if (addStatus == ESP_OK) {
        // Pair success
        Serial.println("Pair success");
        return true;
      } else if (addStatus == ESP_ERR_ESPNOW_NOT_INIT) {
        // How did we get so far!!
        Serial.println("ESPNOW Not Init");
        return false;
      } else if (addStatus == ESP_ERR_ESPNOW_ARG) {
        Serial.println("Invalid Argument");
        return false;
      } else if (addStatus == ESP_ERR_ESPNOW_FULL) {
        Serial.println("Peer list full");
        return false;
      } else if (addStatus == ESP_ERR_ESPNOW_NO_MEM) {
        Serial.println("Out of memory");
        return false;
      } else if (addStatus == ESP_ERR_ESPNOW_EXIST) {
        Serial.println("Peer Exists");
        return true;
      } else {
        Serial.println("Not sure what happened");
        return false;
      }
    }
  } else {
    // No slave found to process
    Serial.println("No Slave found to process");
    return false;
  }
}

void deletePeer() {
  esp_err_t delStatus = esp_now_del_peer(slave.peer_addr);
  Serial.print("Slave Delete Status: ");
  if (delStatus == ESP_OK) {
    // Delete success
    Serial.println("Success");
  } else if (delStatus == ESP_ERR_ESPNOW_NOT_INIT) {
    // How did we get so far!!
    Serial.println("ESPNOW Not Init");
  } else if (delStatus == ESP_ERR_ESPNOW_ARG) {
    Serial.println("Invalid Argument");
  } else if (delStatus == ESP_ERR_ESPNOW_NOT_FOUND) {
    Serial.println("Peer not found.");
  } else {
    Serial.println("Not sure what happened");
  }
}

uint8_t data = 0;
// send data
void sendData() {
  data++;
  const uint8_t *peer_addr = slave.peer_addr;
  Serial.print("Sending: "); Serial.println(data);
  esp_err_t result = esp_now_send(peer_addr, &data, sizeof(data));
  Serial.print("Send Status: ");
  if (result == ESP_OK) {
    Serial.println("Success");
  } else if (result == ESP_ERR_ESPNOW_NOT_INIT) {
    // How did we get so far!!
    Serial.println("ESPNOW not Init.");
  } else if (result == ESP_ERR_ESPNOW_ARG) {
    Serial.println("Invalid Argument");
  } else if (result == ESP_ERR_ESPNOW_INTERNAL) {
    Serial.println("Internal Error");
  } else if (result == ESP_ERR_ESPNOW_NO_MEM) {
    Serial.println("ESP_ERR_ESPNOW_NO_MEM");
  } else if (result == ESP_ERR_ESPNOW_NOT_FOUND) {
    Serial.println("Peer not found.");
  } else {
    Serial.println("Not sure what happened");
  }
}

// callback when data is sent from Master to Slave
void OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
  char macStr[18];
  snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
           mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
  Serial.print("Last Packet Sent to: "); Serial.println(macStr);
  Serial.print("Last Packet Send Status: "); Serial.println(status == ESP_NOW_SEND_SUCCESS ? "Delivery Success" : "Delivery Fail");
}

void setup() {
  Serial.begin(115200);
  //Set device in STA mode to begin with
  WiFi.mode(WIFI_STA);
  Serial.println("ESPNow/Basic/Master Example");
  // This is the mac address of the Master in Station Mode
  Serial.print("STA MAC: "); Serial.println(WiFi.macAddress());
  // Init ESPNow with a fallback logic
  InitESPNow();
  // Once ESPNow is successfully Init, we will register for Send CB to
  // get the status of Trasnmitted packet
  esp_now_register_send_cb(OnDataSent); // 注册发送回调函数
}

void loop() {
  // In the loop we scan for slave
  ScanForSlave();
  // If Slave is found, it would be populate in `slave` variable
  // We will check if `slave` is defined and then we proceed further
  if (slave.channel == CHANNEL) { // check if slave channel is defined
    // `slave` is defined
    // Add slave as peer if it has not been added already
    bool isPaired = manageSlave();
    if (isPaired) {
      // pair success or already paired
      // Send data to device
      sendData();
    } else {
      // slave pair failed
      Serial.println("Slave pair failed!");
    }
  }
  else {
    // No slave found to process
  }

  // wait for 3seconds to run the logic again
  delay(3000);
}

Slave代码

/**
   ESPNOW - Basic communication - Slave
   Date: 26th September 2017
   Author: Arvind Ravulavaru <https://github.com/arvindr21>
   Purpose: ESPNow Communication between a Master ESP32 and a Slave ESP32
   Description: This sketch consists of the code for the Slave module.
   Resources: (A bit outdated)
   a. https://espressif.com/sites/default/files/documentation/esp-now_user_guide_en.pdf
   b. http://www.esploradores.com/practica-6-conexion-esp-now/

   << This Device Slave >>

   Flow: Master
   Step 1 : ESPNow Init on Master and set it in STA mode
   Step 2 : Start scanning for Slave ESP32 (we have added a prefix of `slave` to the SSID of slave for an easy setup)
   Step 3 : Once found, add Slave as peer
   Step 4 : Register for send callback
   Step 5 : Start Transmitting data from Master to Slave

   Flow: Slave
   Step 1 : ESPNow Init on Slave
   Step 2 : Update the SSID of Slave with a prefix of `slave`
   Step 3 : Set Slave in AP mode
   Step 4 : Register for receive callback and wait for data
   Step 5 : Once data arrives, print it in the serial monitor

   Note: Master and Slave have been defined to easily understand the setup.
         Based on the ESPNOW API, there is no concept of Master and Slave.
         Any devices can act as master or salve.
*/

#include <esp_now.h>
#include <WiFi.h>

#define CHANNEL 1 // 当前只能使用0,1通道

// Init ESP Now with fallback
void InitESPNow() {
  WiFi.disconnect();
  if (esp_now_init() == ESP_OK) {
    Serial.println("ESPNow Init Success");
  }
  else {
    Serial.println("ESPNow Init Failed");
    // Retry InitESPNow, add a counte and then restart?
    // InitESPNow();
    // or Simply Restart
    ESP.restart();
  }
}

// config AP SSID
void configDeviceAP() {
  const char *SSID = "Slave_1";
  bool result = WiFi.softAP(SSID, "Slave_1_Password", CHANNEL, 0);
  if (!result) {
    Serial.println("AP Config failed.");
  } else {
    Serial.println("AP Config Success. Broadcasting with AP: " + String(SSID));
  }
}

void setup() {
  Serial.begin(115200);
  Serial.println("ESPNow/Basic/Slave Example");
  //Set device in AP mode to begin with
  WiFi.mode(WIFI_AP);
  // configure device AP mode
  configDeviceAP();
  // This is the mac address of the Slave in AP Mode
  Serial.print("AP MAC: "); Serial.println(WiFi.softAPmacAddress());
  // Init ESPNow with a fallback logic
  InitESPNow();
  // Once ESPNow is successfully Init, we will register for recv CB to
  // get recv packer info.
  esp_now_register_recv_cb(OnDataRecv);
}

// callback when data is recv from Master
void OnDataRecv(const uint8_t *mac_addr, const uint8_t *data, int data_len) {
  char macStr[18];
  snprintf(macStr, sizeof(macStr), "%02x:%02x:%02x:%02x:%02x:%02x",
           mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
  Serial.print("Last Packet Recv from: "); Serial.println(macStr);
  Serial.print("Last Packet Recv Data: "); Serial.println(*data);
  Serial.println("");
}

void loop() {
  // Chill
}

Master串口打信息

ESPNow/Basic/Master Example
STA MAC: C4:4F:33:3F:28:B1
ESPNow Init Success

Found 16 devices 
Found a Slave.
1: Slave_1 [B4:E6:2D:D5:FF:0A] (-34)
Slave Found, processing..
Slave Status: Pair success
Sending: 1
Send Status: Success
Last Packet Sent to: b4:e6:2d:d5:ff:0a
Last Packet Send Status: Delivery Success

Slave串口打印信息

ESPNow/Basic/Slave Example
AP Config Success. Broadcasting with AP: Slave_1
AP MAC: B4:E6:2D:D5:FF:0A
ESPNow Init Success
Last Packet Recv from: c4:4f:33:3f:28:b1
Last Packet Recv Data: 1

9.11 ESP-MESH 无线组网,多设备通信更方便

概述

ESP-MESH 是一套建立在 Wi-Fi 协议之上的网络协议。ESP-MESH 允许分布在大范围区域内(室内和室外)的大量设备(下文称节点)在同一个 WLAN(无线局域网)中相互连接。ESP-MESH 具有自组网和自修复的特性,也就是说 mesh 网络可以自主地构建和维护。

简介

传统网络架构示意图

​ 传统 Wi-Fi 网络架构

传统基础设施 Wi-Fi 网络是一个“单点对多点”的网络。这种网络架构的中心节点为接入点 (AP),其他节点 (station) 均与 AP 直接相连。其中,AP 负责各个 station 之间的仲裁和转发,一些 AP 还会通过路由器与外部 IP 网络交换数据。在传统 Wi-Fi 网络架构中,1)由于所有 station 均需与 AP 直接相连,不能距离 AP 太远,因此覆盖区域相对有限;2)受到 AP 容量的限制,因此网络中允许的 station 数量相对有限,很容易超载。

ESP-MESH 网络架构示意图

​ ESP-MESH 网络架构示意图

ESP-MESH 与传统 Wi-Fi 网络的不同之处在于:网络中的节点不需要连接到中心节点,而是可以与相邻节点连接。各节点均负责相连节点的数据中继。由于无需受限于距离中心节点的位置,所有节点仍可互连,因此 ESP-MESH 网络的覆盖区域更广。类似地,由于不再受限于中心节点的容量限制,ESP-MESH 允许更多节点接入,也不易于超载。

ESP-MESH 概念

术语

术语 描述
节点 任何 属于可以成为 ESP-MESH 网络一部分的设备
根节点 网络顶部的节点
子节点 如节点 X 连接至节点 Y,且 X 相较 Y 与根节点的距离更远(跨越的连接数量更多),则称 X 为 Y 的子节点。
父节点 与子节点对应的概念
后裔节点 任何可以从根节点追溯到的节点
兄弟节点 连接至同一个父节点的所有节点
连接 AP 和 station 之间的传统 Wi-Fi 关联。ESP-MESH 中的节点使用 station 接口与另一个节点的 SoftAP 接口产生关联,进而形成连接。连接包括 Wi-Fi 网络中的身份验证和关联过程。
上行连接 从节点到其父节点的连接
下行连接 从父节点到其一个子节点的连接
无线 hop 源节点和目标节点间无线连接路径中的一部分。单跳 指遍历单个连接的数据包,多跳 指遍历多个连接的数据包。
子网 子网指 ESP-MESH 网络的一部分,包括一个节点及其所有后代节点。因此,根节点的子网包括 ESP-MESH 网络中的所有节点。
MAC 地址 在 ESP-MESH 网络中用于区别每个节点或路由器的唯一地址
DS 分布式系统(外部 IP 网络)

树型拓扑

ESP-MESH 建立在传统 Wi-Fi 协议之上,可被视为一种将多个独立 Wi-Fi 网络组合为一个单一 WLAN 网络的组网协议。在 Wi-Fi 网络中,station 在任何时候都仅限于与 AP 建立单个连接(上行连接),而 AP 则可以同时连接到多个 station(下行连接)。然而,ESP-MESH 网络则允许节点同时充当 station 和 AP。因此,ESP-MESH 中的节点可以使用 其 SoftAP 接口建立多个下行连接,同时使用 其 station 接口建立一个上行连接。这将自然产生一个由多层父子结构组成的树型网络拓扑结构。

ESP-MESH 树型拓扑图

​ ESP-MESH 树型拓扑

ESP-MESH 是一个多跳网络,也就是说网络中的节点可以通过单跳或多跳向网络中的其他节点传送数据包。因此,ESP-MESH 中的节点不仅传输自己的数据包,而且同时充当其他节点的中继。假设 ESP-MESH 网络中的任意两个节点存在物理层上连接(通过单跳或多跳),则这两个节点可以进行通信。

注解

ESP-MESH 网络中的大小(节点总数)取决于网络中允许的最大层级,以及每个节点可以具有的最大下行连接数。因此,这两个变量可用于配置 ESP-MESH 网络的大小。

节点类型

ESP-MESH 节点类型图

​ ESP-MESH 节点类型

根节点: 指网络顶部的节点,是 ESP-MESH 网络和外部 IP 网络之间的唯一接口。根节点直接连接至传统的 Wi-Fi 路由器,并在 ESP-MESH 网络的节点和外部 IP 网络之间中继数据包。 ESP-MESH 网络中只能有一个根节点,且根节点的上行连接只能是路由器。如上图所示,节点 A 即为该 ESP-MESH 网络的根节点。

叶子节点: 指不允许拥有任何子节点(即无下行连接)的节点。因此,叶子节点只能传输或接收自己的数据包,但不能转发其他节点的数据包。如果节点处于 ESP-MESH 网络的最大允许层级,则该节点将成为叶子节点。叶子节点不会再产生下行连接,这可以防止节点继续生成下行连接,从而确保网络层级不会超出限制。由于建立下行连接必须使用 SoftAP 接口,因此一些没有 SoftAP 接口的节点(仅有 station 接口)也将被分配为叶子节点。如上图所示,位于网络最外层的 L/M/N 节点即为叶子节点。

中间父节点:既不是属于根节点也不属于叶子节点的节点即为中间父节点。中间父节点必须有且仅有一个上行连接(即一个父节点),但可以具有 0 个或多个下行连接(即 0 个或多个子节点)。因此,中间父节点可以发送和接收自己的数据包,也可以转发其上行和下行连接的数据包。如上图所示,节点 B 到 J 即为中间父节点。 注意,E/F/G/I/J 等没有下行连接的中间父节点并不等同于叶子节点,原因在于这些节点仍允许形成下行连接。

空闲节点:尚未加入网络的节点即为空闲节点。空闲节点将尝试与中间父节点形成上行连接,或者在有条件的情况下(参见 自动根节点选择 )成为一个根节点。如上图所示,K 和 O 节点即为空闲节点。

信标帧和 RSSI 阈值

ESP-MESH 中能够形成下行连接的每个节点(即具有 SoftAP 接口)都会定期传输 Wi-Fi 信标帧。节点可以通过信标帧让其他节点检测自己的存在和状态。空闲节点将侦听信标帧以生成一个潜在父节点列表,并与其中一个潜在父节点形成上行连接。ESP-MESH 使用“供应商信息元素”来存储元数据,例如:

  • 节点类型(根节点、中间父节点、叶子节点、空闲节点)

  • 节点当前所处的层级

  • 网络中允许的最大层级

  • 当前子节点数量

  • 可接受的最大下行连接数量

潜在上行连接的信号强度可由潜在父节点信标帧的 RSSI 表示。为了防止节点形成弱上行连接,ESP-MESH 采用了针对信标帧的 RSSI 阈值控制机制。如果节点检测到某节点的信标帧 RSSI 过低(即低于预设阈值),则会在尝试形成上行连接时忽略该节点。

RSSI 阈值效果图

​ RSSI 阈值的影响

上图(A 侧) 展示了 RSSI 阈值将如何影响空闲节点的候选父节点数量。

上图(B 侧) 展示了 RF 屏蔽物将如何降低潜在父节点的 RSSI。由于存在 RF 屏蔽物,节点 X 的 RSSI 高于阈值的区域显著减小。这会导致空闲节点忽略节点 X,即使从地理位置上看 X 就在空闲节点附近。相反,该空闲节点将从更远的地方找到一个 RSSI 更强的节点 Y 形成上行连接。

注解

事实上,ESP-MESH 网络中的节点在 MAC 层仍可以接收所有的信标帧,但 RSSI 阈值控制功能可以过滤掉所有 RSSI 低于预设阈值的信标帧。

首选父节点

当一个空闲节点有多个候选父节点(潜在父节点)时,空闲节点将与其中的 首选父节点 形成上行连接。首选父节点基于以下条件确定:

  • 候选父节点所处的层级

  • 候选父节点当前具有的下行连接(子节点)数量

在网络中所处层级较浅的候选父节点(包括根节点)将优先成为首选父节点。这有助于在形成上行连接时控制 ESP-MESH 网络中的总层级使之最小。例如,在位于第二层和第三层的候选父节点间选择时,位于第二层的候选父节点将始终优先成为首选父节点。

如果同一层上存在多个候选父节点,则子节点最少的候选父节点将优先成为首选父节点。这有助于平衡同一层节点的下行连接数量。

首选父节点选择示意图

​ 首选父节点选择

上图(A 侧) 展示了空闲节点 G 如何在 B/C/D/E/F 五个候选父节点中选择首选父节点:首先,B/C 节点优于 D/E/F 节点,因为这两个节点所处的层级更浅。其次,C 节点优于 B 节点,因为 C 节点的下行连接数量(子节点数量)更少。

上图(B 侧) 展示了空闲节点 G 如何在根节点 A 和其他候选父节点中选择首选父节点,此时根节点 A 处于空闲节点 G 范围之内(即空闲节点 G 接收到的根节点 A 信标帧 RSSI 强度高于预设阈值):由于根节点 A 处于网络中最浅的层,因此将成为首选父节点。

注解

用户还可以自行定义首选父节点的选择规则,也可以直接指定某个节点为首选父节点(见 Mesh 手动配网示例 )。

路由表

ESP-MESH 网络中的每个节点均会维护自己的路由表,并按路由表将数据包(请见 ESP-MESH 数据包)沿正确的路线发送至正确的目标节点。某个特定节点的路由表将包含 该节点的子网中所有节点的 MAC 地址,也包括该节点自己的 MAC 地址。每个路由表会划分为多个子路由表,与每个子节点的子网对应。

ESP-MESH 路由表示例图

​ ESP-MESH 路由表示例

以上图为例,节点 B 的路由表中将包含节点 B 到节点 I 的 MAC 地址(即相当于节点 B 的子网)。节点 B 的路由表可划分为节点 C 和 G 的子路由表,分别包含节点 C 到节点 F 的 MAC 地址、节点 G 到节点 I 的 MAC 地址。

ESP-MESH 利用路由表来使用以下规则进行转发,确定 ESP-MESH 数据包应根据向上行转发还是向下行转发。

1. 如果数据包的目标 MAC 地址处于当前节点的路由表中且不是当前节点本身,则选择包含目标 MAC 地址的子路由表,并将数据包向下转发给子路由表对应的子节点。

2. 如果数据包的目标 MAC 地址不在当前节点的路由表内,则将数据包向上转发给当前节点的父节点,并重复执行该操作直至数据包达到目标地址。此步骤可重复至根节点(根节点包含整个网络的全部节点)。

注解

用户可以通过调用 esp_mesh_get_routing_table() 获取一个节点的路由表,调用 esp_mesh_get_routing_table_size() 获取一个路由表的大小,也可通过调用 esp_mesh_get_subnet_nodes_list() 获取某个子节点的子路由表,调用 esp_mesh_get_subnet_nodes_num() 获取子路由表的大小。

建立网络

一般过程

警告

ESP-MESH 正式开始构建网络前,必须确保网络中所有节点具有相同的配置(见 mesh_cfg_t)。每个节点必须配置 相同 MESH 网络 ID、路由器配置和 SoftAP 配置

ESP-MESH 网络将首先选择根节点,然后逐层形成下行连接,直到所有节点均加入网络。网络的布局可能取决于诸如根节点选择、父节点选择和异步上电复位等因素。但简单来说,一个 ESP-MESH 网络的构建过程可以概括为以下步骤:

ESP-MESH 网络构建过程示意图

​ ESP-MESH 网络构建过程

1. 根节点选择

根节点直接进行指定(见 用户指定根节点)或通过选举由信号强度最强的节点担任(见 自动根节点选择)。一旦选定,根节点将与路由器连接,并开始允许下行连接形成。如上图所示,节点 A 被选为根节点,因此节点 A 上行连接到路由器。

2. 第二层形成

一旦根节点连接到路由器,根节点范围内的空闲节点将开始与根节点连接,从而形成第二层网络。一旦连接,第二层节点成为中间父节点(假设最大允许层级大于 2 层),并进而形成下一层。如上图所示,节点 B 到节点 D 都在根节点的连接范围内。因此,节点 B 到节点 D 将与根节点形成上行连接,并成为中间父节点。

3. 其余层形成

剩余的空闲节点将与所处范围内的中间父节点连接,并形成新的层。一旦连接,根据网络的最大允许层级,空闲节点成为中间父节点或叶子节点。此后重复该步骤,直到网络中的所有空闲节点均加入网络或达到网络最大允许层级。如上图所示,节点 E/F/G 分别与节点 B/C/D 连接,并成为中间父节点。

4. 限制树深度

为了防止网络超过最大允许层级,最大允许层级上的节点将在完成连接后成为叶子节点。这样一来,其他空闲节点将无法与这些最大允许层上的叶子节点形成连接,因此不会超过最大允许层级。然而,如果空闲节点无法找到其他潜在父节点,则将无限期地保持空闲状态。如上图所示,网络的最大允许层级为四。因此,节点 H 在完成连接后将成为叶子节点,以防止任何下行连接的形成。

自动根节点选择

在自动模式下,根节点的选择取决于相对于路由器的信号强度。每个空闲节点将通过 Wi-Fi 信标帧发送自己的 MAC 地址和路由器 RSSI 值。 MAC 地址可以表示网络中的唯一节点,而 路由器 RSSI 值 代表相对于路由器的信号强度。

此后,每个节点将同时扫描来自其他空闲节点的信标帧。如果节点检测到具有更强的路由器 RSSI 的信标帧,则节点将开始传输该信标帧的内容(相当于为这个节点投票)。经过最小迭代次数(可预先设置,默认为 10 次)将选举出路由器 RSSI 值最强的信标帧。

在达到预设迭代次数后,每个节点将单独检查其 得票百分比得票数/总票数)以确定它是否应该成为根节点。 如果节点的得票百分比大于预设的阈值(默认为 90%),则该节点将成为根节点

下图展示了在 ESP-MESH 网络中,根节点的自动选择过程。

根节点选举流程示例图

​ 根节点选举示例

1. 上电复位时,每个节点开始传输自己的信标帧(包括 MAC 地址和路由器 RSSI 值)。

2. 在多次传输和扫描迭代中,路由器 RSSI 最强的信标帧将在整个网络中传播。节点 C 具有最强的路由器 RSSI 值(-10 dB),因此它的信标帧将在整个网络中传播。所有参与选举的节点均给节点 C 投票,因此节点 C 的得票百分比为 100%。因此,节点 C 成为根节点,并与路由器连接。

3. 一旦节点 C 与路由器连接,节点 C 将成为节点 A/B/D/E 的首选父节点(即最浅的节点),并与这些节点连接。节点 A/B/D/E 将形成网络的第二层。

4. 节点 F 和节点 G 分别连接节点 D 和节点 E,并完成网络构建过程。

注解

用户可以通过 esp_mesh_set_attempts() 配置选举的最小迭代次数。用户应根据网络内的节点数量配置迭代次数(即 mesh 网络越大,所需的迭代次数越高)。

警告

得票百分比阈值 也可以使用 esp_mesh_set_vote_percentage() 进行配置。得票百分比阈值过低 可能导致同一 mesh 网络中两个或多个节点成为根节点,进而分化为多个 mesh 网络。如果发生这种情况,ESP-MESH 具有内部机制,可自主解决 根节点冲突。这些具有多个根节点的网络将围绕一个根节点形成一个网络。然而,两个或多个路由器 SSID 相同但路由器 BSSID 不同的根节点冲突尚无法解决。

用户指定根节点

根节点也可以由用户指定,即直接让指定的根节点与路由器连接,并放弃选举过程。当根节点指定后,网络内的所有其他节点也必须放弃选举过程,以防止根节点冲突的发生。下图展示了在 ESP-MESH 网络中,根节点的手动选择过程。

根节点指定过程示例图

​ 根节点指定示例(根节点 = A,最大层级 = 4)

1. 节点 A 是由用户指定的根节点,因此直接与路由器连接。此时,所有其他节点放弃选举过程。

2. 节点 C 和节点 D 将节点 A 选为自己的首选父节点,并与其形成连接。这两个节点将形成网络的第二层。

3. 类似地,节点 B 和节点 E 将与节点 C 连接,节点 F 将与节点 D 连接。这三个节点将形成网络的第三层。

4. 节点 G 将与节点 E 连接,形成网络的第四层。然而,由于该网络的最大允许层级已配置为 4,因此节点 G 将成为叶子节点,以防止形成任何新层。

注解

一旦指定根节点,该根节点应调用 esp_mesh_set_parent() 使其直接与路由器连接。类似地,所有其他节点都应该调用 esp_mesh_fix_root() 放弃选举过程。

选择父节点

默认情况下,ESP-MESH 具有可以自组网的特点,也就是每个节点都可以自主选择与其形成上行连接的潜在父节点。自主选择出的父节点被称为首选父节点。用于选择首选父节点的标准旨在减少 ESP-MESH 网络的层级,并平衡各个潜在父节点的下行连接数(参见 首选父节点)。

不过,ESP-MESH 也允许用户禁用自组网功能,即允许用户自己定义父节点选择标准,或直接指定某个节点为父节点(见: Mesh 手动组网示例 )。

异步上电复位

ESP-MESH 网络构建可能会受到节点上电顺序的影响。如果网络中的某些节点为异步上电(即相隔几分钟上电),网络的最终结构可能与所有节点同步上电时的理想情况不同。延迟上电的节点将遵循以下规则:

规则 1:如果网络中已存在根节点,则延迟节点不会尝试选举成为新的根节点,即使自身的路由器 RSSI 更强。相反,延迟节点与任何其他空闲节点无异,将通过与首选父节点连接来加入网络。如果该延迟节点为用户指定的根节点,则网络中的所有其他节点将保持空闲状态,直到延迟节点完成上电。

规则 2:如果延迟节点形成上行连接,并成为中间父节点,则后续也可能成为其他节点(即其他更浅的节点)的新首选父节点。此时,其他节点切换上行连接至该延迟节点(见 父节点切换)。

规则 3:如果空闲节点的指定父节点上电延迟了,则该空闲节点在没有找到指定父节点前不会尝试形成任何上行连接。空闲节点将无限期地保持空闲,直到其指定的父节点上电完成。

下方示例展示了异步上电对网络构建的影响。

异步电源示例图

​ 网络构建(异步电源)示例

1. 节点 A/C/D/F/G/H 同步上电,并通过广播其 MAC 地址和路由器 RSSI 开始选举根节点。节点 A 的 RSSI 最强,因此当选为根节点。

2. 一旦节点 A 成为根节点,其余的节点就开始与其首选父节点逐层形成上行连接,并最终形成一个具有五层的网络。

3. 节点 B/E 由于存在上电延迟,因此即使路由器 RSSI 比节点 A 更强(-20 dB 和 -10 dB)也不会尝试成为根节点。相反,这两个上电延迟节点均将与对应的首选父节点 A 和 C 形成上行连接。加入网络后,节点 B/E 均将成为中间父节点。

4. 节点 B 由于所处层级变化(现为第二层)而成为新的首选父节点,因此节点 D/G 将切换其上行连接从而选择新的首选父节点。由于切换的发生,最终的网络层级从原来的五层减少至三层。

同步上电:如果所有节点均同步上电,节点 E (-10 dB)由于路由器 RSSI 最强而成为根节点。此时形成的网络结构将与异步上电的情况截然不同。但是,如果用户手动切换根节点,则仍可以达到同步上电的网络结构 (请见 esp_mesh_waive_root())。

注解

从某种程度上,ESP-MESH 可以自动修复部分因异步上电引起的父节点选择的偏差(请见 父节点切换)

环路避免、检测和处理

环路是指特定节点与其后代节点(特定节点子网中的节点)形成上行连接的情况。因此产生的循环连接路径将打破 mesh 网络的树型拓扑结构。ESP-MESH 的节点在选择父节点时将主动排除路由表(见 路由表)中的节点,从而避免与其子网中的节点建立上行连接并形成环路。

在存在环路的情况下,ESP-MESH 可利用路径验证机制和能量传递机制来检测环路的产生。因与子节点建立上行连接而导致环路形成的父节点将通知子节点环路的存在,并主动断开连接。

管理网络

作为一个自修复网络,ESP-MESH 可以检测并修正网络路由中的故障。当具有一个或多个子节点的父节点断开或父节点与其子节点之间的连接不稳定时,会发生故障。ESP-MESH 中的子节点将自主选择一个新的父节点,并与其形成上行连接,以维持网络互连。ESP-MESH 可以处理根节点故障和中间父节点故障。

根节点故障

如果根节点断开,则与其连接的节点(第二层节点)将及时检测到该根节点故障。第二层节点将主动尝试与根节点重连。但是在多次尝试失败后,第二层节点将启动新一轮的根节点选举。 第二层中 RSSI 最强的节点将当选为新的根节点,而剩余的第二层节点将与新的根节点(如果不在范围内的话,也可与相邻父节点连接)形成上行连接。

如果根节点和下面多层的节点(例如根节点、第二层节点和第三层节点)同时断开,则位于最浅层的仍在正常工作的节点将发起根节点选举。下方示例展示了网络从根节点断开故障中进行自修复。

根节点故障的自修复示意图

​ 根节点故障的自修复示意

1. 节点 C 是网络的根节点。节点 A/B/D/E 是连接到节点 C 的第二层节点。

2. 节点 C 断开。在多次重连尝试失败后,第二层节点开始通过广播其路由器 RSSI 开始新一轮的选举。此时,节点 B 的路由器 RSSI 最强。

3. 节点 B 被选为根节点,并开始接受下行连接。剩余的第二层节点 A/D/E 形成与节点 B 的上行连接,因此网络已经恢复,并且可以继续正常运行。

注解

如果是手动指定的根节点断开,则无法进行自动修复。任何节点不会在存在指定根节点的情况下开始选举过程

中间父节点故障

如果中间父节点断开,则与之断开的子节点将主动尝试与该父节点重连。在多次重连尝试失败后,每个子节点开始扫描潜在父节点(请见 信标帧和 RSSI 阈值)。

如果存在其他可用的潜在父节点,每个子节点将分别给自己选择一个新的首选父节点(请见 首选父节点),并与它形成上行连接。如果特定子节点没有其他潜在的父节点,则将无限期地保持空闲状态。

下方示例展示了网络从中间父节点断开故障中进行自修复。

中间父节点故障的自修复示意图

​ 中间父节点故障的自修复

1. 网络中存在节点 A 至 G。

2. 节点 C 断开。节点 F/G 检测到节点 C 的断开故障,并尝试与节点 C 重新连接。在多次重连尝试失败后,节点 F/G 将开始选择新的首选父节点。

3. 节点 G 因其范围内不存在任何父节点而暂时保持空闲。节点 F 的范围中有 B 和 E 两个节点,但节点 B 因为所处层级更浅而当选新的父节点。节点 F 将与节点 B 连接后,并成为一个中间父节点,节点 G 将于节点 F 相连。这样一来,网络已经恢复了,但结构发生了变化(网络层级增加了 1 层)。

注解

如果子节点的父节点已被指定,则子节点不会尝试与其他潜在父节点连接。此时,该子节点将无限期地保持空闲状态。

根节点切换

除非根节点断开,否则 ESP-MESH 不会自动切换根节点。即使根节点的路由器 RSSI 降低至必须断开的情况,根节点也将保持不变。根节点切换是指明确启动新选举过程的行为,即具有更强路由器 RSSI 的节点选为新的根节点。这可以用于应对根节点性能降低的情况。

要触发根节点切换,当前根节点必须明确调用 esp_mesh_waive_root() 以触发新的选举。当下根节点将指示网络中的所有节点开始发送并扫描信标帧(见 自动根节点选择),但与此同时一直保持联网(即不会变为空闲节点)。如果另一个节点收到的票数超过当前根节点,则将启动根节点切换过程,否则根节点将保持不变

新选出的根节点向当前的根节点发送 切换请求,而原先的根节点将返回一个应答通知,表示已经准备好切换。一旦接收到应答,新选出的根节点将与其父节点断开连接,并迅速与路由器形成上行连接,进而成为网络的新根节点。原先的根节点将断开与路由器的连接,并与此同时保持其所有下行连接 并进入空闲状态。之前的根节点将开始扫描潜在的父节点并选择首选父节点。

下图说明了根节点切换的示例。

根节点切换示意图

​ 切换根节点示例

1. 节点 C 是当前的根节点,但路由器 RSSI 值 (-85 dB) 降低至较低水平。此时,新的选举过程被触发了。所有节点开始传输和扫描信标帧(此时仍保持连接)。

2. 经过多轮传输和扫描后,节点 B 被选为新的根节点。节点 B 向节点 C 发送了一个 切换请求,节点 C 回复一个应答。

3. 节点 B 与其父节点断开连接,并与路由器连接,成为网络中的新根节点。节点 C 与路由器断开连接,进入空闲状态,并开始扫描并选择新的首选父节点。 节点 C 在整个过程中仍保持其所有的下行连接

4. 节点 C 选择节点 B 作为其的首选父节点,与之形成上行连接,并成为一个第二层节点。由于节点 C 仍保持相同的子网,因此根节点切换后的网络结构没有变化。然后,由于切换的发生,节点 C 子网中每个节点的所处层级均增加了一层。如果根节点切换过程中产生了新的根节点,则 父节点切换 可以随后调整网络结构。

注解

根节点切换必须要求选举,因此只有在使用自组网 ESP-MESH 网络时才支持。换句话说,如果使用指定的根节点,则不能进行根节点切换。

父节点切换

父节点切换是指一个子节点将其上行连接切换到更浅一层的另一个父节点。父节点切换是自动的,这意味着如果较浅层出现了可用的潜在父节点(因“异步上电复位”产生),子节点将自动更改其上行连接。

所有潜在的父节点将定期发送信标帧(参见 信标帧和 RSSI 阈值),从而允许子节点扫描较浅层的父节点的可用性。由于父节点切换,自组网 ESP-MESH 网络可以动态调整其网络结构,以确保每个连接均具有良好的 RSSI 值,并且网络中的层级最小。

数据传输

ESP-MESH 数据包

ESP-MESH 网络使用 ESP-MESH 数据包传输数据。ESP-MESH 数据包 完全包含在 Wi-Fi 数据帧 中。ESP-MESH 网络中的多跳数据传输将涉及通过不同 Wi-Fi 数据帧在每个无线跳上传输的单个 ESP-MESH 数据包。

下图显示了 ESP-MESH 数据包的结构及其与 Wi-Fi 数据帧的关系。

ESP-MESH 数据包示意图

​ ESP-MESH 数据包

ESP-MESH 数据包的 报头 包含源节点和目标节点的 MAC 地址。选项 (option) 字段包含有关特殊类型 ESP-MESH 数据包的信息,例如组传输或来自外部 IP 网络的数据包(请参阅 MESH_OPT_SEND_GROUPMESH_OPT_RECV_DS_ADDR)。

ESP-MESH 数据包的 有效载荷 包含实际的应用数据。该数据可以为原始二进制数据,也可以是使用 HTTP、MQTT 和 JSON 等应用层协议的编码数据(请见:mesh_proto_t)。

注解

当向外部 IP 网络发送 ESP-MESH 数据包时,报头的目标地址字段将包含目标服务器的 IP 地址和端口号,而不是节点的 MAC 地址(请见:mesh_addr_t)。此外,根节点将处理外发 TCP/IP 数据包的形成。

组控制和组播

组播功能允许将单个 ESP-MESH 数据包同时发送给网络中的多个节点。ESP-MESH 中的组播可以通过“指定一个目标节点列表”或“预配置一个节点组”来实现。这两种组播方式均需调用 esp_mesh_send() 实现。

如果通过“指定目标节点列表”实现组播,用户必须首先将 ESP-MESH 数据包的目标地址设置为 组播组地址 (比如 01:00:5E:xx:xx:xx)。这表明 ESP-MESH 数据包是一个拥有一组地址的组播数据包,且该地址应该从报头选项中获得。然后,用户必须将目标节点的 MAC 地址列为选项(请见: mesh_opt_tMESH_OPT_SEND_GROUP)。这种组播方法不需要进行提前设置,但由于每个目标节点的 MAC 地址均需列为报头的选项字段,因此会产生大量开销数据。

分组组播允许 ESP-MESH 数据包被发送到一个预先配置的节点组。每个分组都有一个具有唯一性的 ID 标识。用户可通过 esp_mesh_set_group_id() 将节点加入一个组。分组组播需要将 ESP-MESH 数据包的目标地址设置为目标组的 ID,还必须设置 MESH_DATA_GROUP 标志位。分组组播产生的开销更小,但必须提前将节点加入分组中。

注解

在组播期间,网络中的所有节点在 MAC 层都会收到 ESP-MESH 数据包。然而,不包括在 MAC 地址列表或目标组中的节点将简单地过滤掉这些数据包。

广播

广播功能允许将单个 ESP-MESH 数据包同时发送给网络中的所有节点。每个节点可以将一个广播包转发至其所有上行和下行连接,使得数据包尽可能快地在整个网络中传播。但是,ESP-MESH 利用以下方法来避免在广播期间浪费带宽。

1. 当中间父节点收到来自其父节点的广播包时,它会将该数据包转发给自己的各个子节点,同时为自己保存一份数据包的副本。

2. 当中间父节点是广播的源节点时,它会将该数据包向上发送至其父节点,并向下发送给自己的各个子节点。

3. 当中间父节点接收到一个来自其子节点的广播包时,它会将该数据包转发给其父节点和其余子节点,同时为自己保存一份数据包的副本。

4. 当叶子节点是广播的源节点时,它会直接将该数据包发送至其父节点。

5. 当根节点是广播的源节点时,它会将该数据包发送至自己的所有子节点。

6. 当根节点收到来自其子节点的广播包时,它会将该数据包转发给其余子节点,同时为自己保存一份数据包的副本。

7. 当节点接收到一个源地址与自身 MAC 地址匹配的广播包时,它会将该广播包丢弃。

8. 当中间父节点收到一个来自其父节点的广播包时(该数据包最初来自该父节点的一个子节点),它会将该广播包丢弃。

上行流量控制

ESP-MESH 依赖父节点来控制其直接子节点的上行数据流。为了防止父节点的消息缓冲因上行传输过载而溢出,父节点将为每个子节点分配一个称为 接收窗口 的上行传输配额。 每个子节点均必须申请接收窗口才允许进行上行传输。接收窗口的大小可以动态调整。完成从子节点到父节点的上行传输包括以下步骤:

1. 在每次传输之前,子节点向其父节点发送窗口请求。窗口请求中包括一个序号,与子节点的待传输数据包相对应。

2. 父节点接收窗口请求,并将序号与子节点发送的前一个数据包的序号进行比较,用于计算返回给子节点的接收窗口大小。

3. 子节点根据父节点指定的窗口大小发送数据包。如果子节点的接收窗口耗尽,它必须通过发送请求获得另一个接收窗口,然后才允许继续发送。

注解

ESP-MESH 不支持任何下行流量控制。

警告

由于 父节点切换,数据包可能会在上行传输期间丢失。

由于根节点是通向外部 IP 网络的唯一接口,因此下行节点必须了解根节点与外部 IP 网络的连接状态。否则,节点可能会尝试向一个已经与 IP 网络断开连接的根节点发送数据,从而造成不必要的传输和数据包丢失。ESP-MESH 可以基于监测根节点和外部 IP 网络的连接状态,提供一种稳定外发数据吞吐量的机制。根节点可以通过调用 esp_mesh_post_toDS_state() 将自身与外部 IP 网络的连接状态广播给所有其他节点。

双向数据流

下图展示了 ESP-MESH 双向数据流涉及的各种网络层。

ESP-MESH 双向数据流示意图

​ ESP-MESH双向数据流

由于使用 路由表ESP-MESH 能够在 mesh 层中完全处理数据包的转发。TCP/IP 层仅与 mesh 网络的根节点有关,可帮助根节点与外部 IP 网络的数据包传送。

信道切换

背景

在传统的 Wi-Fi 网络中,信道 代表预设的频率范围。在基础设施基本服务集 (BSS) 中,工作 AP 及与之相连的 station 必须处于传输信标的工作信道(1 到 14)中。物理上相邻的 BSS 使用相同的工作信道会导致干扰产生和性能下降。

为了允许 BSS 适应不断变化的物理层条件并保持性能,Wi-Fi 网络中增加了 网络信道切换 的机制。网络信道切换是将 BSS 移至新的工作信道,并同时最大限度地减少期间对 BSS 的影响。然而,我们应该认识到,网络信道切换可能不会成功,无法将原信道中的所有 station 均移动至新的信道。

在基础设施 Wi-Fi 网络中,网络信道切换由 AP 触发,目的是将该 AP 及与之相连的所有 station 同步切换到新的信道。网络信道切换是通过在 AP 的周期性发送信标帧内嵌入一个 信道切换公告 (CSA) 元素来实现的。在网络信号切换前,该 CSA 元素用于向所有连接的 station 广播有关即将发生的网络信道切换,并且将包含在多个信标帧中。

一个 CSA 元素包含有关 新信道号信道切换计数 的信息。其中,信道切换计数 指示在网络信道切换之前剩余的信标帧间隔 (TBTT) 数量。因此,信道切换计数 依每个信标帧递减,并且允许与之连接的 station 与 AP 同步进行信道切换。

ESP-MESH 网络信道切换

ESP-MESH 网络信道切换还利用包含 CSA 元素的信标帧。然而,ESP-MESH 作为一个多跳网络,其信标帧可能无法到达网络中的所有节点(这点与单跳网络不同),因此信道切换过程更加复杂。因此,ESP-MESH 网络依赖于通过节点转发 CSA 元素,从而实现在整个网络中的传播。

当具有一个或多个子节点的中间父节点接收到包含 CSA 元素的信标帧时,该节点会将该元素包含在其下一个发送的信标帧(即具有相同的 新信道号信道切换计数)中,从而实现该 CSA 元素的转发。鉴于 ESP-MESH 网络中的所有节点都接收到相同的 CSA 元素,这些节点可以使用 信道切换计数 来同步其信道切换,但也会经历因 CSA 元素转发造成的延迟。

ESP-MESH 网络信道切换可以由路由器或根节点触发。

根节点触发

由根节点触发的信道切换只能在 ESP-MESH 网络未连接到路由器 时才会发生。通过调用 esp_mesh_switch_channel(),根节点将设置一个初始 信道切换计数 值,并开始在其信标帧中包含 CSA 元素。接着,每个 CSA 元素将抵达第二层节点,并通过第二层节点自己的信标帧继续进行向下转发。

路由器触发

当 ESP-MESH 网络连接到路由器时,整个网络必须与路由器采用同一个信道。因此,根节点在连接到路由器时无法触发信道切换

当根节点从路由器接收到包含 CSA 元素的信标帧时,根节点将 CSA 元素中的信道切换计数值设置为自定义值,然后再通过信标帧继续向下转发。此后,该 信道切换计数 将依转发次数相对于自定义值依次递减。该自定义值可以基于诸如网络层级、当前节点数等因素。

ESP-MESH 网络及其路由器可能具有不同且变化的信标间隔,因此需要将 信道切换计数 值设置为自定义值。也就是说,路由器提供的 信道切换计数 值与 ESP-MESH 网络无关。通过使用自定义值,ESP-MESH 网络中的节点能够相对于 ESP-MESH 网络的信标间隔同步切换信道。也正因如此,ESP-MESH 网络也会出现信道与路由器及其连接 station 的信道切换不同步的情况。

网络信道切换的影响

由于 ESP-MESH 网络信道切换与路由器的信道切换不同步,ESP-MESH 网络和路由器之间会出现 临时信道差异。

  • ESP-MESH 网络的信道切换时间取决于 ESP-MESH 网络的信标间隔和根节点的自定义 信道切换计数

  • 在 ESP-MESH 网络切换期间,信道差异将阻止根节点和路由器之间的任何数据交换。在 ESP-MESH 网络中,根节点和中间父节点将请求与其连接的子节点停止传输,直至信道切换发生(通过将 CSA 元素的 信道切换模式 字段置为 1)。

  • 频繁的路由器触发网络信道切换可能会降低 ESP-MESH 网络的性能。请注意,这可能是由 ESP-MESH 网络本身造成的(例如由于 ESP-MESH 网络的无线介质争用等原因)。此时,用户应该禁用路由器触发的自主信道切换,并直接指定一个信道。

当存在 临时信道差异 时,根节点从技术上来说仍保持连接至路由器。

  • 如果根节点经过一定数量信标间隔仍无法接到信标帧或探测来自路由器的响应,则会断开连接。

  • 断开连接时,根节点将自动重新扫描所有信道以确定是否存在路由器。

如果根节点无法接收任何路由器的 CSA 信标帧(例如短暂的路由器切换时间),则路由器将在没有 ESP-MESH 网络的情况下切换信道。

  • 在路由器切换信道后,根节点将不再能够接收路由器的信标帧和探测响应,并导致在一定数量的信标间隔后断开连接。

  • 在断开连接后,根节点将重新所有信道,寻找路由器。

  • 根节点将在整个过程中维护与之相连的下行连接。

注解

虽然 ESP-MESH 网络信道切换的目的是将网络内的所有节点移动到新的工作信道,但也应该认识到,信道切换可能无法成功移动所有节点(比如由于节点故障等原因)。

信道和路由器切换配置

ESP-MESH 允许通过配置启用或禁用自主信道切换。同样,也可以通过配置启用或禁用自主路由器切换(即当根节点自主连接到另一个路由器时)。自主信道切换和自主路由器切换取决于以下配置参数和运行时间条件。

允许信道切换:本参数决定是否允许 ESP-MESH 网络进行自主信道切换,具体可通过 mesh_cfg_t 结构体中的 allow_channel_switch 字段进行配置。

预设信道:ESP-MESH 网络可以将 mesh_cfg_t 结构体中的 channel 字段设置为相应的信道号,而具备一个预设信道。如果未设置此字段,则 allow_channel_switch 的设置将被覆盖,即始终允许信道切换。

允许路由器切换:本参数决定是否允许 ESP-MESH 网络进行自主路由器切换,具体可通过 mesh_router_t 结构体中的 allow_router_switch 字段进行配置。

预设路由器 BSSID:ESP-MESH 网络可以将 mesh_router_t 结构体的 bssid 字段设置为 目标路由器的 BSSID,而预设一个路由器。如果未设置此字段,则 allow_router_switch 的设置将被覆盖,即始终允许路由器切换。

存在根节点:根节点的存在也会影响是否允许信道或路由器切换。

下表说明了在不同参数/条件组合下是否允许信道切换和路由器切换。请注意,X 代表参数“不关心”。

预设信道 允许信道切换 预置路由器 BSSID 允许路由器切换 存在根节点 允许切换?
N X N X X 信道与路由器
N X Y N X 仅信道
N X Y Y X 信道与路由器
Y Y N X X 信道与路由器
Y N N X N 仅路由器
Y N N X Y 信道与路由器
Y Y Y N X 仅信道
Y N Y N N
Y N Y N Y 仅信道
Y Y Y Y X 信道与路由器
Y N Y Y N 仅路由器
Y N Y Y Y 信道与路由器

性能

ESP-MESH 网络的性能可以基于以下多个指标进行评估:

组网时长:从头开始构建 ESP-MESH 网络所需的总时长。

修复时间:从网络检测到节点断开到执行适当操作(例如生成新的根节点或形成新的连接等)以修复网络所需的时间。

每跳延迟:数据每经过一次无线 hop 而经历的延迟,即从父节点向子节点(或从子节点向父节点)发送一个数据包所需的时间。

网络节点容量:ESP-MESH 网络可以同时支持的节点总数。该指标取决于节点可以接受到的最大下行连接数和网络中允许的最大层级。

ESP-MESH 网络的常见性能指标如下表所示:

  • 组网时长:< 60 秒

  • 修复时间

    根节点断开:< 10 秒

    子节点断开:< 5 秒

  • 每条延迟:10 到 30 毫秒

注解

上述性能指标的测试条件见下。

  • 测试设备数量:100

  • 最大允许下行连接数量:6

  • 最大允许层级:6

注解

吞吐量取决于数据包错误率和 hop 数量。

根节点访问外部 IP 网络的吞吐量直接受到 ESP-MESH 网络中节点数量和路由器带宽的影响。

用户应注意,ESP-MESH 网络的性能与网络配置和工作环境密切相关。

更多注意事项

  • 数据传输使用 Wi-Fi WPA2-PSK 加密

  • Mesh 网络 IE 使用 AES 加密

安装 painlessMesh 库

要在 Arduino IDE 中使用 ESP-MESH,我们需要先安装一个库来支持 ESP-MESH 组网程序的编写,这个库的名称就是:painlessMesh。根据这个库的官方介绍,这个库可以简化 ESP-MESH 的程序编写,让你更加专注于功能的实现,而不必关心 ESP-MESH 网络架设与管理的细节。

painlessMesh is a library that takes care of the particulars of creating a simple mesh network using esp8266 and esp32 hardware. The goal is to allow the programmer to work with a mesh network without having to worry about how the network is structured or managed.

打开 Arduino 软件的库管理器,搜索 painlessMesh,即可选择对应的版本进行安装。

image-20210520131959323

在安装 painlessMesh 库的时候,如果提示你需要安装其他依赖库,选择同意安装全部库就行。

库依赖关系

painlessMesh 使用了以下库,这些库可以通过 Arduino 库管理器进行安装

  • ArduinoJson

  • TaskScheduler

  • ESPAsyncTCP (ESP8266)

  • AsyncTCP (ESP32)

局限和警告

  • 尽量避免在代码中使用 delay()。为了维护网格,我们需要在后台执行一些任务。使用 delay() 将阻止这些任务的发生,并可能导致网格失去稳定性/崩溃。相反,我们建议使用 painlessMesh 中包含的调度器。该调度器是微修改的 TaskScheduler 库版本。文档可以在这里找到。有关如何使用调度器的其他示例,请参见示例文件夹。

  • painlessMesh 订阅 WiFi 事件。请注意,因此 painlessMesh 可能与试图绑定到相同事件的用户程序/其他库不兼容。

  • 尽量在每分钟发送的消息(尤其是广播消息)的数量上保持保守。这是为了防止硬件过载。处理器在处理能力和内存方面都是有限的,因此很容易超载和破坏网格。尽管 MESH 网络试图阻止这种情况的发生,但并非总是能做到。

  • 由于高流量,消息可能丢失或丢失,您不能依赖于所有要传递的消息。其中一个建议是,每隔一段时间重新发送一次消息。另一种方法是让节点在收到消息时发送回复。如果发送节点在一定时间内没有得到回复,则可以重新发送消息。

API参考

使用 painlessMesh 是轻松的。

首先包括该库,并像这样创建一个painlessMesh对象。

#include <painlessMesh.h>
painlessMesh  mesh;

主要成员函数包括在下面。完整的文档可以在这里找到

init() - 初始化网状网络

void painlessMesh::init(String ssid, String password, uint16_t port = 5555, WiFiMode_t connectMode = WIFI_AP_STA, _auth_mode authmode = AUTH_WPA2_PSK, uint8_t channel = 1, phy_mode_t phymode = PHY_MODE_11G, uint8_t maxtpw = 82, uint8_t hidden = 0, uint8_t maxconn = 4)

将此添加到您的setup()函数。初始化网状网络。该例程执行以下操作。

  • 启动无线网络

  • 开始搜索属于网状网络的其他wifi网络

  • 登录到找到的最佳网状网络节点。如果找不到任何内容,它将在5秒钟内开始新的搜索。

ssid=网格的名称。所有节点共享相同的AP ssid。它们由BSSID区分。

password=您的网状网络的wifi密码。

port=您要在其上运行网状服务器的TCP端口。如果未指定,则默认为5555。

connectMode=在WIFI_AP,WIFI_STA和WIFI_AP_STA(默认)模式之间切换

stop() - 停止节点

void painlessMesh::stop()

停止节点。这将导致该节点与所有其他节点断开连接并停止/发送消息。

update() - 运行各种维护任务

void painlessMesh::update( void )

将此添加到您的loop()函数中该例程运行各种维护任务,并不是很有趣,但是没有它,事情将无法进行。

onReceive() - 为寻址到该节点的任何消息设置回调例程

void painlessMesh::onReceive( &receivedCallback )

为寻址到该节点的任何消息设置回调例程。回调例程具有以下结构。

void receivedCallback( uint32_t from, String &msg )

每次此节点收到消息时,都会调用此回调例程。

from是邮件原始发件人的ID。

msg是包含邮件的字符串。该消息可以是任何东西。JSON,其他一些文本字符串或二进制数据。

onNewConnection() - 每当本地节点建立新连接时,就会触发该事件

void painlessMesh::onNewConnection( &newConnectionCallback )

每当本地节点建立新连接时,就会触发该事件。回调具有以下结构。

void newConnectionCallback( uint32_t nodeId )

nodeId 是网格中新的连接节点ID。

onChangedConnections() - 每当网格拓扑发生更改时,就会触发此事件

void painlessMesh::onChangedConnections( &changedConnectionsCallback )

每当网格拓扑发生更改时,就会触发此事件。回调具有以下结构。

void onChangedConnections()

没有传递任何参数。这只是一个信号。

isConnected() - 返回给定节点当前是否连接到网格

bool painlessMesh::isConnected( nodeId )

返回给定节点当前是否连接到网格。

nodeId 是请求引用的节点ID。

onNodeTimeAdjusted() - 每次调整本地时间以使其与网格时间同步时,都会触发该事件

void painlessMesh::onNodeTimeAdjusted( &nodeTimeAdjustedCallback )

每次调整本地时间以使其与网格时间同步时,都会触发该事件。回调具有以下结构。

void onNodeTimeAdjusted(int32_t offset)

offset 是已经计算并应用于本地时钟的调整增量。

onNodeDelayReceived() - 发送请求后,如果收到时间延迟保证响应,则会触发此事件

void onNodeDelayReceived(nodeDelayCallback_t onDelayReceived)

发送请求后,如果收到时间延迟保证响应,则会触发此事件。回调具有以下结构。

void onNodeDelayReceived(uint32_t nodeId, int32_t delay)

nodeId 发起响应的节点。

delay network trip延迟,以微秒为单位。

sendBroadcast() - 将消息发送到整个网状网络上的每个节点

bool painlessMesh::sendBroadcast( String &msg, bool includeSelf = false)

将消息发送到整个网状网络上的每个节点。默认情况下,当前节点不接收消息(includeSelf = false)。includeSelf = true会覆盖此行为,从而导致receivedCallback在发送广播消息时调用。

如果一切正常,则返回true;否则,则返回false。如果失败,则将错误消息打印到Serial.print。

sendSingle() - 使用ID == dest将消息发送到节点

bool painlessMesh :: sendSingle(uint32_t dest,String&msg)

使用ID == dest将消息发送到节点。

如果一切正常,则返回true;否则,则返回false。如果失败,则将错误消息打印到Serial.print。

subConnectionJson() - 以JSON格式返回网格拓扑

String painlessMesh::subConnectionJson()

以JSON格式返回网格拓扑。

getNodeList() - 获取所有已知节点的列表

std::list<uint32_t> painlessMesh::getNodeList()

获取所有已知节点的列表。这包括直接和间接连接到当前节点的节点。

getNodeId() - 返回我们正在运行的节点的chipId

uint32_t painlessMesh::getNodeId( void )

返回我们正在运行的节点的chipId。

getNodeTime() - 返回网格时基微秒计数器

uint32_t painlessMesh::getNodeTime( void )

返回网格时基微秒计数器。从第一个节点启动开始,经过71分钟。

节点尝试使用基于SNTP的协议保持公共时基彼此同步

startDelayMeas() - 向节点发送数据包以测量到该节点的网络行程延迟

bool painlessMesh::startDelayMeas(uint32_t nodeId)

向节点发送数据包以测量到该节点的网络行程延迟。如果将nodeId连接到网格,则返回true,否则返回false。调用此函数后,用户程序必须等待所指定的回调形式的响应void painlessMesh::onNodeDelayReceived(nodeDelayCallback_t onDelayReceived)

nodeDelayCallback_t是形式为的功能void (uint32_t nodeId, int32_t delay)

stationManual() - 将节点连接到网格外部的AP

void painlessMesh::stationManual( String ssid, String password, uint16_t port, uint8_t *remote_ip )

将节点连接到网格外部的AP。当指定remote_ip和时port,该节点在建立WiFi连接后打开TCP连接。

注意:网格必须与AP在同一WiFi通道上。

ESP-MESH 基础讲解

先打开 painlessMesh 库的基础示例:basic.ino 程序,来了解一下这个库的基本使用方法。basic 示例程序路径如下:Arduino IDE → 文件 →Edge101WE Painless Mesh → basic

basic.ino 程序如下:

//************************************************************
// this is a simple example that uses the painlessMesh library
//
// 1. sends a silly message to every node on the mesh at a random time between 1 and 5 seconds
// 2. prints anything it receives to Serial.print
//
//
//************************************************************
#include "esp32painlessMesh.h"

#define   MESH_PREFIX     "whateverYouLike"
#define   MESH_PASSWORD   "somethingSneaky"
#define   MESH_PORT       5555

Scheduler userScheduler; // to control your personal task
painlessMesh  mesh;

// User stub
void sendMessage() ; // Prototype so PlatformIO doesn't complain

Task taskSendMessage( TASK_SECOND * 1 , TASK_FOREVER, &sendMessage );

void sendMessage() {
  String msg = "Hello from node ";
  msg += mesh.getNodeId();
  mesh.sendBroadcast( msg );
  taskSendMessage.setInterval( random( TASK_SECOND * 1, TASK_SECOND * 5 ));
}

// Needed for painless library
void receivedCallback( uint32_t from, String &msg ) {
  Serial.printf("startHere: Received from %u msg=%s\n", from, msg.c_str());
}

void newConnectionCallback(uint32_t nodeId) {
    Serial.printf("--> startHere: New Connection, nodeId = %u\n", nodeId);
}

void changedConnectionCallback() {
  Serial.printf("Changed connections\n");
}

void nodeTimeAdjustedCallback(int32_t offset) {
    Serial.printf("Adjusted time %u. Offset = %d\n", mesh.getNodeTime(),offset);
}

void setup() {
  Serial.begin(115200);

//mesh.setDebugMsgTypes( ERROR | MESH_STATUS | CONNECTION | SYNC | COMMUNICATION | GENERAL | MSG_TYPES | REMOTE ); // all types on
  mesh.setDebugMsgTypes( ERROR | STARTUP );  // set before init() so that you can see startup messages

  mesh.init( MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT );
  mesh.onReceive(&receivedCallback);
  mesh.onNewConnection(&newConnectionCallback);
  mesh.onChangedConnections(&changedConnectionCallback);
  mesh.onNodeTimeAdjusted(&nodeTimeAdjustedCallback);

  userScheduler.addTask( taskSendMessage );
  taskSendMessage.enable();
}

void loop() {
  // it will run the user scheduler as well
  mesh.update();
}

MESH 鉴权信息

首先在程序的开头,我们引入了 painlessMesh 这个库,以便后续程序可以使用这个库的相关功能:

#include "painlessMesh.h"

接着对 ESP-MESH 组网的一些鉴权信息进行设定,以便具有相同鉴权信息的设备,可以互相组网。其中,MESH_PREFIX 可以理解为 ESP-MESH 网络的账号,MESH_PASSWORD 则是 ESP-MESH 网络的密码,MESH_PORT 为ESP-MESH 网络的端口号。这 3 个信息可以根据你的需要随便修改,只要互相 MESH 组网的设备之间这 3 个信息相同即可。设置完这些信息之后,就可以实例化一个 mesh 对象,用来处理后续的各种信息收发。

#define   MESH_PREFIX     "whateverYouLike"
#define   MESH_PASSWORD   "somethingSneaky"
#define   MESH_PORT       5555

painlessMesh  mesh;

MESH 设备发送信息

然后实例化一个 Scheduler 任务管理器 userScheduler,帮助ESP- MESH 网络设备调度他们的任务,比如说定时更新传感器相关的信息,然后将数据发送给其他设备等。Scheduler 任务管理也是 painlessMesh 库推荐使用的方式,因为在 ESP-MESH 网络中要尽量避免延时 delay() 相关的代码。实例化 userScheduler 之后,再来创建一个任务 taskSendMessage,并且调用 sendMessage() 函数,这个函数就是负责向其他设备发送信息的。

Scheduler userScheduler;

void sendMessage();

Task taskSendMessage( TASK_SECOND * 1 , TASK_FOREVER, &sendMessage );

sendMessage() 函数的具体内容如下,在这个函数中,实现了向其他节点设备发送了一句打招呼的语句。如果我们要向其他设备发送传感器信息或者数据,只要去修改这个函数中的内容即可。

void sendMessage() {
  String msg = "Hello from node ";
  msg += mesh.getNodeId();
  mesh.sendBroadcast( msg );
  taskSendMessage.setInterval( random( TASK_SECOND * 1, TASK_SECOND * 5 ));
}

MESH 网络回调函数

接下来是几个 painlessMesh 库必备的回调函数实现。

  • receivedCallback() 函数:负责将从其他设备接收到的信息在串口监视器中打印出来;

  • newConnectionCallback() 函数:负责通知有没有新设备接入到 ESP-MESH 网络中;

  • changedConnectionCallback() 函数:负责通知 ESP-MESH 网络连接出现变化,比如有设备离线或有新设备加入等;

  • nodeTimeAdjustedCallback() 函数:负责打印时间同步信息,以确保 ESP-MESH 网络中所有设备的时间是同步的。

void receivedCallback( uint32_t from, String &msg ) {
  Serial.printf("startHere: Received from %u msg=%s\n", from, msg.c_str());
}

void newConnectionCallback(uint32_t nodeId) {
    Serial.printf("--> startHere: New Connection, nodeId = %u\n", nodeId);
}

void changedConnectionCallback() {
  Serial.printf("Changed connections\n");
}

void nodeTimeAdjustedCallback(int32_t offset) {
    Serial.printf("Adjusted time %u. Offset = %d\n", mesh.getNodeTime(),offset);
}

setup() 初始化设置

在 setup() 初始化程序中,先初始化串口,方便后面打印信息。

Serial.begin(115200);

然后是对 ESP-MESH 网络进行相关初始化设置:

  • 设置打印的调试信息类型为 ERROR 与 STARTUP 等级;

  • 然后根据 MESH_PREFIX、MESH_PASSWORD、userScheduler、MESH_PORT 等信息初始化 MESH 网络;

  • 借着分别设置 onReceive(接收到消息时)、onNewConnection(有新的设备连接时)、onChangedConnections(连接的设备发生变化时)、onNodeTimeAdjusted(节点设备时间调整并同步时)的回调函数,用来处理 ESP-MESH 网络事件。

mesh.setDebugMsgTypes( ERROR | STARTUP );

mesh.init( MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT );
mesh.onReceive(&receivedCallback);
mesh.onNewConnection(&newConnectionCallback);
mesh.onChangedConnections(&changedConnectionCallback);
mesh.onNodeTimeAdjusted(&nodeTimeAdjustedCallback);

最后在 setup() 中设置定时发送消息任务,以及对任务使能。

userScheduler.addTask( taskSendMessage );
taskSendMessage.enable();

loop() 重复运行

loop() 程序非常简单,只要不断去更新 ESP-MESH 网络即可:

mesh.update();

程序测试

分别将程序上传到 2 块以上的 Edge101WE 主板中,然后分别打开不同的串口监视器,可以看到从其他 ESP-MESH 网络设备上发来的信息,以及一些网络状态变更的信息。

将所有 Edge101WE 主板上电,打开其中一块板子的串口监视器,可以看到从其他不同节点 ID 对应的设备发送过来的消息。

startHere: Received from 859777202 msg=Hello from node 859777202
startHere: Received from 768999178 msg=Hello from node 768999178
startHere: Received from 768999178 msg=Hello from node 768999178
startHere: Received from 859777202 msg=Hello from node 859777202
startHere: Received from 768999178 msg=Hello from node 768999178

偶尔也会打印出时间同步的消息:

Adjusted time 603810989. Offset = -2822554
Adjusted time 604061057. Offset = 12174
Adjusted time 604325461. Offset = 7404
Adjusted time 604524669. Offset = -18167
Adjusted time 604749029. Offset = -369
startHere: Received from 859777202 msg=Hello from node 859777202

当拔下其中某一块 Edge101WE 主板的电源之后,可以看到连接发生变化的消息被打印出来。

Changed connections
--> startHere: New Connection, nodeId = 768999178
Adjusted time 603810989. Offset = -2822554
Adjusted time 604061057. Offset = 12174
Adjusted time 604325461. Offset = 7404
Adjusted time 604524669. Offset = -18167
Adjusted time 604749029. Offset = -369
startHere: Received from 859777202 msg=Hello from node 859777202
startHere: Received from 768999178 msg=Hello from node 768999178

当有新的 Edge101WE 主板上电加入 ESP-MESH 网络时,也可以在串口监视器中看到新设备加入的信息。

--> startHere: New Connection, nodeId = 768999178
Adjusted time 603810989. Offset = -2822554
Adjusted time 604061057. Offset = 12174
Adjusted time 604325461. Offset = 7404
Adjusted time 604524669. Offset = -18167
Adjusted time 604749029. Offset = -369
startHere: Received from 859777202 msg=Hello from node 859777202
startHere: Received from 768999178 msg=Hello from node 768999178

这些信息充分说明了 ESP-MESH 具有自组网和自修复的特性,可以自主地构建和维护。

ESP-MESH 组网实例

上面初步学习了 ESP-MESH 网络的编程,下面我们修改 basic 程序,来实现一个简单设备组网实例。

我们使用三个 Edge101WE 主板,当任意主板的板载按钮按下,使其他两个主板的LED灯亮,当释放按钮LED灯灭。

发送回调函数代码修改

在前面讲解的 basic 程序的基础之上,我们只需要修改 sendMessage() 函数即可:通过检测GPIO38引脚上的用户按钮,如果按钮有按下,发送“Light on”信息,没人的话,发送“Light off”信息给其他设备。

#define UserButton 38

void sendMessage() {
  pinMode(UserButton, INPUT);
  String msg = "Message from node PIR: ";
  if (digitalRead(UserButton) ) {
    msg += "Light on.";
  } else {
    msg += "Light off.";
  } 
  mesh.sendBroadcast( msg );
}

接收回调函数代码修改

当接收到其他主板发送的 Light on 信息就打开LED灯,否则就关闭LED灯。

#define USerLED 15

void receivedCallback( uint32_t from, String &msg ) {
  pinMode(USerLED, OUTPUT);
  Serial.printf("startHere: Received from %u msg=%s\n", from, msg.c_str());
  if (msg.indexOf("Light on") > 0) {
    digitalWrite(USerLED, HIGH);
  } else {
    digitalWrite(USerLED, LOW);
  }
}

例程:三块 Edge101WE 主板按钮和LED互相控制

#include "esp32painlessMesh.h"

#define   MESH_PREFIX     "whateverYouLike"
#define   MESH_PASSWORD   "somethingSneaky"
#define   MESH_PORT       5555

#define	  UserButton 	  38
#define   USerLED          15

Scheduler userScheduler; // to control your personal task
painlessMesh  mesh;

// User stub
void sendMessage() ; // Prototype so PlatformIO doesn't complain

Task taskSendMessage( TASK_SECOND * 1 , TASK_FOREVER, &sendMessage );

void sendMessage() {
  pinMode(UserButton, INPUT_PULLUP);
  String msg = "Message from node PIR: ";
  if (digitalRead(UserButton) == 0) {
    msg += "Light on.";
  } else {
    msg += "Light off.";
  } 
  mesh.sendBroadcast( msg );
}

// Needed for painless library
void receivedCallback( uint32_t from, String &msg ) {
  pinMode(USerLED, OUTPUT);
  Serial.printf("startHere: Received from %u msg=%s\n", from, msg.c_str());
  if (msg.indexOf("Light on") > 0) {
    digitalWrite(USerLED, HIGH);
  } else {
    digitalWrite(USerLED, LOW);
  }
}

void newConnectionCallback(uint32_t nodeId) {
    Serial.printf("--> startHere: New Connection, nodeId = %u\n", nodeId);
}

void changedConnectionCallback() {
  Serial.printf("Changed connections\n");
}

void nodeTimeAdjustedCallback(int32_t offset) {
    Serial.printf("Adjusted time %u. Offset = %d\n", mesh.getNodeTime(),offset);
}

void setup() {
  Serial.begin(115200);

//mesh.setDebugMsgTypes( ERROR | MESH_STATUS | CONNECTION | SYNC | COMMUNICATION | GENERAL | MSG_TYPES | REMOTE ); // all types on
  mesh.setDebugMsgTypes( ERROR | STARTUP );  // set before init() so that you can see startup messages

  mesh.init( MESH_PREFIX, MESH_PASSWORD, &userScheduler, MESH_PORT );
  mesh.onReceive(&receivedCallback);
  mesh.onNewConnection(&newConnectionCallback);
  mesh.onChangedConnections(&changedConnectionCallback);
  mesh.onNodeTimeAdjusted(&nodeTimeAdjustedCallback);

  userScheduler.addTask( taskSendMessage );
  taskSendMessage.enable();
}

void loop() {
  // it will run the user scheduler as well
  mesh.update();
}

9.12 MQTT

教程前面介绍的 HTTP 连接属于短连接,而在物联网应用中,广泛应用的却是 MQTT 协议。Arduino 平台上常用的 MQTT 实现库是 PubSubClient

9.12.1 MQTT协议

简介

MQTT 协议(Message Queuing Telemetry Transport),翻译过来就是遥信消息队列传输,是 IBM公 司于1999年提出的,现在最新版本是 3.1.1。MQTT是一个基于 TCP 的发布订阅协议,设计的初始目的是为了极有限的内存设备和网络带宽很低的网络不可靠的通信,非常适合物联网通信。

img

MQTT 属于应用层协议,基于 TCP 协议,确保了可靠性。MQTT协议详细介绍可以参考 MQTT中文文档

MQTT通信模型如下:

img

  • 发布方(Publisher)将消息发送到 Broker(MQTT服务器)。

  • Broker 接收到消息以后,检查下都有哪些订阅方订阅了此类消息,然后将消息发送到这些订阅方。

  • 订阅方(Subscriber)从 Broker 获取该消息。

MQTT 消息的 QOS

MQTT 消息支持三种 QOS 等级:

  • QoS 0:最多一次,消息发布完全依赖底层 TCP/IP 网络。分发的消息可能丢失或重复。例如,这个等级可用于环境传感器数据,单次的数据丢失没关系,因为不久后还会有第二次发送。

  • QoS 1:至少一次,确保消息可以到达,但消息可能会重复。

  • QoS 2:只有一次,确保消息只到达一次。例如,这个等级可用在一个计费系统中,这里如果消息重复或丢失会导致不正确的收费。

MQTT 控制报文格式

MQTT 控制报文由三部分组成:

  • 固定报头(Fixed header),每个 MQTT 控制报文都包含一个固定报头;固定报头指明控制报文类型、标志 Flags、剩余长度三大部分。

  • 可变报头(Variable header),某些 MQTT 控制报文包含一个可变报头部分;它在固定报头和有效载荷之间;可变报头的内容根据报文类。

  • 型的不同而不同,通常包括 报文标识符(Packet Identifier)。

  • 有效载荷(Payload),某些 MQTT 控制报文在报文的最后部分包含一个有效载荷,也就是携带的数据信息。

整体上说,MQTT整体控制报文协议就是:

固定报头(一定有) + 可变报头(部分有) + 有效载荷(部分有)

数据结构简单,传输数据量小,这也是为什么能应用于物联网应用的原因之一。

MQTT控制报文

CONNECT – 连接服务端

注意点:

  • 客户端到服务端的网络连接建立后,客户端发送给服务端的第一个报文必须是 CONNECT 报文;

  • 在一个网络连接上,客户端只能发送一次 CONNECT 报文。服务端必须将客户端发送的第二个 CONNECT 报文当作协议违规处理并断开客户端的连接;

  • 有效载荷包含一个或多个编码的字段。包括客户端的唯一标识符,Will 主题,Will 消息,用户名和密码。除了客户端标识之外,其它的字段都是可选的,基于标志位来决定可变报头中是否需要包含这些字段。

报文格式:

固定报头 + 可变报头 + 有效载荷

  • 固定报头: MQTTCONNECT(1 << 4)

  • 可变报头: 协议名(Protocol Name),协议级别(Protocol Level),连接标志(Connect Flags)和保持连接(Keep Alive)

  • 有效载荷: 客户端标识符,遗嘱主题,遗嘱消息,用户名,密码

CONNACK – 确认连接请求

注意点:

  • 服务端发送 CONNACK 报文响应从客户端收到的 CONNECT 报文。服务端发送给客户端的第一个报文必须是 CONNACK;

  • 如果客户端在合理的时间内没有收到服务端的 CONNACK 报文,客户端应该关闭网络连接。合理 的时间取决于应用的类型和通信基础设施;

报文格式:

固定报头 + 可变报头

  • 固定报头: MQTTCONNACK(2 << 4)

  • 可变报头: 连接确认标志 + 连接返回码

PUBLISH – 发布消息

注意点:

PUBLISH 控制报文是指从客户端向服务端或者服务端向客户端传输一个应用消息;

报文格式:

固定报头 + 可变报头 + 有效载荷

  • 固定报头: MQTTPUBLISH(3 << 4),重发标志 DUP,服务质量等级 QoS,保留标志 RETAIN,剩余长度字段

  • 可变报头: 主题名和报文标识符

  • 有效载荷: 被发布的应用消息

PUBACK – 发布确认

注意点:

  • PUBACK 报文是对 QoS 1 等级的 PUBLISH 报文的响应

报文格式:

固定报头 + 可变报头

  • 固定报头: MQTTPUBACK(4 << 4),剩余长度字段

  • 可变报头: 包含等待确认的PUBLISH报文的报文标识符

PUBREC – 发布收到(QoS 2,第一步)

注意点:

  • PUBREC 报文是对 QoS 等级 2 的 PUBLISH 报文的响应。它是 QoS 2 等级协议交换的第二个报文。

报文格式:

固定报头 + 可变报头

  • 固定报头: MQTTPUBREC(5 << 4),剩余长度字段

  • 可变报头: 包含等待确认的PUBLISH报文的报文标识符

PUBREL – 发布释放(QoS 2,第二步)

注意点:

  • PUBREL 报文是对 PUBREC 报文的响应。它是 QoS 2 等级协议交换的第三个报文。

报文格式:

固定报头 + 可变报头

  • 固定报头: MQTTPUBREL(6 << 4),剩余长度字段

  • 可变报头: 包含与等待确认的 PUBREC 报文相同的报文标识符。

PUBCOMP – 发布完成(QoS 2,第三步)

注意点:

  • PUBCOMP 报文是对 PUBREL 报文的响应。它是 QoS 2 等级协议交换的第四个也是最后一个报文。

报文格式:

固定报头 + 可变报头

  • 固定报头: MQTTPUBCOMP(7 << 4),剩余长度字段

  • 可变报头: 包含与等待确认的 PUBREL 报文相同的报文标识符

SUBSCRIBE – 订阅主题

注意点:

  • 客户端向服务端发送 SUBSCRIBE 报文用于创建一个或多个订阅

  • 为了将应用消息转发给与那些订阅匹配的主题,服务端发送 PUBLISH 报文给客户端

  • SUBSCRIBE 报文也(为每个订阅)指定了最大的 QoS 等级,服务端根据这个发送应用消息给客户端

报文格式:

固定报头 + 可变报头 + 有效载荷

  • 固定报头: MQTTSUBSCRIBE(8 << 4),剩余长度字段

  • 可变报头: 报文标识符

  • 有效载荷:包含了一个主题过滤器列表,它们表示客户端想要订阅的主题

SUBACK – 订阅确认

注意点:

  • 服务端发送 SUBACK 报文给客户端,用于确认它已收到并且正在处理 SUBSCRIBE 报文

  • SUBACK 报文包含一个返回码清单,它们指定了 SUBSCRIBE 请求的每个订阅被授予的最大 QoS 等级

报文格式:

固定报头 + 可变报头 + 有效载荷

  • 固定报头: MQTTSUBACK(9 << 4),剩余长度字段

  • 可变报头: 包含等待确认的 SUBSCRIBE 报文的报文标识符

  • 有效载荷:包含一个返回码清单。每个返回码对应等待确认的 SUBSCRIBE 报文中的一个主题过滤器

UNSUBSCRIBE – 取消订阅

注意点:

  • 客户端发送 UNSUBSCRIBE 报文给服务端,用于取消订阅主题

报文格式:

固定报头 + 可变报头 + 有效载荷

  • 固定报头: MQTTUNSUBSCRIBE(10 << 4),剩余长度字段

  • 可变报头: 报文标识符

  • 有效载荷:包含客户端想要取消订阅的主题过滤器列表

UNSUBACK – 取消订阅确认

注意点:

  • 服务端发送 UNSUBACK 报文给客户端用于确认收到 UNSUBSCRIBE 报文

报文格式:

固定报头 + 可变报头

  • 固定报头: MQTTUNSUBACK(11 << 4),剩余长度字段

  • 可变报头: 包含等待确认的 UNSUBSCRIBE 报文的报文标识符

PINGREQ – 心跳请求

注意点:

客户端发送 PINGREQ 报文给服务端

  • 在没有任何其它控制报文从客户端发给服务端时,告知服务端客户端还活着

  • 请求服务端发送 响应确认它还活着

  • 使用网络以确认网络连接没有断开

  • 保持连接(Keep Alive)处理中用到这个报文

报文格式:

固定报头

  • 固定报头: MQTTPINGREQ(12 << 4),剩余长度字段

PINGRESP – 心跳响应

注意点:

  • 服务端发送 PINGRESP 报文响应客户端的 PINGREQ 报文

  • 保持连接(Keep Alive)处理中用到这个报文

报文格式:

固定报头

  • 固定报头: MQTTPINGRESP(13 << 4),剩余长度字段

DISCONNECT – 断开连接

注意点:

  • DISCONNECT 报文是客户端发给服务端的最后一个控制报文。

  • 表示客户端正常断开连接。

报文格式:

固定报头

  • 固定报头: MQTTDISCONNECT(14 << 4),剩余长度字段

9.12.2 ArduinoMQTT库 - PubSubClient

API参考

PubSubClient() - 初始化构造器

PubSubClient();
PubSubClient(Client& client);
PubSubClient(IPAddress, uint16_t, Client& client);
PubSubClient(IPAddress, uint16_t, Client& client, Stream&);
PubSubClient(IPAddress, uint16_t, MQTT_CALLBACK_SIGNATURE,Client& client);
PubSubClient(IPAddress, uint16_t, MQTT_CALLBACK_SIGNATURE,Client& client, Stream&);
PubSubClient(uint8_t *, uint16_t, Client& client);
PubSubClient(uint8_t *, uint16_t, Client& client, Stream&);
PubSubClient(uint8_t *, uint16_t, MQTT_CALLBACK_SIGNATURE,Client& client);
PubSubClient(uint8_t *, uint16_t, MQTT_CALLBACK_SIGNATURE,Client& client, Stream&);
PubSubClient(const char*, uint16_t, Client& client);
PubSubClient(const char*, uint16_t, Client& client, Stream&);
PubSubClient(const char*, uint16_t, MQTT_CALLBACK_SIGNATURE,Client& client);
PubSubClient(const char*, uint16_t, MQTT_CALLBACK_SIGNATURE,Client& client, Stream&);

创建一个没有初始化的 PubSubClient 对象。在使用 PubSubClient 对象之前,必须配置完整的内容。

语法

WiFiClient espClient;
PubSubClient client;

void setup() {
    client.setClient(espClient);
    client.setServer("broker.example.com",1883);
    // client is now configured for use
}

参数

传入值 说明 值范围
Client& client 客户端实例
IPAddress addr mqtt服务器ip地址
uint16_t post mqtt服务器端口
Stream& 可选的实例Stream,用于存储收到的消息
MQTT_CALLBACK_SIGNATURE callback方法
const char* domain mqtt服务器域名

返回

setServer() - 配置服务器

PubSubClient& setServer(IPAddress ip, uint16_t port);
PubSubClient& setServer(uint8_t * ip, uint16_t port);
PubSubClient& setServer(const char * domain, uint16_t port);

语法

WiFiClient espClient;
PubSubClient client;

void setup() {
    client.setClient(espClient);
    client.setServer("broker.example.com",1883);
    // client is now configured for use
}

参数

传入值 说明 值范围
IPAddress ip MQTT服务器ip地址,IPAddress
uint8_t * ip MQTT服务器ip地址,数组
uint16_t port MQTT服务器端口
const char * domain MQTT服务器domain地址

返回

返回值 说明 值范围
*this 返回this指针,意味着我们可以实现链式调用

setCallback() - 处理消息回调

PubSubClient& setCallback(MQTT_CALLBACK_SIGNATURE);

语法

// 设置订阅消息回调
client.setCallback(callback);

参数

传入值 说明 值范围
MQTT_CALLBACK_SIGNATURE 回调签名

返回

返回值 说明 值范围
*this 返回this指针,意味着我们可以实现链式调用

注意

  • MQTT_CALLBACK_SIGNATURE是一个函数定义

setClient() - 配置客户端

PubSubClient& setClient(Client& client);

语法

WiFiClient espClient;
PubSubClient client;

void setup() {
    client.setClient(espClient);
    client.setServer("broker.example.com",1883);
    // client is now configured for use
}

参数

传入值 说明 值范围
Client& client client实例,比如wificlient

返回

返回值 说明 值范围
*this 返回this指针,意味着我们可以实现链式调用

setStream() - 配置流

PubSubClient& setStream(Stream& stream);

语法

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

// Update these with values suitable for your network.
byte mac[]= {  0xDE, 0xED, 0xBA, 0xFE, 0xFE, 0xED };
IPAddress ip(172, 16, 0, 100);
IPAddress server(172, 16, 0, 2);

SRAM sram(4, SRAM_1024);

void callback(char* topic, byte* payload, unsigned int length) {
  sram.seek(1);

  // do something with the message
  for(uint8_t i=0; i<length; i++) {
Serial.write(sram.read());
  }
  Serial.println();

  // Reset position for the next message to be stored
  sram.seek(1);
}

EthernetClient ethClient;
PubSubClient client(server, 1883, callback, ethClient, sram);

void setup()
{
  Ethernet.begin(mac, ip);
  if (client.connect("arduinoClient")) {
client.publish("outTopic","hello world");
client.subscribe("inTopic");
  }

  sram.begin();
  sram.seek(1);

  Serial.begin(9600);
}

void loop()
{
  client.loop();
}

参数

传入值 说明 值范围
Stream& stream

返回

返回值 说明 值范围
*this 客户端实例,返回this指针,意味着我们可以实现链式调用

connect() - 连接MQTT服务(CONNECT控制报文)

boolean connect(const char* id);
boolean connect(const char* id, const char* user, const char* pass);
boolean connect(const char* id, const char* willTopic, uint8_t willQos, boolean willRetain, const char* willMessage);
boolean connect(const char* id, const char* user, const char* pass, const char* willTopic, uint8_t willQos, boolean willRetain, const char* willMessage);

语法

// Attempt to connect
if (client.connect(clientId.c_str())) {
    Serial.println("connected");
    

参数

传入值 说明 值范围
const char* id client端标识符
const char* user 用户账号
const char* pass 用户密码
const char* willTopic 遗嘱主题
uint8_t willQos 遗嘱消息质量等级
boolean willRetain 是否保留信息
const char* willMessage 遗嘱内容

返回

返回值 说明 值范围
bool true:连接成功
false:连接不成功

注意

  • MQTT_KEEPALIVE 默认 15 S;

  • MQTT_SOCKET_TIMEOUT 默认15 S;

disconnect() - 断开连接(DISCONNECT报文)

void disconnect();

客户端断开连接 (客户端发给服务端的最后一个控制报文。表示客户端正常断开连接)

语法

client.disconnect();

参数

返回

publish() - 发布消息

boolean publish(const char* topic, const char* payload);
boolean publish(const char* topic, const char* payload, boolean retained);
boolean publish(const char* topic, const char* payload, unsigned int plength,boolean retained);
boolean publish(const char* topic, const uint8_t * payload, unsigned int plength);
boolean publish(const char* topic, const uint8_t * payload, unsigned int plength, boolean retained);
boolean publish_P(const char* topic, const uint8_t * payload, unsigned int plength, boolean retained);

语法

if (client.connect(clientId.c_str())) {
      Serial.println("connected");
      // Once connected, publish an announcement...
      client.publish("outTopic", "hello world");
      // ... and resubscribe
      client.subscribe("inTopic");

参数

传入值 说明 值范围
const char* topic 主题
const char* payload 有效负载
boolean retained 是否保持
unsigned int plength 负载内容长度

返回

返回值 说明 值范围

subscribe() - 订阅主题(SUBSCRIBE报文)

boolean subscribe(const char* topic);
boolean subscribe(const char* topic, uint8_t qos);

订阅主题 (客户端向服务端发送 SUBSCRIBE 报文用于创建一个或多个订阅)

语法

if (client.connect(clientId.c_str())) {
      Serial.println("connected");
      // Once connected, publish an announcement...
      client.publish("outTopic", "hello world");
      // ... and resubscribe
      client.subscribe("inTopic");

参数

传入值 说明 值范围
const char* topic 主题
uint8_t qos 质量等级

返回

返回值 说明 值范围
bool true:订阅成功
false:订阅不成功

unsubscribe() - 取消订阅主题

boolean unsubscribe(const char* topic);

取消订阅主题 (客户端发送 UNSUBSCRIBE 报文给服务端,用于取消订阅主题)

取消订阅报文格式: 固定报头(报文类型+剩余长度) + 可变报头(报文标识符)+ 有效载荷(主题过滤器列表)

语法


参数

传入值 说明 值范围
const char* topic 具体主题

返回

返回值 说明 值范围
bool true:取消成功
false:取消不成功

connected() - 判断客户端是否连接上服务器

boolean connected();

语法

void loop() {
  // 重连机制
  if (!client.connected()) {
    reconnect();
  }

参数

返回

返回值 说明 值范围
bool true:连接上服务器
false:没有连接上服务器

loop() - 处理消息以及保持心跳

boolean loop();

语法

void loop() {
    // 不断监听信息
    client.loop();
}

参数

返回

返回值 说明 值范围
bool true:有连接
false:未连接

state() - 获取MQTT客户端当前状态

int state();

语法

Serial.print(client.state());

参数

返回

返回值 说明 值范围
int 状态定义:
MQTT_CONNECTION_TIMEOUT -4
MQTT_CONNECTION_LOST -3
MQTT_CONNECT_FAILED -2
MQTT_DISCONNECTED -1
MQTT_CONNECTED 0
MQTT_CONNECT_BAD_PROTOCOL 1
MQTT_CONNECT_BAD_CLIENT_ID 2
MQTT_CONNECT_UNAVAILABLE 3
MQTT_CONNECT_BAD_CREDENTIALS 4
MQTT_CONNECT_UNAUTHORIZED 5

更多配置选项

以下配置选项可用于配置库。它们包含在PubSubClient.h。

MQTT_ MAX_ PACKET_ SIZE

设置客户端将处理的最大数据包大小(以字节为单位)。收到的超过此大小的任何数据包都将被忽略。 默认值:128个字节

MQTT_ KEEPALIVE

设置客户端将使用的keepalive间隔(以秒为单位)。这用于在没有发送或接收其他数据包时维持连接。 默认值:15秒

MQTT_ VERSION

设置要使用的MQTT协议的版本。 默认值:MQTT 3.1.1

MQTT_ MAX_ TRANSFER_ SIZE

设置每次写入调用中传递给网络客户端的最大字节数。某些硬件限制了可以一次传递给他们的数据,例如 Arduino Wifi Shield。 默认值:undefined(在每次写入调用中传递完整数据包)

MQTT_ SOCKET_ TIMEOUT

设置从网络读取时的超时。这也适用于调用的超时connect。 默认值:15秒

订阅回调

如果客户端用于订阅主题,则必须在构造函数中提供回调函数。当新消息到达客户端时,将调用此函数。

回调函数具有以下签名:

void callback(const char[] topic, byte* payload, unsigned int length)

参数: topic - 消息到达的主题 (const char[])。 payload - 消息有效负载 (byte array)。 length - 消息有效负载的长度 (unsigned int)。

在内部,客户端对入站和出站消息使用相同的缓冲区。在回调函数返回之后,或者如果从回调函数中调用其中任何一个 publish 或之后 subscribe,将覆盖传递给函数的 topic 和 payload 值。如果超出此要求,应用程序应创建自己的值副本。

例程:发布与订阅

连接一个公共的 MQTT 服务器,每2秒发布一次 “hello world” 消息到主题 “outTopic”,客户端监听主题 “inTopic”,并判断负载内容来控制灯亮灭。

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

#include <WiFi.h>
#include <PubSubClient.h>

#define LED  15 

// Update these with values suitable for your network.
const char* ssid = "your_ssid";
const char* password = "your_password";
const char* mqtt_server = "broker.mqtt-dashboard.com";

WiFiClient espClient;
PubSubClient client(espClient);
long lastMsg = 0;
char msg[50];
int value = 0;

void setup_wifi() {

  delay(10);
  // We start by connecting to a WiFi network
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  randomSeed(micros());

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
}

void callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);
  }
  Serial.println();

  // Switch on the LED if an 0 was received as first character
  if ((char)payload[0] == '0') {
    digitalWrite(LED, LOW);   // Turn the LED off (Note that LOW is the voltage level
    // but actually the LED is on; this is because
    // it is active low on the ESP-01)
  } else {
    digitalWrite(LED, HIGH);  // Turn the LED on by making the voltage HIGH
  }

}

void reconnect() {
  // Loop until we're reconnected
  while (!client.connected()) {
    Serial.print("Attempting MQTT connection...");
    // Create a random client ID
    String clientId = "FireBeetleClient-";
    clientId += String(random(0xffff), HEX);
    // Attempt to connect
    if (client.connect(clientId.c_str())) {
      Serial.println("connected");
      // Once connected, publish an announcement...
      client.publish("outTopic", "hello world");
      // ... and resubscribe
      client.subscribe("inTopic");
    } else {
      Serial.print("failed, rc=");
      Serial.print(client.state());
      Serial.println(" try again in 5 seconds");
      // Wait 5 seconds before retrying
      delay(5000);
    }
  }
}

void setup() {
  pinMode(LED, OUTPUT);     // Initialize the BUILTIN_LED pin as an output
  Serial.begin(115200);
  setup_wifi();
  client.setServer(mqtt_server, 1883);
  client.setCallback(callback);
}

void loop() {

  if (!client.connected()) {
    reconnect();
  }
  client.loop();

  long now = millis();
  if (now - lastMsg > 2000) {
    lastMsg = now;
    ++value;
    snprintf (msg, 75, "hello world #%ld", value);
    Serial.print("Publish message: ");
    Serial.println(msg);
    client.publish("outTopic", msg);
  }
}

串口打印结果

Connecting to dfrobotOffice
........
WiFi connected
IP address: 
192.168.0.221
Attempting MQTT connection...connected
Publish message: hello world #1
Publish message: hello world #2
Message arrived [inTopic] 88
Publish message: hello world #3
Publish message: hello world #4

你也可以部署一个私有MQTT服务器。

腾讯云搭建 Mosquitto MQTT服务器 https://cloud.tencent.com/developer/article/1161563

也可以在本地计算机或者树莓派主板上部署MQTT服务器。例如在树莓派上部署 Home Assistant,它本身自带了Mosquitto MQTT代理软件。

树莓派安装 Home Assistant 教程 https://www.home-assistant.io/installation/raspberrypi

Home Assistant 使用案例 https://sspai.com/post/60414