Coping with the TCP TIME-WAIT state on busy Linux servers

TL;DR

Do not enable net.ipv4.tcp_tw_recycle—it doesn’t even exist anymore since Linux 4.12. Most of the time, TIME-WAIT sockets are harmless. Otherwise, jump to the summary for the recommended solutions.

The Linux kernel documentation is not very helpful about what net.ipv4.tcp_tw_recycle and net.ipv4.tcp_tw_reuse do. This lack of documentation opens the path to numerous tuning guides advising to set both these settings to 1 to reduce the number of entries in the TIME-WAIT state. However, as stated by the tcp(7) manual page, the net.ipv4.tcp_tw_recycle option is quite problematic for public-facing servers as it won’t handle connections from two different computers behind the same NAT device, which is a problem hard to detect and waiting to bite you:

Enable fast recycling of TIME-WAIT sockets. Enabling this option is not recommended since this causes problems when wrking with NAT (Network Address Translation).

I will provide here a more detailed explanation of how to properly handle the TIME-WAIT state. Also, keep in mind we are looking at the TCP stack of Linux. This is completely unrelated to Netfilter connection tracking which may be tweaked in other ways.1

继续阅读Coping with the TCP TIME-WAIT state on busy Linux servers

WireGuard 教程:WireGuard 的工作原理

本文翻译自:https://github.com/pirate/wireguard-docs

WireGuard 是由 Jason Donenfeld 等人用 C 语言编写的一个开源 VPN 协议,被视为下一代 VPN 协议,旨在解决许多困扰 IPSec/IKEv2OpenVPN 或 L2TP 等其他 VPN 协议的问题。它与 Tinc 和 MeshBird 等现代 VPN 产品有一些相似之处,即加密技术先进、配置简单。从 2020 年 1 月开始,它已经并入了 Linux 内核的 5.6 版本,这意味着大多数 Linux 发行版的用户将拥有一个开箱即用的 WireGuard。

无论你是想破墙而出,还是想在服务器之间组网,WireGuard 都不会让你失望,它就是组网的『乐高积木』,就像 ZFS 是构建文件系统的『乐高积木』一样。

继续阅读WireGuard 教程:WireGuard 的工作原理

UWB中TOF测距法的公式推导

UWB中TOF测距法的公式推导

UWB常用测距方法有两种:飞行时间测距法(TOF)和到达时间差法(TDOA)。这里说一下TOF。

TOF

飞行时间法(Time of Flight,TOF)是一种双向测距技术,它通过测量UWB信号在基站与标签之间往返的飞行时间来计算距离。根据数学关系,一点到已知点的距离为常数,那么这点一定在以已知点为圆心,以该常数为半径的圆上。有两个已知点,就有两个交点。以三个已知点和距离作三个圆,他们交于同一个点,该点就是标签的位置。

TOF定位方式需要基站和标签往返通信,因此就造成了TOF功耗大大提高,续航时间相对较短.

TOF又分为两种:单边双向测距和双边双向测距。

单边双向测距

单边双向测距(Single-sided Two-way Ranging: SS-TWR)是对单个往返消息时间上的简单测量,设备A主动发送数据到设备B,设备B返回数据响应设备A。如下图所示:

单边双向测距的流程是这样的:设备A(Device A)主动发送(TX)数据,同时记录发送时间戳,设备B(Device B)接收到之后记录接收时间戳;延时 $T_{reply}$ 之后,设备B发送数据,同时记录发送时间戳,设备A接收数据,同时记录接收时间戳。

所以可以拿到两个时间差数据,设备A的时间差 $T_{round}$ 和设备B的时间差 $T_{reply}$ ,最终得到无线信号的飞行时间 $T_{prop}$ 如下:

$ T_{prop} = \frac{1}{2}(T_{round}-T_{reply}) $

两个差值时间都是基于本地的时钟计算得到的,本地时钟误差可以抵消,但是不同设备之间会存在微小的时钟偏移,假设设备A和B的时钟偏移分别为 $e_A$ 和 $e_B$ ,则飞行时间测量值为:

$ \hat{T}_{prop} = \frac{1}{2}[T_{round}(1+e_A)-T_{reply}(1+e_B)] $

于是测距误差如下:

$ Error = \hat{T}_{prop} - T_{prop} = \frac{1}{2}(T_{round}\cdot e_A-T_{reply}\cdot e_B) = \frac{1}{2}T_{reply}(e_A-e_B) + T_{prop}\cdot e_A $

因为 $ T_{reply} >> T_{prop} $ , 所以可以忽略后一项,得到

$ Error = \hat{T}_{prop} - T_{prop} \approx \frac{1}{2}T_{reply}(e_A-e_B) $

由此可以看出,随着 $T_{reply} $ 和时钟偏移的增加,会增加飞行时间的误差,从而使得测距不准确。因此单边双向测距(SS-TWR)并不常用,但对于特定的应用,如果对于精度要求不是很高,但是需要更短的测距时间可以采用。注意 $ T_{reply} $ 不仅仅是设备B接收到发送的时间,也包括装载数据和发送数据耗费的时间(UWB除了支持定位之外,也可以传输数据,标准可以装载128字节,扩展模式可以装载1024字节数据)。

双边双向测距

双边双向测距(Double-sided Two-way Ranging)是单边双向测距的一种扩展测距方法,记录了两个往返的时间戳,最后得到飞行时间,虽然增加了响应的时间,但会降低测距误差。

双边双向测距分为两次测距,设备A主动发起第一次测距消息,设备B响应,当设备A收到数据之后,再返回数据,最终可以得到如下四个时间差:$ T_{round1} $ 、$ T_{reply1} $ 、$ T_{round2} $ 、$ T_{reply2} $ ,如下图所示:

双边双向测距飞行时间计算方法:

由单边双向测距方法可得

$ T_{prop} = \frac{1}{2}(T_{round1}-T_{reply1}) $

$ T_{prop} = \frac{1}{2}(T_{round2}-T_{reply2}) $

所以

$ \begin{flalign}T_{round1} \times T_{round2} = (2T_{prop}+T_{reply1})(2T_{prop}+T_{reply2}) = 4T_{prop}^2+2T_{prop}(T_{reply1}+T_{reply2})+T_{reply1}T_{reply2} \end{flalign}$

$ \begin{flalign} T_{round1} \times T_{round2} - T_{reply1}T_{reply2} = 4T_{prop}^2+2T_{prop}(T_{reply1}+T_{reply2}) \\ = T_{prop}(4T_{prop}+2T_{reply1}+2T_{reply2}) \\ = T_{prop}(T_{round1} + T_{round2} + T_{reply1} + T_{reply2}) \end{flalign}$

于是可以得到如下计算 $ T_{prop} $ 的公式:

$ T_{prop} = \frac{T_{round1} \times T_{round2} - T_{reply1} \times T_{reply2}}{T_{round1} + T_{round2} + T_{reply1} + T_{reply2}} $

以上测距的机制是非对称的测距方法,因为他们对于响应时间不要求是相同的。下面分析双边双向测距飞行时间的误差:

$ \begin{flalign} \hat{T}_{prop} = \frac{T_{round1}(1+e_A) \times T_{round2}(1+e_B) - T_{reply1}(1+e_B) \times T_{reply2}(1+e_A)}{T_{round1}(1+e_A) + T_{round2}(1+e_B) + T_{reply1}(1+e_B) + T_{reply2}(1+e_A)} \\ = \frac{(4T_{prop}^2+2T_{prop}(T_{reply1}+T_{reply2}))(1+e_A)(1+e_B)} {4T_{prop}+2(T_{reply1}+T_{reply2})+(2T_{prop}+T_{reply1}+T_{reply2})(e_A+e_B)}\\ =\frac{2(1+e_A)(1+e_B)}{(1+e_A)+(1+e_B)}T_{prop} \end{flalign}$

于是

$ T_{prop} = \frac{(1+e_A)+(1+e_B)}{2(1+e_A)(1+e_B)}\hat{T}_{prop} $

$ \begin{flalign} Error = \hat{T}_{prop} - T_{prop} = \left(1-\frac{(1+e_A)+(1+e_B)}{2(1+e_A)(1+e_B)}\right)\hat{T}_{prop} \\ = \frac{e_A+e_B+2e_A e_B}{2(1+e_A)(1+e_B)}\hat{T}_{prop} \end{flalign} $

因为 $ e_A <<1 $,$ e_B<<1$,略去高次项,可得

$ Error \approx \frac{e_A+e_B}{2}\hat{T}_{prop} $

由此可以看出,误差仅与钟漂和飞行时间有关。

假设一个使用场景:使用20ppm的晶体,UWB的工作距离范围为300m,则无线信号空中飞行时间大概为$1 \mu s$,误差为$ 20 \times 10^{-6} \times 1 \times 10^{-6} = 20 \times 10^{-12} = 20ps $时钟误差是在ps级别的,换算为距离之后仅为6mm。

注意:响应时间是不需要相等的,也就是 $ T_{reply1} $ 不一定要等于 $ T_{reply2} $ ,这样对于MCU系统的处理带来了很多便利。

若双边双向测距方法响应时间对称,也就是 $ T_{reply1} $ 和 $ T_{reply2} $ 相等,飞行时间计算方法如下:

$ T_{prop} = \frac{1}{4}(T_{round1}-T_{reply1}+T_{round2}-T_{reply2}) $

这种方法比较简单,只是需要一些时间戳做加减法,但其难点在于,怎么保证 $T_{reply1} $ 和 $ T_{reply2} $ 是相等的。

此种方法的误差分析如下:

$ \begin{flalign} \hat{T}_{prop} = \frac{1}{4}\left[T_{round1}(1+e_A)-T_{reply1}(1+e_B)+T_{round2}(1+e_B)-T_{reply2}(1+e_A)\right] \end{flalign}$

$ \begin{flalign} Error = \hat{T}_{prop} - T_{prop} = \frac{1}{4}\left[(T_{round1}-T_{reply2})e_A +(T_{round2}-T_{reply1})e_B\right] \\ =\frac{1}{4}\left[2(e_A+e_B)T_{prop} +(e_A-e_B)(T_{reply1}-T_{reply2})\right] \end{flalign} $

因为 $ T_{reply1}-T_{reply2} >> T_{prop} $ ,可忽略$ T_{prop} $项,从而得到

$ Error \approx \frac{1}{4}(e_A-e_B)(T_{reply1}-T_{reply2})$

可见此误差与响应时间差成正比。

参考链接


智能硬件Nvidia Jetson Nano B01

产品参数

GPU 128-core Maxwell
CPU Quad-core ARM A57 @ 1.43 GHz
内存 4 GB 64-bit LPDDR4 25.6 GB/s
存储 micro SD 卡 
视频编码 4K @ 30   |   4x 1080p @ 30   |   9x 720p @ 30
(H.264/H.265)
视频解码 4K @ 60   |   2x 4K @ 30   |   8x 1080p @ 30   |   18x 720p @ 30
(H.264/H.265)
摄像头 2x MIPI CSI-2 D-PHY lanes
联网 千兆以太网,M.2 Key E 接口外扩 (可外接: AC8265 双模网卡 )
显示 HDMI 和 DP 显示接口
USB 4x USB 3.0,USB 2.0 Micro-B
扩展接口 GPIO,I2C,I2S,SPI,UART
其他 260-pin 连接器

Jetson Nano系统安装

1、JetPack介绍

JetPack SDK包括最新的Linux驱动程序包(L4T),带有Linux操作系统和CUDA-X加速库,以及用于深度学习、计算机视觉、加速计算和多媒体的API。 它还包括用于主机和开发人员套件的示例、文档和开发人员工具,并支持更高级别的SDK,例如用于流式视频分析的DeepStream和用于机器人的Isaac。

2、JetPack 4.4

JetPack 目前最新版本是4.4,支持Vulkan 1.2、TensorRT 7.1.3 、cuDNN 8.0、CUDA 10.2 等。

3、下载和安装

  • 下载 Jetson Nano镜像,镜像中包含提供引导加载程序、Ubuntu18.04、必要的固件、NVIDIA驱动程序、示例文件系统等。
  • 使用 Etcher 或者 Raspberry Pi Imager 将镜像烧录到SD卡(建议至少32G)中。

设置VNC服务

1.执行更新

$ sudo apt-get update

2.安装vino服务端

这个vino服务端我使用的镜像文件是安装好了的,但是古早版的镜像文件可能没有,所以可以执行下代码看看是否有安装。

$ sudo apt-get install vino

3.开启VNC 服务

$ sudo ln -s ../vino-server.service /usr/lib/systemd/user/graphical-session.target.wants

4.配置VNC服务

$ gsettings set org.gnome.Vino prompt-enabled false

$ gsettings set org.gnome.Vino require-encryption false

设置开机自启动

1.创建VNC自动启动文件

创建文件夹,然后创建一个自动启动文件

$ mkdir -p ~/.config/autostart

$ sudo gedit ~/.config/autostart/vino-server.desktop

2.添加以下内容到vino-server.desktop文件中

[Desktop Entry]
 
Type=Application
 
Name=Vino VNC server
 
Exec=/usr/lib/vino/vino-server
 
NoDisplay=true

这个时候,虽说是自动启动了,但是只有进入桌面后才自动启动服务,所以需要取消登录密码,启动就进入桌面。

参考链接


关于HTTP请求走私的小记

0x00 写在前面

之前一次线上赛,遇到一道Web题,涉及了HTTP请求走私。由于之前未学习过,从而我展开了HTTP请求走私的学习之旅。

0x01 HTTP请求走私是什么

HTTP请求走私是一种干扰网站处理从一个或多个用户接收的HTTP请求序列的方式的技术。使攻击者可以绕过安全控制,未经授权访问敏感数据并直接危害其他应用程序用户。

0x02 为什么会产生HTTP请求走私

请求走私漏洞成因

前端服务器(CDN)和后端服务器接收数据不同步,引起对客户端传入的数据理解不一致,从而导致漏洞的产生。

大多数HTTP请求走私漏洞的出现是因为HTTP规范提供了两种不同的方法来指定请求的结束位置:Content-Length标头和Transfer-Encoding标头。
同时使用两种不同的方法时,Content-Length无效。当使用多个服务器时,对客户端传入的数据理解不一致时,就会出现有些服务器认为Content-Length的长度有效,有些以Transfer-Encoding有效。而一般情况下,反向代理服务器与后端的源站服务器之间,会重用TCP链接。这样超出的长度就会拼接到下一次请求进行请求,从而导致HTTP请求走私漏洞。

RFC2616规范

如果接收的消息同时包含传输编码头字段(Transfer-Encoding)和内容长度头(Content-Length)字段,则必须忽略后者。

由于规范默许可以使用Transfer-EncodingContent-Length处理请求,因此很少有服务器拒绝此类请求。每当我们找到一种方法,将Transfer-Encoding隐藏在服务端的一个chain中时,它将会回退到使用Content-Length去发送请求。

走私攻击实现

当向代理服务器发送一个比较模糊的HTTP请求时,由于两者服务器的实现方式不同,代理服务器可能认为这是一个HTTP请求,然后将其转发给了后端的源站服务器,但源站服务器经过解析处理后,只认为其中的一部分为正常请求,剩下的那一部分,就算是走私的请求,当该部分对正常用户的请求造成了影响之后,就实现了HTTP走私攻击。

扩展:为什么会出现多次请求

这与最为广泛的HTTP 1.1的协议特性——Keep-Alive&Pipeline有关。

HTTP1.0之前的协议设计中,客户端每进行一次HTTP请求,需要同服务器建立一个TCP链接。

而现代的Web页面是由多种资源组成的,要获取一个网页的内容,不仅要请求HTML文档,还有JS、CSS、图片等各种资源,如果按照之前的协议设计,就会导致HTTP服务器的负载开销增大。于是在HTTP1.1中,增加了Keep-AlivePipeline这两个特性。

Keep-Alive:在HTTP请求中增加一个特殊的请求头Connection: Keep-Alive,告诉服务器,接收完这次HTTP请求后,不要关闭TCP链接,后面对相同目标服务器的HTTP请求,重用这一个TCP链接。这样只需要进行一次TCP握手的过程,可以减少服务器的开销,节约资源,还能加快访问速度。这个特性在HTTP1.1中默认开启的。

Pipeline(http管线化):http管线化是一项实现了多个http请求但不需要等待响应就能够写进同一个socket的技术,仅有http1.1规范支持http管线化。在这里,客户端可以像流水线一样发送自己的HTTP请求,而不需要等待服务器的响应,服务器那边接收到请求后,需要遵循先入先出机制,将请求和响应严格对应起来,再将响应发送给客户端。

现在,浏览器默认不启用Pipeline的,但是一般的服务器都提供了对Pipleline的支持。

继续阅读关于HTTP请求走私的小记

Transfer-Encoding 的作用

通过HTTP传送数据时,有些时候并不能事先确定body的长度,因此无法得到Content-Length的值, 就不能在header中指定Content-Length了,造成的最直接的影响就是:接收方无法通过Content-Length得到报文体的长度, 那怎么判断发送方发送完毕了呢?HTTP 1.1协议在header中引入了Transfer-Encoding,当其值为chunked时, 表明采用chunked编码方式来进行报文体的传输

HTTP 1.1中有两个实体头(Entity-Header)直接与编码相关,分别为Content-Encoding和Transfer-Encoding.
先说Content-Encoding, 该头表示实体已经采用了的编码方式.Content-Encoding是请求URL对应实体(Entity)本身的一部分.比如请求URL为http://host/image.png.gz时,可能会得到的Content-Encoding为gzip.Content-Encoding的值是不区分大小写的,目前HTTP1.1标准中已包括的有gzip/compress/deflate/identity等.
与Content-Encoding头对应,HTTP请求中包含了一个Accept-Encoding头,该头用来说明用户代理(User-Agent,一般也就是浏览器)能接受哪些类型的编码. 如果HTTP请求中不存在该头,服务器可以认为用户代理能接受任何编码类型.

接下来重点描述Transfer-Encoding, 该头表示为了达到安全传输或者数据压缩等目的而对实体进行的编码. Transfer-Encoding与Content-Encoding的不同之处在于:
1, Transfer-Encoding只是在传输过程中才有的,并非请求URL对应实体的本身特性.
2, Transfer-Encoding是一个"跳到跳"头,而Content-Encoding是"端到端"头.
该头的用途举例如,请求URL为http://host/abc.txt,服务器发送数据时认为该文件可用gzip方式压缩以节省带宽,接收端看到Transfer-Encoding为gzip首先进行解码然后才能得到请求实体.
此外多个编码可能同时对同一实体使用,所以Transfer-Encoding头中编码顺序相当重要,它代表了解码的顺序过程.同样,Transfer-Encoding的值也是不区分大小写的,目前HTTP1.1标准中已包括的有gzip/compress/deflate/identity/chunked等.
Transfer-Encoding中有一类特定编码:chunked编码.该编码将实体分块传送并逐块标明长度,直到长度为0块表示传输结束, 这在实体长度未知时特别有用(比如由数据库动态产生的数据). HTTP1.1标准规定,只要使用了Transfer-Encoding的地方就必须使用chunked编码,并且chunked必须为最后一层编码.任何HTTP 1.1应用都必须能处理chunked编码.
与Transfer-Encoding对应的请求头为TE,它主要表示请求发起者愿意接收的Transfer-Encoding类型. 如果TE为空或者不存在,则表示唯一能接受的类型为chunked.
其他与Transfer-Encoding相关的头还包括Trailer,它与chunked编码相关,就不细述了.

顾名思义,Content-Length表示传输的实体长度,以字节为单位(在请求方法为HEAD时表示会要发送的长度,但并不实际发送.).Content-Length受Transfer-Encoding影响很大,只要Transfer-Encoding不为identity,则实际传输长度由编码中的chunked决定,Content-Length即使存在也被忽略.

关于HTTP Message Body的长度
在HTTP中有消息体(Message body)和实体(Entity body)之分,简单说来在没有Transfer-Encoding作用时,消息体就是实体,而应用了Transfer-Encoding后,消息体就是编码后的实体,如下:

    Message body = Transfer-Encoding encode(Entity body)
如何确定消息体的长度? HTTP 1.1标准给出了如下方法(按照优先级依次排列):
    1, 响应状态(Response Status)为1xx/204/304或者请求方法为HEAD时,消息体长度为0.
    2, 如果使用了非"identity"的Transfer-Encoding编码方式,则消息体长度由"chunked"编码决定,除非该消息以连接关闭为结束.
    3, 如果存在"Content-Length"实体头,则消息长度为该数值.
    3, 如果消息使用关闭连接方式代表消息体结束,则长度由关闭前收到的长度决定. 该条对HTTP Request包含的消息体不适用.

具体详细的 RFC 7230 说明如下:

3.3.3.  Message Body Length

   The length of a message body is determined by one of the following
   (in order of precedence):

   1.  Any response to a HEAD request and any response with a 1xx
       (Informational), 204 (No Content), or 304 (Not Modified) status
       code is always terminated by the first empty line after the
       header fields, regardless of the header fields present in the
       message, and thus cannot contain a message body.

   2.  Any 2xx (Successful) response to a CONNECT request implies that
       the connection will become a tunnel immediately after the empty
       line that concludes the header fields.  A client MUST ignore any
       Content-Length or Transfer-Encoding header fields received in
       such a message.

   3.  If a Transfer-Encoding header field is present and the chunked
       transfer coding (Section 4.1) is the final encoding, the message
       body length is determined by reading and decoding the chunked
       data until the transfer coding indicates the data is complete.

       If a Transfer-Encoding header field is present in a response and
       the chunked transfer coding is not the final encoding, the
       message body length is determined by reading the connection until
       it is closed by the server.  If a Transfer-Encoding header field
       is present in a request and the chunked transfer coding is not
       the final encoding, the message body length cannot be determined
       reliably; the server MUST respond with the 400 (Bad Request)
       status code and then close the connection.

       If a message is received with both a Transfer-Encoding and a
       Content-Length header field, the Transfer-Encoding overrides the
       Content-Length.  Such a message might indicate an attempt to
       perform request smuggling (Section 9.5) or response splitting
       (Section 9.4) and ought to be handled as an error.  A sender MUST
       remove the received Content-Length field prior to forwarding such
       a message downstream.

   4.  If a message is received without Transfer-Encoding and with
       either multiple Content-Length header fields having differing
       field-values or a single Content-Length header field having an
       invalid value, then the message framing is invalid and the
       recipient MUST treat it as an unrecoverable error.  If this is a
       request message, the server MUST respond with a 400 (Bad Request)
       status code and then close the connection.  If this is a response
       message received by a proxy, the proxy MUST close the connection
       to the server, discard the received response, and send a 502 (Bad
       Gateway) response to the client.  If this is a response message
       received by a user agent, the user agent MUST close the
       connection to the server and discard the received response.

   5.  If a valid Content-Length header field is present without
       Transfer-Encoding, its decimal value defines the expected message
       body length in octets.  If the sender closes the connection or
       the recipient times out before the indicated number of octets are
       received, the recipient MUST consider the message to be
       incomplete and close the connection.

   6.  If this is a request message and none of the above are true, then
       the message body length is zero (no message body is present).

   7.  Otherwise, this is a response message without a declared message
       body length, so the message body length is determined by the
       number of octets received prior to the server closing the
       connection.

参考链接


Transfer-Encoding 的作用

RS485 软件流控 及 数据完整性确保

首先 串口 的流控大家应该都有所了解,通常是硬件 CTS/RTS 或软件 XON/XOFF 这两种流控方式,然而因为 RS485 是总线形式,所以传统的方法都不再适用。

有人会觉得奇怪,貌似从来没有考虑过 RS485 流控的问题,没错,传统 RS485 都是一收一发,用不着考虑流控,然而这种一收一发的效率比较低,譬如在 IoT 火热的今天,如果用 RS485 来传输网络数据,那么传统的做法就很低效了。

然后,针对数据完整性确保的问题,很多同行都没有留意到一个细节问题,他们通常判断是否收到回复 OK 的数据包,如果没收到数据包就超时重发一次。 这种做法大多情况都没有问题,但是某些场景,譬如发送一个命令让滑轨左移 10mm, 滑轨成功接收命令并返回 OK, 然而主机因为干扰等各种问题没有收到滑轨的回复,那么重发命令就会导致滑轨错误左移 20mm. 当然你可以说目前用到的设备都是绝对位置控制,不会有影响,但万一哪天新做一个设备,到那时再改协议,难道就不考虑兼容自己以往的产品了吗?

当然还是有很多朋友有注意到这个问题,本文使用的解决方法原理上跟这些朋友也是相同的。

我接下来提出的方案最大的亮点是共用同一套机制,同时解决了流控、数据完整性确保、大数据分包等功能,而且比较高效和简单。

同样,最底层的协议我们依然使用 CDBUS, 因为它比较简单,又支持硬件增强(可以主动避让冲突,实现多主机、对等通讯、主动上报数据等功能),能最大程度体现出本文方法的性能优势。

你可能没有听过 CDBUS 这个名字,但你可能曾经或正在使用相似的协议,它的组成包含 3 个部分: - 3 个字节的头:「源地址,目标地址,用户数据长度」 - 0~255 字节的用户数据(因为数据长度用 1 个字节表示) - 2 个字节的 CRC 校验,涵盖整个数据包,校验算法同 ModBus.

数据包与数据包之间要有一定的空闲时间,来隔开不同的数据包,详细请参见 CDBUS 的协议定义: https://github.com/dukelec/cdbus_ip

譬如地址 0x00 为主机,0x01 为 1 号从机,那么主机发送两个字节数据 0x10 0x11 给 1 号从机的完整数据为:

[00 01 02 10 11 49 f0]

然后 1 号从机回覆单个 0x10 给主机:

[01 00 01 10 04 b8]

然而 CDBUS 只是最底层的协议,接下来我们要定义上述用户数据的格式,最简单常用的方式就是首字节为命令号,然后后面跟可选命令参数; 回覆数据第一个字节通常为状态,然后是返回的数据。

这种方式完善之后也有一个名字,叫 CDNET, 它的定义在:https://github.com/dukelec/cdnet
(本文的内容这个连接都有包含,但本文会更加通俗的讲解一下关键细节。)

CDNET 协议有 3 个级别,由首字节的高两位决定:

Bit7    Bit6    描述
0        x      Level 0: 上述最简单的形式
1        0      Level 1: 支持跨网、组播、流控等高阶功能
1        1      Level 2: 裸数据模式,支持大数据拆包,譬如传输 IPv4/v6 数据包

实际使用根据情况自由选择某一个或某几个来用就好。

Level 0 格式
请求
首字节:

位     描述
[7]    等于 0: Level 0
[6]    等于 0: 请求
[5:0]  dst_port, 范围 0~63

CDNET 的端口号可以看做类似电脑的 UDP 端口,也可以看做是一个命令号。

第二字节及其后:命令参数

回复
首字节:

位     描述
[7]    等于 0: Level 0
[6]    等于 1: 回复
[5]    1: [4:0] 存放用户数据;0: 不使用
[4:0]  不使用或用户数据,数据必须 ≤ 31

例如: 回复 [0x40, 0x0c] 和回复 [0x6c] 是相同的意思。

0x40: 'b0100_0000
0x0c: 'b0000_1100
----
0x6c: 'b0110_1100

首字节的用户数据(如果有)、第二个字节及其后:回复的状态 和/或 数据。

Level 1 格式
首字节:

位     描述
[7]    等于 1
[6]    等于 0
[5]    MULTI_NET (跨网)
[4]    MULTICAST (组播)
[3]    SEQUENCE (序列号)
[2:0]  PORT_SIZE (端口大小设置)

MULTI_NET & MULTICAST

MULTI_NET   MULTICAST   描述
    0          0         Local net: 本地网络,不追加数据
    0          1         Local net multicast: 追加 2 字节 [multicast-id] 组播号
    1          0         Cross net: 追加 4 字节: [src_net, src_mac, dst_net, dst_mac]
    1          1         Cross net multicast: 追加 4 字节: [src_net, src_mac, multicast-id]

这个与本文主题无关,就不展开了。

SEQUENCE
0: 无序列号;
1: 追加 1 字节序列号 SEQ_NUM, 这个是重点,稍后会主要说明。

PORT_SIZE:

Bit2   Bit1   Bit0   SRC_PORT        DST_PORT
  0     0      0     Default port     1 byte
  0     0      1     Default port     2 bytes
  0     1      0     1 byte           Default port
  0     1      1     2 bytes          Default port
  1     0      0     1 byte           1 byte
  1     0      1     1 byte           2 bytes
  1     1      0     2 bytes          1 byte
  1     1      1     2 bytes          2 bytes

注: - 默认端口通常定为 0xcdcd, 所以不用额外追加字节. - 追加的字节按顺序,先是 src_port 再是 dst_port.

Level 2 格式
首字节:

位       描述
[7]      等于 1
[6]      等于 1
[5:4]    FRAGMENT(大数据分包)
[3]      SEQUENCE(序列号)
[2:0]    User-defined flag

FRAGMENT:
Bit5   Bit4   DST_PORT
  0     0      Not fragment
  0     1      First fragment
  1     0      More fragment
  1     1      Last fragment

注: - 使用分包功能的时候必须同时选择使用 SEQUENCE. - 开始分包的时候 SEQ_NUM 不需要归零.

一般情况下,要求不高,使用最简单的 Level 0 格式就好了,如果命令比较多,那么就可以用 Level 1 格式,用不到的功能不用理会即可。

Level 1 没有大数据分包功能,因为通常 MCU 也用不到那么大的数据包,即使是烧录代码这种要传大数据的功能,也是可以在命令内部定义地址和数据长度的,譬如我的 STM32 总线代码升级的命令定义:

// flash memory manipulation, port 11:
// erase: 0xff, addr_32, len_32 | return [] on success
// read: 0x00, addr_32, len_8 | return [data]
// write: 0x01, addr_32 + [data] | return [] on success

而 Level 2 譬如可以用来传 IPv4/v6 数据包,那么就不得不加入拆包的功能了。因为 Level 1 和 Level 2 的序列号部分是一样的,所以接下来就混在一起讲了。

CDBUS 协议将前 0~9 保留专用,10 及其后的用户可以随便用,保留的部分目前也就用了 4 个,而且也不是强制的,用户愿意实现就实现,不用或者自己想怎么用就怎么用也没问题。 上篇文章说了端口或命令 0x01 是用来查询设备信息的,命令 0x03 是用来设置地址的,还详细说了如何使用这两个端口来实现地址自动分配,剩下两个端口其中 0x02 是用来设置波特率的, 对于本文最关键的端口 0x00 是用于流控、完整性确保、大数据拆包的了,其定义如下:

Port 0
配合 Level 1 和 2 头中的 SEQUENCE 字段使用。
命令启用 SEQUENCE 后追加的对应字节 SEQ_NUM[6:0] 的低 7 位会每次自动加 1.
而 SEQ_NUM 的第 7 位用来指示接收方是否要报告状态。
Port 0 本身的命令不可以启用 SEQUENCE.

Port 0 命令定义:

主动读目标的 SEQ_NUM:
Write []
Return: [SEQ_NUM] (如果没有记录 bit 7 置 1)

主动设置目标的 SEQ_NUM:
Write [0x00, SEQ_NUM]
Return: []

目标回复 SEQ_NUM:
Write [SEQ_NUM]
Return: None

实际示例:
(-> 和 <- 是端口层的数据流, >> 和 << 是 CDNET 数据包层面的数据流,不含最低层的 CDBUS 的部分)

  设备 A                         设备 B       描述

  [0x00, 0x00]          ->      Port0        首次通讯设置对方的 SEQ_NUM
  Default port          <-      []           设置成功返回
  [0x88, 0x00, ...]     >>                   开始发送数据
  [0x88, 0x01, ...]     >>
  [0x88, 0x82, ...]     >>                   这次的数据标注了需要回复 SEQ_NUM @2
  [0x88, 0x03, ...]     >>
  [0x88, 0x04, ...]     >>
  Port0                 <-      [0x03]       回复 SEQ_NUM @2 (每成功接收一个包计数加 1, 回复当前计数 0x03)
  [0x88, 0x85, ...]     >>                   标注了需要回复 SEQ_NUM @5
  Port0                 <-      [0x06]       回复 SEQ_NUM @5

效率提升的重点就在这里,我们可以自行选择多久回复一次,而不是每次都要回复状态,如果最后一次数据包没有标注需要回复,那么会引发超时,然后主动读一次目标的 SEQ_NUM 以做同步。 之所以引发超时,是因为所有发出的数据包都不能立刻释放,要等确认对方收到才会释放,以防需要出错重传。 因为有 SEQ_NUM 号,所以即使同一个命令重复发送,对方也会只执行一次。

流控的功能也包含在内,譬如发送方时刻只允许最多 6 个数据包没有释放,那么等收到回复,释放掉 3 个,再发送 3 个数据包,这样可以最大化的利用总线带宽。 而且万一有多方发送数据至同一个节点,发送方也可以因频繁超时,来动态降低最大允许 pending 的数据包数量。

再来说大包拆分,也是很简单,拆分包有 3 个标记,分别是起始、继续、结束,譬如一个大包拆开了 4 个小包,且如果当前 SEQ_NUM 为 23,那么这四个小包的 SEQ_NUM 和标记对应关系就是:

23: 拆分启始标志
24: 继续
25: 继续
26: 结束

这样接收方也就很容易的把四个小包还原成原始的大包,万一出问题,也只是重新传输错掉或丢掉的包(及其后的包)。

为了简便,对于 CDNET 协议,并不是丢一个包就只重传一个包,其后传的包也需要重传,因为接收方只是简单判断序号,不对便拒绝接收,这么做是为了保持简单,毕竟错包、丢包的概率很低。

最后,想说的是,这篇文章的内容都是经过实践检验的,我有用来传输摄像头视频,DEMO 可以在这篇介绍文章中看到:https://github.com/dukelec/cdbus_doc/blob/master/intro_zh.md

协议的实现部分代码也是开源的,就是上面的 CDNET 连接,另外有一些使用 CDNET 的示例代码,譬如这个 STM32F103 的步进电机控制器:https://github.com/dukelec/stepper_motor_controller

当然,这些代码、库我也会进一步优化改善。

参考链接


RS485 软件流控 及 数据完整性确保

完整教程:设计一款小巧但强大的传感器

由IEEE制定的新型单对以太网(SPE)或10BASE-T1L物理层标准,为传输设备运行状况信息实施状态监测(CbM)应用提供了新的连接解决方案。SPE提供共享电源和高带宽数据架构,可通过低成本双线电缆在超过1000米的距离实现10 Mbps数据和电源的共享。

ADI公司设计了业界首款10BASE-T1L MAC-PHY(ADIN1110),这是一款集成MAC的单对以太网收发器。ADIN1110使用简单的SPI总线与嵌入式微控制器通信,从而可降低传感器的功耗并减少固件开发时间。

在本文中,您将了解如何设计一款体型小巧但功能强大的传感器,如图1所示。本文将介绍:

● 如何设计小型共享数据和电源通信接口

● 如何为传感器设计超低噪声电源

● 微控制器和软件架构选择

● 选择合适的MEMS振动传感器

● 集成数字硬件设计和机械外壳

● 电脑上的数据采集UI示例

继续阅读完整教程:设计一款小巧但强大的传感器

【玩转ESP32】ESP32串口使用

1、ESP32串口

ESP32芯片有三个UART控制器(UART0, UART1UART2),其中UART0GPIO3用于U0RXDGPIO1用于U0TXD)用作下载、调试串口,引脚不可改变;

UART1UART2的引脚是可以设置的。 UART1默认引脚是GPIO9用作U1RXDGPIO10用作U1TXD,但是这两个引脚也是用于外接flash的,因此在使用UART1的时候需要设置其他引脚;

UART2默认引脚是GPIO16用作U2RXDGPIO17用作U2TXD

2、API

components/driver/include/driver/uart.h中可以查看api;

在examples/peripherals/uart中也可以参考官方的各种串口例程。

2.1、安装uart驱动
esp_err_t uart_driver_install(uart_port_t uart_num, int rx_buffer_size, int tx_buffer_size, int queue_size, QueueHandle_t* uart_queue, int intr_alloc_flags);

这里要注意参数:uart_queue属于freertos里面的队列句柄,在这里表示用于指示来自串口底层中断的队列消息。

2.2、uart参数配置
esp_err_t uart_param_config(uart_port_t uart_num, const uart_config_t *uart_config);
2.3、接收阈值设置
esp_err_t uart_set_rx_full_threshold(uart_port_t uart_num, int threshold);
2.4、串口引脚设置
esp_err_t uart_set_pin(uart_port_t uart_num, int tx_io_num, int rx_io_num, int rts_io_num, int cts_io_num);
2.5、从接收缓冲区读取数据
int uart_read_bytes(uart_port_t uart_num, uint8_t* buf, uint32_t length, TickType_t ticks_to_wait);
2.6、数据写入发送缓冲区
int uart_write_bytes(uart_port_t uart_num, const char* src, size_t size);

3、代码实现

3.1、参数定义
#define EX_UART_NUM UART_NUM_1  //串口1

#define TXD_PIN    (GPIO_NUM_17) //txd使用gpio17
#define RXD_PIN    (GPIO_NUM_16) //rxd使用gpio16

#define BUF_SIZE (128)   //缓冲区定义
static QueueHandle_t uart_queue; //队列句柄
static uint8_t  uartbuf[BUF_SIZE];
3.2、串口初始化
void uart_comm_init(void)
{
    /* Configure parameters of an UART driver,
     * communication pins and install the driver */
    uart_config_t uart_config = {
        .baud_rate = 115200,
        .data_bits = UART_DATA_8_BITS,
        .parity = UART_PARITY_DISABLE,
        .stop_bits = UART_STOP_BITS_1,
        .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
        .source_clk = UART_SCLK_APB,
    };
    //Install UART driver, and get the queue.
    uart_driver_install(EX_UART_NUM, BUF_SIZE * 2, BUF_SIZE * 2, 20, &uart_queue, 0);
    uart_param_config(EX_UART_NUM, &uart_config);
    uart_set_rx_full_threshold(EX_UART_NUM,126);

    //Set UART pins (using UART0 default pins ie no changes.)
    uart_set_pin(EX_UART_NUM, TXD_PIN, RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);

    //Create a task to handler UART event from ISR
    xTaskCreate(uart_event_task, "uart_event_task", 2048, NULL, 12, NULL); //创建串口任务
}
3.3、串口任务函数
static void uart_event_task(void *pvParameters)
{
    uart_event_t event;
    for(;;) {
        //阻塞接收串口队列,
        //这个队列在底层发送,用户只需在应用层接收即可
        if(xQueueReceive(uart_queue, (void * )&event, (portTickType)portMAX_DELAY)) {
            switch(event.type) {//各种串口事件
                case UART_DATA:
                    ESP_LOGI(TAG, "[UART DATA]: %d", event.size);
                    uart_read_bytes(EX_UART_NUM, uartbuf, event.size, portMAX_DELAY); //阻塞接收
                    ESP_LOGI(TAG, "[DATA EVT]:");
                    uart_write_bytes(EX_UART_NUM, uartbuf, event.size);//原样发送
                    break;
                //Event of HW FIFO overflow detected
                case UART_FIFO_OVF: //硬件fifo溢出
                    ESP_LOGI(TAG, "hw fifo overflow");
                    // If fifo overflow happened, you should consider adding flow control for your application.
                    // The ISR has already reset the rx FIFO,
                    // As an example, we directly flush the rx buffer here in order to read more data.
                    uart_flush_input(EX_UART_NUM);
                    xQueueReset(uart_queue);
                    break;
                //Event of UART ring buffer full
                case UART_BUFFER_FULL: //环形缓冲区满
                    ESP_LOGI(TAG, "ring buffer full");
                    // If buffer full happened, you should consider encreasing your buffer size
                    // As an example, we directly flush the rx buffer here in order to read more data.
                    uart_flush_input(EX_UART_NUM);
                    xQueueReset(uart_queue);
                    break;
                //Event of UART RX break detected
                case UART_BREAK:
                    ESP_LOGI(TAG, "uart rx break");
                    break;
                //Event of UART parity check error
                case UART_PARITY_ERR:
                    ESP_LOGI(TAG, "uart parity error");
                    break;
                //Event of UART frame error
                case UART_FRAME_ERR:
                    ESP_LOGI(TAG, "uart frame error");
                    break;
                //UART_PATTERN_DET
                case UART_PATTERN_DET:
                    break;
                //Others
                default:
                    ESP_LOGI(TAG, "uart event type: %d", event.type);
                    break;
            }
        }
    }
    vTaskDelete(NULL);
}

4、使用总结

  1. 初始化串口参数,(队列指针、缓冲区、引脚等);
  2. 任务中阻塞等待串口队列 。

如何实现 ESP32 固件的 OTA 在线升级更新

1、背景

在实际产品开发过程中,在线升级可以远程解决产品软件开发引入的问题,更好地满足用户需求。

2、OTA 简介

OTA(空中)更新是使用 Wi-Fi 连接而不是串行端口将固件加载到 ESP 模块的过程。

2.1、ESP32 的 OTA 升级有三种方式:
  • Arduino IDE:主要用于软件开发阶段,实现不接线固件烧写
  • Web Browser:通过 Web 浏览器手动提供应用程序更新模块
  • HTTP Server:自动使用http服务器 - 针对产品应用

在三种升级情况下,必须通过串行端口完成第一个固件上传。

OTA 进程没有强加的安全性,需要确保开发人员只能从合法/受信任的来源获得更新。更新完成后,模块将重新启动,并执行新的代码。开发人员应确保在模块上运行的应用程序以安全的方式关闭并重新启动。

2.2、保密性 Security

模块必须以无线方式显示,以便通过新的草图进行更新。 这使得模块被强行入侵并加载了其他代码。 为了减少被黑客入侵的可能性,请考虑使用密码保护您的上传,选择某些OTA端口等。

可以提高安全性的 ArduinoOTA 库接口:

void setPort(uint16_t port);
void setHostname(const char* hostname);
void setPassword(const char* password);
void onStart(OTA_CALLBACK(fn));
void onEnd(OTA_CALLBACK(fn));
void onProgress(OTA_CALLBACK_PROGRESS(fn));
void onError(OTA_CALLBACK_ERROR (fn));

已经内置了某些保护功能,不需要开发人员进行任何其他编码。ArduinoOTA和espota.py使用Digest-MD5来验证上传。使用MD5校验和,在ESP端验证传输数据的完整性。

2.3、OTA 升级策略 - 针对 HTTP

ESP32 连接 HTTP 服务器,发送请求 Get 升级固件;每次读取1KB固件数据,写入Flash。

ESP32 SPI Flash 内有与升级相关的(至少)四个分区:OTA data、Factory App、OTA_0、OTA_1。其中 FactoryApp 内存有出厂时的默认固件。

首次进行 OTA 升级时,OTA Demo 向 OTA_0 分区烧录目标固件,并在烧录完成后,更新 OTA data 分区数据并重启。

系统重启时获取 OTA data 分区数据进行计算,决定此后加载 OTA_0 分区的固件执行(而不是默认的 Factory App 分区内的固件),从而实现升级。

同理,若某次升级后 ESP32 已经在执行 OTA_0 内的固件,此时再升级时 OTA Demo 就会向 OTA_1 分区写入目标固件。再次启动后,执行 OTA_1 分区实现升级。以此类推,升级的目标固件始终在 OTA_0、OTA_1 两个分区之间交互烧录,不会影响到出厂时的 Factory App 固件。

3、OTA 实例解析

3.1、Arduino IDE方案固件更新

从 Arduino IDE 无线上传模块适用于以下典型场景:

在固件开发过程中,通过串行加载更快的替代方案 - 用于更新少量模块,只有模块在与 Arduino IDE 的计算机相同的网络上可用。

参考实例:

#include <WiFi.h>
#include <ESPmDNS.h>
#include <WiFiUdp.h>
#include <ArduinoOTA.h>
 
const char* ssid = "..........";
const char* password = "..........";
 
void setup() {
  Serial.begin(115200);
  Serial.println("Booting");
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.waitForConnectResult() != WL_CONNECTED) {
    Serial.println("Connection Failed! Rebooting...");
    delay(5000);
    ESP.restart();
  }
 
  // Port defaults to 3232
  // ArduinoOTA.setPort(3232);
 
  // Hostname defaults to esp3232-[MAC]
  // ArduinoOTA.setHostname("myesp32");
 
  // No authentication by default
  // ArduinoOTA.setPassword("admin");
 
  // Password can be set with it's md5 value as well
  // MD5(admin) = 21232f297a57a5a743894a0e4a801fc3
  // ArduinoOTA.setPasswordHash("21232f297a57a5a743894a0e4a801fc3");
 
  ArduinoOTA
    .onStart([]() {
      String type;
      if (ArduinoOTA.getCommand() == U_FLASH)
        type = "sketch";
      else // U_SPIFFS
        type = "filesystem";
 
      // NOTE: if updating SPIFFS this would be the place to unmount SPIFFS using SPIFFS.end()
      Serial.println("Start updating " + type);
    })
    .onEnd([]() {
      Serial.println("\nEnd");
    })
    .onProgress([](unsigned int progress, unsigned int total) {
      Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
    })
    .onError([](ota_error_t error) {
      Serial.printf("Error[%u]: ", error);
      if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
      else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
      else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
      else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
      else if (error == OTA_END_ERROR) Serial.println("End Failed");
    });
 
  ArduinoOTA.begin();
 
  Serial.println("Ready");
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
}
 
void loop() {
  ArduinoOTA.handle();
}
3.2、Web Browser 方案固件更新

该方案使用场景:

直接从 Arduino IDE 加载是不方便或不可能的

用户无法从外部更新服务器公开 OTA 的模块

在设置更新服务器不可行时,将部署后的更新提供给少量模块

参考实例:

#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <ESPmDNS.h>
#include <Update.h>
 
const char* host = "esp32";
const char* ssid = "xxx";
const char* password = "xxxx";
 
WebServer server(80);
 
/*
 * Login page
 */
 
const char* loginIndex = 
 "<form name='loginForm'>"
    "<table width='20%' bgcolor='A09F9F' align='center'>"
        "<tr>"
            "<td colspan=2>"
                "<center><font size=4><b>ESP32 Login Page</b></font></center>"
                "<br>"
            "</td>"
            "<br>"
            "<br>"
        "</tr>"
        "<td>Username:</td>"
        "<td><input type='text' size=25 name='userid'><br></td>"
        "</tr>"
        "<br>"
        "<br>"
        "<tr>"
            "<td>Password:</td>"
            "<td><input type='Password' size=25 name='pwd'><br></td>"
            "<br>"
            "<br>"
        "</tr>"
        "<tr>"
            "<td><input type='submit' onclick='check(this.form)' value='Login'></td>"
        "</tr>"
    "</table>"
"</form>"
"<script>"
    "function check(form)"
    "{"
    "if(form.userid.value=='admin' && form.pwd.value=='admin')"
    "{"
    "window.open('/serverIndex')"
    "}"
    "else"
    "{"
    " alert('Error Password or Username')/*displays error message*/"
    "}"
    "}"
"</script>";
 
/*
 * Server Index Page
 */
 
const char* serverIndex = 
"<script src='https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js'></script>"
"<form method='POST' action='#' enctype='multipart/form-data' id='upload_form'>"
   "<input type='file' name='update'>"
        "<input type='submit' value='Update'>"
    "</form>"
 "<div id='prg'>progress: 0%</div>"
 "<script>"
  "$('form').submit(function(e){"
  "e.preventDefault();"
  "var form = $('#upload_form')[0];"
  "var data = new FormData(form);"
  " $.ajax({"
  "url: '/update',"
  "type: 'POST',"
  "data: data,"
  "contentType: false,"
  "processData:false,"
  "xhr: function() {"
  "var xhr = new window.XMLHttpRequest();"
  "xhr.upload.addEventListener('progress', function(evt) {"
  "if (evt.lengthComputable) {"
  "var per = evt.loaded / evt.total;"
  "$('#prg').html('progress: ' + Math.round(per*100) + '%');"
  "}"
  "}, false);"
  "return xhr;"
  "},"
  "success:function(d, s) {"
  "console.log('success!')" 
 "},"
 "error: function (a, b, c) {"
 "}"
 "});"
 "});"
 "</script>";
 
/*
 * setup function
 */
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());
 
  /*use mdns for host name resolution*/
  if (!MDNS.begin(host)) { //http://esp32.local
    Serial.println("Error setting up MDNS responder!");
    while (1) {
      delay(1000);
    }
  }
  Serial.println("mDNS responder started");
  /*return index page which is stored in serverIndex */
  server.on("/", HTTP_GET, []() {
    server.sendHeader("Connection", "close");
    server.send(200, "text/html", loginIndex);
  });
  server.on("/serverIndex", HTTP_GET, []() {
    server.sendHeader("Connection", "close");
    server.send(200, "text/html", serverIndex);
  });
  /*handling uploading firmware file */
  server.on("/update", HTTP_POST, []() {
    server.sendHeader("Connection", "close");
    server.send(200, "text/plain", (Update.hasError()) ? "FAIL" : "OK");
    ESP.restart();
  }, []() {
    HTTPUpload& upload = server.upload();
    if (upload.status == UPLOAD_FILE_START) {
      Serial.printf("Update: %s\n", upload.filename.c_str());
      if (!Update.begin(UPDATE_SIZE_UNKNOWN)) { //start with max available size
        Update.printError(Serial);
      }
    } else if (upload.status == UPLOAD_FILE_WRITE) {
      /* flashing firmware to ESP*/
      if (Update.write(upload.buf, upload.currentSize) != upload.currentSize) {
        Update.printError(Serial);
      }
    } else if (upload.status == UPLOAD_FILE_END) {
      if (Update.end(true)) { //true to set the size to the current progress
        Serial.printf("Update Success: %u\nRebooting...\n", upload.totalSize);
      } else {
        Update.printError(Serial);
      }
    }
  });
  server.begin();
}
 
void loop(void) {
  server.handleClient();
  delay(1);
}
3.3、HTTP 服务器实现更新

ESPhttpUpdate 类可以检查更新并从 HTTP Web 服务器下载二进制文件。可以从网络或 Internet 上的每个 IP 或域名地址下载更新,主要应用于远程服务器更新升级。

参考实例:

/**
   AWS S3 OTA Update
   Date: 14th June 2017
   Author: Arvind Ravulavaru <https://github.com/arvindr21>
   Purpose: Perform an OTA update from a bin located in Amazon S3 (HTTP Only)
 
   Upload:
   Step 1 : Download the sample bin file from the examples folder
   Step 2 : Upload it to your Amazon S3 account, in a bucket of your choice
   Step 3 : Once uploaded, inside S3, select the bin file >> More (button on top of the file list) >> Make Public
   Step 4 : You S3 URL => http://bucket-name.s3.ap-south-1.amazonaws.com/sketch-name.ino.bin
   Step 5 : Build the above URL and fire it either in your browser or curl it `curl -I -v http://bucket-name.ap-south-1.amazonaws.com/sketch-name.ino.bin` to validate the same
   Step 6:  Plug in your SSID, Password, S3 Host and Bin file below
 
   Build & upload
   Step 1 : Menu > Sketch > Export Compiled Library. The bin file will be saved in the sketch folder (Menu > Sketch > Show Sketch folder)
   Step 2 : Upload bin to S3 and continue the above process
 
   // Check the bottom of this sketch for sample serial monitor log, during and after successful OTA Update
*/
 
#include <WiFi.h>
#include <Update.h>
 
WiFiClient client;
 
// Variables to validate
// response from S3
int contentLength = 0;
bool isValidContentType = false;
 
// Your SSID and PSWD that the chip needs
// to connect to
const char* SSID = "YOUR-SSID";
const char* PSWD = "YOUR-SSID-PSWD";
 
// S3 Bucket Config
String host = "bucket-name.s3.ap-south-1.amazonaws.com"; // Host => bucket-name.s3.region.amazonaws.com
int port = 80; // Non https. For HTTPS 443. As of today, HTTPS doesn't work.
String bin = "/sketch-name.ino.bin"; // bin file name with a slash in front.
 
// Utility to extract header value from headers
String getHeaderValue(String header, String headerName) {
  return header.substring(strlen(headerName.c_str()));
}
 
// OTA Logic 
void execOTA() {
  Serial.println("Connecting to: " + String(host));
  // Connect to S3
  if (client.connect(host.c_str(), port)) {
    // Connection Succeed.
    // Fecthing the bin
    Serial.println("Fetching Bin: " + String(bin));
 
    // Get the contents of the bin file
    client.print(String("GET ") + bin + " HTTP/1.1\r\n" +
                 "Host: " + host + "\r\n" +
                 "Cache-Control: no-cache\r\n" +
                 "Connection: close\r\n\r\n");
 
    // Check what is being sent
    //    Serial.print(String("GET ") + bin + " HTTP/1.1\r\n" +
    //                 "Host: " + host + "\r\n" +
    //                 "Cache-Control: no-cache\r\n" +
    //                 "Connection: close\r\n\r\n");
 
    unsigned long timeout = millis();
    while (client.available() == 0) {
      if (millis() - timeout > 5000) {
        Serial.println("Client Timeout !");
        client.stop();
        return;
      }
    }
    // Once the response is available,
    // check stuff
 
    /*
       Response Structure
        HTTP/1.1 200 OK
        x-amz-id-2: NVKxnU1aIQMmpGKhSwpCBh8y2JPbak18QLIfE+OiUDOos+7UftZKjtCFqrwsGOZRN5Zee0jpTd0=
        x-amz-request-id: 2D56B47560B764EC
        Date: Wed, 14 Jun 2017 03:33:59 GMT
        Last-Modified: Fri, 02 Jun 2017 14:50:11 GMT
        ETag: "d2afebbaaebc38cd669ce36727152af9"
        Accept-Ranges: bytes
        Content-Type: application/octet-stream
        Content-Length: 357280
        Server: AmazonS3
                                   
        {{BIN FILE CONTENTS}}
 
    */
    while (client.available()) {
      // read line till /n
      String line = client.readStringUntil('\n');
      // remove space, to check if the line is end of headers
      line.trim();
 
      // if the the line is empty,
      // this is end of headers
      // break the while and feed the
      // remaining `client` to the
      // Update.writeStream();
      if (!line.length()) {
        //headers ended
        break; // and get the OTA started
      }
 
      // Check if the HTTP Response is 200
      // else break and Exit Update
      if (line.startsWith("HTTP/1.1")) {
        if (line.indexOf("200") < 0) {
          Serial.println("Got a non 200 status code from server. Exiting OTA Update.");
          break;
        }
      }
 
      // extract headers here
      // Start with content length
      if (line.startsWith("Content-Length: ")) {
        contentLength = atoi((getHeaderValue(line, "Content-Length: ")).c_str());
        Serial.println("Got " + String(contentLength) + " bytes from server");
      }
 
      // Next, the content type
      if (line.startsWith("Content-Type: ")) {
        String contentType = getHeaderValue(line, "Content-Type: ");
        Serial.println("Got " + contentType + " payload.");
        if (contentType == "application/octet-stream") {
          isValidContentType = true;
        }
      }
    }
  } else {
    // Connect to S3 failed
    // May be try?
    // Probably a choppy network?
    Serial.println("Connection to " + String(host) + " failed. Please check your setup");
    // retry??
    // execOTA();
  }
 
  // Check what is the contentLength and if content type is `application/octet-stream`
  Serial.println("contentLength : " + String(contentLength) + ", isValidContentType : " + String(isValidContentType));
 
  // check contentLength and content type
  if (contentLength && isValidContentType) {
    // Check if there is enough to OTA Update
    bool canBegin = Update.begin(contentLength);
 
    // If yes, begin
    if (canBegin) {
      Serial.println("Begin OTA. This may take 2 - 5 mins to complete. Things might be quite for a while.. Patience!");
      // No activity would appear on the Serial monitor
      // So be patient. This may take 2 - 5mins to complete
      size_t written = Update.writeStream(client);
 
      if (written == contentLength) {
        Serial.println("Written : " + String(written) + " successfully");
      } else {
        Serial.println("Written only : " + String(written) + "/" + String(contentLength) + ". Retry?" );
        // retry??
        // execOTA();
      }
 
      if (Update.end()) {
        Serial.println("OTA done!");
        if (Update.isFinished()) {
          Serial.println("Update successfully completed. Rebooting.");
          ESP.restart();
        } else {
          Serial.println("Update not finished? Something went wrong!");
        }
      } else {
        Serial.println("Error Occurred. Error #: " + String(Update.getError()));
      }
    } else {
      // not enough space to begin OTA
      // Understand the partitions and
      // space availability
      Serial.println("Not enough space to begin OTA");
      client.flush();
    }
  } else {
    Serial.println("There was no content in the response");
    client.flush();
  }
}
 
void setup() {
  //Begin Serial
  Serial.begin(115200);
  delay(10);
 
  Serial.println("Connecting to " + String(SSID));
 
  // Connect to provided SSID and PSWD
  WiFi.begin(SSID, PSWD);
 
  // Wait for connection to establish
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print("."); // Keep the serial monitor lit!
    delay(500);
  }
 
  // Connection Succeed
  Serial.println("");
  Serial.println("Connected to " + String(SSID));
 
  // Execute OTA Update
  execOTA();
}
 
void loop() {
  // chill
}
 
/*
 * Serial Monitor log for this sketch
 * 
 * If the OTA succeeded, it would load the preference sketch, with a small modification. i.e.
 * Print `OTA Update succeeded!! This is an example sketch : Preferences > StartCounter`
 * And then keeps on restarting every 10 seconds, updating the preferences
 * 
 * 
      rst:0x10 (RTCWDT_RTC_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
      configsip: 0, SPIWP:0x00
      clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
      mode:DIO, clock div:1
      load:0x3fff0008,len:8
      load:0x3fff0010,len:160
      load:0x40078000,len:10632
      load:0x40080000,len:252
      entry 0x40080034
      Connecting to SSID
      ......
      Connected to SSID
      Connecting to: bucket-name.s3.ap-south-1.amazonaws.com
      Fetching Bin: /StartCounter.ino.bin
      Got application/octet-stream payload.
      Got 357280 bytes from server
      contentLength : 357280, isValidContentType : 1
      Begin OTA. This may take 2 - 5 mins to complete. Things might be quite for a while.. Patience!
      Written : 357280 successfully
      OTA done!
      Update successfully completed. Rebooting.
      ets Jun  8 2016 00:22:57
      
      rst:0x10 (RTCWDT_RTC_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
      configsip: 0, SPIWP:0x00
      clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
      mode:DIO, clock div:1
      load:0x3fff0008,len:8
      load:0x3fff0010,len:160
      load:0x40078000,len:10632
      load:0x40080000,len:252
      entry 0x40080034
      
      OTA Update succeeded!! This is an example sketch : Preferences > StartCounter
      Current counter value: 1
      Restarting in 10 seconds...
      E (102534) wifi: esp_wifi_stop 802 wifi is not init
      ets Jun  8 2016 00:22:57
      
      rst:0x10 (RTCWDT_RTC_RESET),boot:0x13 (SPI_FAST_FLASH_BOOT)
      configsip: 0, SPIWP:0x00
      clk_drv:0x00,q_drv:0x00,d_drv:0x00,cs0_drv:0x00,hd_drv:0x00,wp_drv:0x00
      mode:DIO, clock div:1
      load:0x3fff0008,len:8
      load:0x3fff0010,len:160
      load:0x40078000,len:10632
      load:0x40080000,len:252
      entry 0x40080034
      
      OTA Update succeeded!! This is an example sketch : Preferences > StartCounter
      Current counter value: 2
      Restarting in 10 seconds...
 
      ....
 * 
 */

refer:

参考链接