diff --git a/README.md b/README.md index b6a4073..741b459 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,7 @@ > 在线文字教程:[FreeRTOS多任务与任务管理.md](./docs/03.FreeRTOS基础/3.2-FreeRTOS多任务与任务管理/FreeRTOS多任务与任务管理.md) > 在线视频教程:[FreeRTOS任务管理详解] -> 教程配套代码:[FreeRTOS多任务示例代码](#) +> 教程配套代码:[FreeRTOS多任务示例代码](#) ## 3.3 FreeRTOS任务看门狗 @@ -380,7 +380,7 @@ 本节介绍ESP32的Wi-Fi模式,包括STA模式和AP模式,以及如何进行基本的Wi-Fi操作,如连接网络和设置路由器等。 -> 在线文字教程:[Wi-Fi模式与基础操作](./docs/06. Wi-Fi功能与相关协议/6.1- Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/ Wi-Fi模式与基础操作.md) +> 在线文字教程:[Wi-Fi模式与基础操作](./docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/Wi-Fi模式与基础操作.md) > 在线视频教程:[Wi-Fi模式开发详解] > 教程配套代码:[Wi-Fi模式示例代码](#) @@ -398,7 +398,7 @@ 本节讲解TCP协议的原理及在ESP32中的实现,包含连接、数据传输及断开连接的操作步骤。 -> 在线文字教程:[TCP协议应用](./docs/06. Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.1-TCP协议/TCP协议应用.md) +> 在线文字教程:[TCP协议应用](./docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.1-TCP协议/TCP协议应用.md) > 在线视频教程:[TCP协议开发详解] > 教程配套代码:[TCP协议示例代码](#) @@ -406,7 +406,7 @@ 本节介绍UDP协议的工作原理及在ESP32上的应用,包含数据发送与接收的实现方法。 -> 在线文字教程:[UDP协议应用](./docs/06. Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.2-UDP协议/UDP协议应用.md) +> 在线文字教程:[UDP协议应用](./docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.2-UDP协议/UDP协议应用.md) > 在线视频教程:[UDP协议开发详解] > 教程配套代码:[UDP协议示例代码](#) @@ -414,7 +414,8 @@ 本节介绍ESP32如何实现HTTP协议,包括创建HTTP客户端和服务器的步骤,以及常见的应用场景。 -> 在线文字教程:[HTTP协议应用](./docs/06. Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3- HTTP协议/HTTP协议应用.md) +> 在线文字教程-1:[HTTP协议(服务端).md](./docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/HTTP协议(服务端).md) +> 在线文字教程-2:[HTTP协议(客户端).md](./docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(客户端)/HTTP协议(客户端).md) > 在线视频教程:[HTTP协议开发详解] > 教程配套代码:[HTTP协议示例代码](#) @@ -422,7 +423,7 @@ 本节讲解WebSocket协议在ESP32中的应用,如何通过WebSocket实现实时双向通信。 -> 在线文字教程:[WebSocket协议应用](./docs/06. Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.4-WebSocket协议/WebSocket协议应用.md) +> 在线文字教程:[WebSocket协议应用](./docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.4-WebSocket协议/WebSocket协议.md) > 在线视频教程:[WebSocket协议开发详解] > 教程配套代码:[WebSocket协议示例代码](#) @@ -430,7 +431,7 @@ 本节介绍ESP32如何实现MQTT协议,包括客户端连接、订阅和发布消息的基本操作。 -> 在线文字教程:[MQTT协议应用](./docs/06. Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.5-MQTT协议/MQTT协议应用.md) +> 在线文字教程:[MQTT协议应用](./docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.5-MQTT协议/MQTT协议应用.md) > 在线视频教程:[MQTT协议开发详解] > 教程配套代码:[MQTT协议示例代码](#) @@ -438,7 +439,7 @@ 本节介绍ESP32的ESP-NOW协议,讲解其在无需Wi-Fi路由器的情况下实现设备间直接通信的应用场景。 -> 在线文字教程:[ESP-NOW协议应用](./docs/06. Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.6-ESP-NOW协议/ESP-NOW协议应用.md) +> 在线文字教程:[ESP-NOW协议应用](./docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.6-ESP-NOW协议/ESP-NOW协议应用.md) > 在线视频教程:[ESP-NOW协议开发详解] > 教程配套代码:[ESP-NOW协议示例代码](#) diff --git a/docs/04.外设学习/4.1-基础外设/4.1.1-GPIO/GPIO入门.md b/docs/04.外设学习/4.1-基础外设/4.1.1-GPIO/GPIO入门.md index f53097b..b8e95df 100644 --- a/docs/04.外设学习/4.1-基础外设/4.1.1-GPIO/GPIO入门.md +++ b/docs/04.外设学习/4.1-基础外设/4.1.1-GPIO/GPIO入门.md @@ -1,17 +1,4 @@ -### 说明: - -1. 本文档由DuRuofu撰写,由DuRuofu负责解释及执行。 -2. 本文档介绍GPIO的基本使用。 - -### 修订历史: - -| 文档名称 | 版本 | 作者 | 时间 | 备注 | -| ---- | ---- | ------------ | ------ | -| ESP32外设-GPIO入门 |v1.0.0| DuRuofu | 2024-02-29 | 首次建立 | - -
- # ESP32外设-GPIO入门 ## 一、GPIO介绍 diff --git a/docs/04.外设学习/4.1-基础外设/4.1.2-UART/UART串口通信.md b/docs/04.外设学习/4.1-基础外设/4.1.2-UART/UART串口通信.md index 8501d2b..5c18548 100644 --- a/docs/04.外设学习/4.1-基础外设/4.1.2-UART/UART串口通信.md +++ b/docs/04.外设学习/4.1-基础外设/4.1.2-UART/UART串口通信.md @@ -1,16 +1,4 @@ -### 说明: - -1. 本文档由DuRuofu撰写,由DuRuofu负责解释及执行。 -2. 本文档介绍ESP32外设-UART的基本使用 - -### 修订历史: - -| 文档名称 | 版本 | 作者 | 时间 | 备注 | -| ---- | ---- | ------------ | ------ | -| ESP32外设-UART入门 |v1.0.0| DuRuofu | 2024-03-03 | 首次建立 | - -
# ESP32外设-UART入门 diff --git a/docs/04.外设学习/4.1-基础外设/4.1.3-定时器/定时器.md b/docs/04.外设学习/4.1-基础外设/4.1.3-定时器/定时器.md index 7833730..0d83ab5 100644 --- a/docs/04.外设学习/4.1-基础外设/4.1.3-定时器/定时器.md +++ b/docs/04.外设学习/4.1-基础外设/4.1.3-定时器/定时器.md @@ -1,16 +1,4 @@ -### 说明: - -1. 本文档由DuRuofu撰写,由DuRuofu负责解释及执行。 -2. 本文档介绍ESP32硬件定时器的使用。(其他型号也可参考) - -### 修订历史: - -| 文档名称 | 版本 | 作者 | 时间 | 备注 | -| ---- | ---- | ------------ | ------ | -| ESP32外设-硬件定时器入门 |v1.0.0| DuRuofu | 2024-03-04 | 首次建立 | - -
# ESP32外设-硬件定时器入门 diff --git a/docs/04.外设学习/4.1-基础外设/4.1.4-ADC/ADC模数转换.md b/docs/04.外设学习/4.1-基础外设/4.1.4-ADC/ADC模数转换.md index 55aeb0f..0e68195 100644 --- a/docs/04.外设学习/4.1-基础外设/4.1.4-ADC/ADC模数转换.md +++ b/docs/04.外设学习/4.1-基础外设/4.1.4-ADC/ADC模数转换.md @@ -1,16 +1,4 @@ -### 说明: - -1. 本文档由DuRuofu撰写,由DuRuofu负责解释及执行。 -2. 本文档介绍ADC外设的使用,个人记录使用,如有侵权请联系删除。 - -### 修订历史: - -| 文档名称 | 版本 | 作者 | 时间 | 备注 | -| ---- | ---- | ------------ | ------ | -| ESP32外设-ADC入门 |v1.0.0| DuRuofu | 2024-03-03 | 首次建立 | - -
# ESP32外设-ADC入门 diff --git a/docs/04.外设学习/4.1-基础外设/4.1.5-DAC/DAC数模转换.md b/docs/04.外设学习/4.1-基础外设/4.1.5-DAC/DAC数模转换.md index a214fe9..73ee457 100644 --- a/docs/04.外设学习/4.1-基础外设/4.1.5-DAC/DAC数模转换.md +++ b/docs/04.外设学习/4.1-基础外设/4.1.5-DAC/DAC数模转换.md @@ -1,14 +1,4 @@ -### 说明: - -1. 本文档由DuRuofu撰写,由DuRuofu负责解释及执行。 -2. 本文档介绍ESP32外设,数模转换器的使用。 - -### 修订历史: - -| 文档名称 | 版本 | 作者 | 时间 | 备注 | -| ------------- | ------ | ------- | ---------- | ---- | -| ESP32外设-DAC入门 | v1.0.0 | DuRuofu | 2024-03-05 | 首次建立 |
diff --git a/docs/04.外设学习/4.1-基础外设/4.1.6-LED_PWM/LED_PWM控制.md b/docs/04.外设学习/4.1-基础外设/4.1.6-LED_PWM/LED_PWM控制.md index c1ea28e..679a006 100644 --- a/docs/04.外设学习/4.1-基础外设/4.1.6-LED_PWM/LED_PWM控制.md +++ b/docs/04.外设学习/4.1-基础外设/4.1.6-LED_PWM/LED_PWM控制.md @@ -1,16 +1,4 @@ -### 说明: - -1. 本文档由DuRuofu撰写,由DuRuofu负责解释及执行。 - -### 修订历史: - -| 文档名称 | 版本 | 作者 | 时间 | 备注 | -| ---------------- | ------ | ------- | ---------- | ---- | -| ESP32外设-LEDPWM入门 | v1.0.0 | DuRuofu | 2024-03-05 | 首次建立 | - -
- # ESP32外设-LEDPWM入门 ## 一、介绍: diff --git a/docs/04.外设学习/4.1-基础外设/4.1.7-I2C/I2C通信.md b/docs/04.外设学习/4.1-基础外设/4.1.7-I2C/I2C通信.md index 2203ab7..bfd1514 100644 --- a/docs/04.外设学习/4.1-基础外设/4.1.7-I2C/I2C通信.md +++ b/docs/04.外设学习/4.1-基础外设/4.1.7-I2C/I2C通信.md @@ -1,17 +1,4 @@ -### 说明: - -1. 本文档由DuRuofu撰写,由DuRuofu负责解释及执行。 -2. 本文档介绍ESP32 I2C外设(主机模式)的基本使用。 - -### 修订历史: - -| 文档名称 | 版本 | 作者 | 时间 | 备注 | -| ---- | ---- | ------------ | ------ | -| ESP32外设-I2C入门 |v1.0.0| DuRuofu | 2024-03-11 | 首次建立 | - -
- # ESP32外设-I2C入门 ## 一、介绍 diff --git a/docs/04.外设学习/4.1-基础外设/4.1.9-I2S/I2S入门.md b/docs/04.外设学习/4.1-基础外设/4.1.9-I2S/I2S入门.md index 6bf71e8..35803b1 100644 --- a/docs/04.外设学习/4.1-基础外设/4.1.9-I2S/I2S入门.md +++ b/docs/04.外设学习/4.1-基础外设/4.1.9-I2S/I2S入门.md @@ -1,17 +1,4 @@ -### 说明: - -1. 本文档由DuRuofu撰写,由DuRuofu负责解释及执行。 -2. 本文档记录ESP-IDF外设:I2S的使用。 - -### 修订历史: - -| 文档名称 | 版本 | 作者 | 时间 | 备注 | -| ------------- | ------ | ------- | ---------- | ---- | -| ESP32外设-I2S入门 | v1.0.0 | DuRuofu | 2024-04-11 | 首次建立 | - -
- # ESP32外设-I2S入门 ## 一、概念 diff --git a/docs/04.外设学习/4.2-存储外设/4.2.1-分区表/分区表.md b/docs/04.外设学习/4.2-存储外设/4.2.1-分区表/分区表.md index 7c7c4cb..9281d6c 100644 --- a/docs/04.外设学习/4.2-存储外设/4.2.1-分区表/分区表.md +++ b/docs/04.外设学习/4.2-存储外设/4.2.1-分区表/分区表.md @@ -1,16 +1,4 @@ -### 说明: - -1. 本文档由DuRuofu撰写,由DuRuofu负责解释及执行。 -2. 本文档主要介绍ESP32flash分区表的相关知识。 - -### 修订历史: - -| 文档名称 | 版本 | 作者 | 时间 | 备注 | -| ------------- | ------ | ------- | ---------- | ---- | -| ESP32存储-分区表入门 | v1.0.0 | DuRuofu | 2024-03-05 | 首次建立 | - -
# ESP32存储-分区表入门 diff --git a/docs/04.外设学习/4.2-存储外设/4.2.2-非易失性存储/非易失性存储.md b/docs/04.外设学习/4.2-存储外设/4.2.2-非易失性存储/非易失性存储.md index e8a2eef..f9e3e16 100644 --- a/docs/04.外设学习/4.2-存储外设/4.2.2-非易失性存储/非易失性存储.md +++ b/docs/04.外设学习/4.2-存储外设/4.2.2-非易失性存储/非易失性存储.md @@ -1,14 +1,4 @@ -### 说明: - -1. 本文档由DuRuofu撰写,由DuRuofu负责解释及执行。 -2. 本文档将详细介绍ESP32 NVS 常用的一些概念。 - -### 修订历史: - -| 文档名称 | 版本 | 作者 | 时间 | 备注 | -| -------------------- | ------ | ------- | ---------- | ---- | -| ESP32存储-非易失性存储 (NVS) | v1.0.0 | DuRuofu | 2024-03-06 | 首次建立 |
diff --git a/docs/04.外设学习/4.2-存储外设/4.2.3-FAT文件系统/FAT文件系统.md b/docs/04.外设学习/4.2-存储外设/4.2.3-FAT文件系统/FAT文件系统.md index 3e7ca88..8076c13 100644 --- a/docs/04.外设学习/4.2-存储外设/4.2.3-FAT文件系统/FAT文件系统.md +++ b/docs/04.外设学习/4.2-存储外设/4.2.3-FAT文件系统/FAT文件系统.md @@ -1,15 +1,4 @@ -### 说明: - -1. 本文档由DuRuofu撰写,由DuRuofu负责解释及执行。 -2. 本文档主要介绍ESP32的FAT (虚拟文件系统) - -### 修订历史: - -| 文档名称 | 版本 | 作者 | 时间 | 备注 | -| ---------------- | ------ | ------- | ---------- | ---- | -| ESP32存储-FAT 文件系统 | v1.0.0 | DuRuofu | 2024-03-06 | 首次建立 | -
# ESP32存储-VFS虚拟文件系统 diff --git a/docs/04.外设学习/4.2-存储外设/4.2.4-SPIFFS文件系统/SPIFFS文件系统.md b/docs/04.外设学习/4.2-存储外设/4.2.4-SPIFFS文件系统/SPIFFS文件系统.md index 9fead2c..fedf155 100644 --- a/docs/04.外设学习/4.2-存储外设/4.2.4-SPIFFS文件系统/SPIFFS文件系统.md +++ b/docs/04.外设学习/4.2-存储外设/4.2.4-SPIFFS文件系统/SPIFFS文件系统.md @@ -1,15 +1,4 @@ -### 说明: - -1. 本文档由DuRuofu撰写,由DuRuofu负责解释及执行。 -2. 本文档主要介绍,ESP32 flash SPIFFS文件系统的使用. - -### 修订历史: - -| 文档名称 | 版本 | 作者 | 时间 | 备注 | -| ------------------ | ------ | ------- | ---------- | ---- | -| ESP32存储-SPIFFS文件系统 | v1.0.0 | DuRuofu | 2024-03-06 | 首次建立 | -
# ESP32存储-SPIFFS文件系统 diff --git a/docs/04.外设学习/4.2-存储外设/4.2.5-SD卡驱动/SD卡驱动.md b/docs/04.外设学习/4.2-存储外设/4.2.5-SD卡驱动/SD卡驱动.md index 4a565c8..1618820 100644 --- a/docs/04.外设学习/4.2-存储外设/4.2.5-SD卡驱动/SD卡驱动.md +++ b/docs/04.外设学习/4.2-存储外设/4.2.5-SD卡驱动/SD卡驱动.md @@ -1,17 +1,4 @@ -### 说明: - -1. 本文档由DuRuofu撰写,由DuRuofu负责解释及执行。 -2. 本文档记录使用ESP-ID的FSD/SDIO/MMC 驱动来使用SD卡。 - -### 修订历史: - -| 文档名称 | 版本 | 作者 | 时间 | 备注 | -| ---------------------- | ------ | ------- | ---------- | ---- | -| ESP32存储-SD、SDIO、MMC 驱动 | v1.0.0 | DuRuofu | 2024-04-11 | 首次建立 | - -
- # ESP32存储-SD、SDIO、MMC 驱动 ## 一、基本概念 diff --git a/docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/Wi-Fi模式与基础操作.md b/docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/Wi-Fi模式与基础操作.md index e69de29..18383ac 100644 --- a/docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/Wi-Fi模式与基础操作.md +++ b/docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/Wi-Fi模式与基础操作.md @@ -0,0 +1,645 @@ + +# ESP32网络入门-WIFI连接 + +## 一、介绍 + +在开始使用WIFI之前,我们需要掌握一些基本的概念和前置知识: + +>最基本的一点:Wi-Fi是物理层和数据链路层的东西,Wi-Fi取代的是以太网的网线和交换机上的口,通过无线电波来收发信息。换句话说,这里说的WIFI暂时不涉及网络层协议。 + +### 1.1 ESP32事件机制 + +> 如果对FreeRTOS的多线程没有任何了解,请先了解一下,可以参考:[线程是什么](https://www.bilibili.com/video/BV1au411E7K1?p=7) + +关于事件循环可以参考下面的链接、大致了解即可。 + +1. [ESP32事件循环](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-reference/system/esp_event.html) +2. [Event Loop 大白话版](https://www.bilibili.com/video/BV1FD4y1j79J/?spm_id_from=333.788&vd_source=ef5a0ab0106372751602034cdd9ab98e) + +### 1.2 ESP32 WIFI的STA和AP模式 + +#### 1.2.1 AP + +AP(Access Point)也就是无线接入点,是一个无线网络的创建者,是网络的中心节点。一般家庭或办公室使用的无线路由器就是一个AP。 + +![](attachments/20240313184111.png) + +#### 1.2.2 STA + +站点(STA,Station)就是每一个连接到无线网络中的终端(如笔记本电脑、PDA及其它可以联网的用户设备)都可称为一个站点。 + +![](attachments/20240313184132.png) + + +## 二、使用 + +>参考:[ESP-IDF:Wi-Fi 驱动程序](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-guides/wifi.html) + +ESP-IDFWiFi库提供的功能如下: + +- 支持仅 station 模式、仅 AP 模式、station/AP 共存模式 +- 支持使用 IEEE 802.11b、IEEE 802.11g、IEEE 802.11n 和 API 配置协议模式 +- 支持 WPA/WPA2/WPA3/WPA2-企业版/WPA3-企业版/WAPI/WPS 和 DPP +- 支持 AMSDU、AMPDU、HT40、QoS 以及其它主要功能 +- 支持乐鑫专属协议,可实现 **1 km** 数据通信量 +- 空中数据传输最高可达 20 MBit/s TCP 吞吐量和 30 MBit/s UDP 吞吐量 +- 支持快速扫描和全信道扫描 +- 支持获取信道状态信息 + +### 2.1 WIFI AP模式 + +>官方示例程序位于: +> +`Espressif\frameworks\esp-idf-v4.4.3\examples\wifi\getting_started\softAP` + +配置流程如下: + +- `nvs_flash_init`,初始化默认 NVS 分区。 +- `esp_netif_init`,初始化底层TCP/IP堆栈(创建一个 LwIP 核心任务,并初始化 LwIP 相关工作。) +- `esp_event_loop_create_default`,创建默认事件循环。 +- `esp_netif_create_default_wifi_ap`,使用默认WiFi AP配置创建esp_netif对象,将netif连接到WiFi并注册默认WiFi处理程序。 +- `esp_wifi_init`,为 WiFi 驱动初始化 WiFi 分配资源,如 WiFi 控制结构、RX/TX 缓冲区、WiFi NVS 结构等,这个 WiFi 也启动 WiFi 任务。必须先调用此API,然后才能调用所有其他WiFi API +- `esp_event_handler_instance_register`,监听WIFI_EVENTWiFi 任意事件,触发事件后,进入回调函数 +- `esp_wifi_set_mode`,设置WiFi工作模式为station、soft-AP或station+soft-AP,默认模式为soft-AP模式。本程序设置为AP +- `esp_wifi_set_config`,设置 ESP32 STA 或 AP 的配置 +- `esp_wifi_start`,根据配置,启动WiFi + +工作流程如下图所示:(看不懂可以忽略,毕竟先用起来慢慢就学会了,也可以看[Michael_ee](https://space.bilibili.com/1338335828)老师的教程:[WIFI热点工作流程](https://www.bilibili.com/video/BV1ye4y1r7XK/?spm_id_from=333.788&vd_source=ef5a0ab0106372751602034cdd9ab98e)) + +![](attachments/20240313190734.png) + +主要的流程分为下面几个部分(图来自官方教程): + +![](attachments/20240313191730.png) + +#### 2.1.1 Wi-Fi准备阶段(图上没有) + +这个阶段我们需要初始化NVS,因为WiFi库内部是依赖这个东西的,NVS的相关知识可以看这篇博客:[ESP32存储-3.VFS虚拟文件系统](https://www.duruofu.top/2024/03/06/4.%E7%A1%AC%E4%BB%B6%E7%9B%B8%E5%85%B3/MCU/ESP32/04.ESP32%E5%AD%98%E5%82%A8%E5%99%A8%E5%85%A5%E9%97%A8/4.3-ESP32%E5%AD%98%E5%82%A8-VFS%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F/ESP32%E5%AD%98%E5%82%A8-VFS%E8%99%9A%E6%8B%9F%E6%96%87%E4%BB%B6%E7%B3%BB%E7%BB%9F%E5%85%A5%E9%97%A8/) + +下面直接贴出代码: +```c +// Initialize NVS +esp_err_t ret = nvs_flash_init(); +if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); +} +ESP_ERROR_CHECK( ret ); +``` + +#### 2.1.2 Wi-Fi 初始化阶段 + +这个阶段主要有下面几个步骤: + +- 主任务通过调用函数 [`esp_netif_init()`](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-reference/network/esp_netif.html#_CPPv414esp_netif_initv "esp_netif_init") 创建一个 LwIP 核心任务,并初始化 LwIP 相关工作。 +- 主任务通过调用函数 [`esp_event_loop_create()`](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-reference/system/esp_event.html#_CPPv421esp_event_loop_createPK21esp_event_loop_args_tP23esp_event_loop_handle_t "esp_event_loop_create") 创建一个系统事件任务,并初始化应用程序事件的回调函数。在此情况下,该回调函数唯一的动作就是将事件中继到应用程序任务中。 +- 主任务通过调用函数 [`esp_netif_create_default_wifi_ap()`](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-reference/network/esp_netif.html#_CPPv432esp_netif_create_default_wifi_apv "esp_netif_create_default_wifi_ap") 或 [`esp_netif_create_default_wifi_sta()`](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-reference/network/esp_netif.html#_CPPv433esp_netif_create_default_wifi_stav "esp_netif_create_default_wifi_sta") 创建有 TCP/IP 堆栈的默认网络接口实例绑定 station 或 AP。 +- 主任务通过调用函数 [`esp_wifi_init()`](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-reference/network/esp_wifi.html#_CPPv413esp_wifi_initPK18wifi_init_config_t "esp_wifi_init") 创建 Wi-Fi 驱动程序任务,并初始化 Wi-Fi 驱动程序。 +- 主任务通过调用 OS API 创建应用程序任务。 + +代码: + +```c +ESP_ERROR_CHECK(esp_netif_init()); + +ESP_ERROR_CHECK(esp_event_loop_create_default()); + +// *esp_netif_ap 可以用来修改AP设置 +esp_netif_t *esp_netif_ap = esp_netif_create_default_wifi_ap(); + +/*Initialize WiFi */ +wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); +// WIFI_INIT_CONFIG_DEFAULT 是一个默认配置的宏 + +ESP_ERROR_CHECK(esp_wifi_init(&cfg)); +``` + +到这里就完成了wifi初始化。 + +#### 2.1.3 Wi-Fi 配置阶段 + +Wi-Fi 驱动程序初始化成功后,可以进入到配置阶段。 + +通过函数`esp_wifi_set_mode`可以设置WiFi工作模式为station、soft-AP或station+soft-AP。 + +通过函数`esp_wifi_set_config`,设置 ESP32 STA 或 AP 的具体配置(参数很多,可以去官方文档仔细看看,下面只配置几个基本参数)。 + +```c +// 设置为AP模式 + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP)); + +// AP详细配置 +wifi_config_t wifi_ap_config = { + .ap = { + .ssid = ESP_WIFI_AP_SSID, // WIFI名称 + .ssid_len = strlen(ESP_WIFI_AP_SSID), // 名称长度 + .channel = 1, // WIFI信道 + .password = ESP_WIFI_AP_PASSWD, // WiFi密码 + .max_connection = 5, // 最大连接数,默认值是 10 + .authmode = WIFI_AUTH_WPA2_PSK, // WiFi认证方式 + }, +}; +ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_ap_config)); +``` + +这里只配配置了基本参数。 +#### 2.1.4 Wi-Fi 启动阶段 + + 使用`esp_wifi_start`,根据配置,启动WiFi + +```c +/* Start WiFi */ +ESP_ERROR_CHECK(esp_wifi_start()); +``` + +这样就完成了一个基本的WIFI AP。 + +但是这样仅仅是最基本的程序,当有外部STA设备接入和断开我们如何在程序里得知呢?这就需要使用之前创建的默认事件循环, + +#### 2.1.5 事件循环 + +调用 [`esp_event_handler_instance_register`](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-reference/system/esp_event.html#_CPPv431esp_event_handler_register_with23esp_event_loop_handle_t16esp_event_base_t7int32_t19esp_event_handler_tPv "esp_event_handler_register_with") 将事件处理函数注册到默认的事件循环中。它与 `esp_event_handler_instance_register_with `函数的功能相同,唯一的区别是将处理器注册到默认事件循环中。 + +```c + +esp_err_t esp_event_handler_instance_register(esp_event_base_t event_base, + int32_t event_id, + esp_event_handler_t event_handler, + void *event_handler_arg, + esp_event_handler_instance_t *instance); +``` + +参数如下: +- 参数 event_base 是要为其注册处理程序的事件的基本ID。 +- 参数 event_id 是要注册处理程序的事件的ID。 +- 参数 event_handler 是当事件被分发时调用的处理函数。 +- 参数 event_handler_arg 是传递给处理函数的除事件数据以外的数据。 +- 参数 instance 是与注册的事件处理器和数据相关的事件处理器实例对象 + +可以在espidf文件里看到相关事件描述: + +![](attachments/20240314165844.png) + + +我们可以在默认事件循环创建后,添加事件处理: + +```c +void WIFI_CallBack(void *event_handler_arg,esp_event_base_t event_base,int32_t event_id,void *event_data) +{ + // 连接事件 + if(event_base == IP_EVENT && event_id == IP_EVENT_AP_STAIPASSIGNED) + { + // 解析数据 + ip_event_ap_staipassigned_t* event_info = (ip_event_ap_staipassigned_t *)event_data; + ESP_LOGW("WIFI_AP", "设备已连接 MAC:"MACSTR"", MAC2STR(event_info->mac)); + } + + // 断开连接事件 + if(event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STADISCONNECTED) + { + wifi_event_ap_stadisconnected_t* event_info = (wifi_event_ap_stadisconnected_t*)event_data; + ESP_LOGW("WIFI_AP", "设备已断开 MAC:"MACSTR"", MAC2STR(event_info->mac)); + } +} + +void app_main(void) +{ + // ...... + + // 初始化默认事件循环 + ESP_ERROR_CHECK(esp_event_loop_create_default()); + // 注册设备连接事件回调 + esp_event_handler_instance_register(IP_EVENT, IP_EVENT_AP_STAIPASSIGNED, WIFI_CallBack, NULL, NULL); + // 注册设备断开连接设备回调 + esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_AP_STADISCONNECTED, WIFI_CallBack, NULL, NULL); + + + // ...... +} + +``` + +我们在这里注册了IP_EVENT_AP_STAIPASSIGNED事件,当STA设备连接和断开后就会调用WIFI_CallBack回调函数,打印连接设备的MAC信息。 + +### 2.2 WIFI STA + +>官方示例代码位于(与热点流程相似,但是细节有所不同): +`Espressif\frameworks\esp-idf-v4.4.3\examples\wifi\getting_started\station + +前面几步配配置方式基本相同 + +下面是官方的程序宏观流程: + +![](attachments/20240314220947.png) + +#### 2.2.1 Wi-Fi准备阶段(与上文AP相同) + +初始化NVS: + +```c +// Initialize NVS +esp_err_t ret = nvs_flash_init(); +if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); +} +ESP_ERROR_CHECK( ret ); +``` + +#### 2.2.2 Wi-Fi初始化阶段 + +这里与AP的配置不同了,我们需要配置设备为STA模式, + +这里唯一的区别是把`esp_netif_create_default_wifi_ap()`修改为` esp_netif_create_default_wifi_sta()`配置为STA模式 + +```c +ESP_ERROR_CHECK(esp_netif_init()); + +ESP_ERROR_CHECK(esp_event_loop_create_default()); + +// *esp_netif_ap 可以用来修改AP设置 +esp_netif_t *esp_netif_sta = esp_netif_create_default_wifi_sta(); + +/*Initialize WiFi */ +wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); +// WIFI_INIT_CONFIG_DEFAULT 是一个默认配置的宏 + +ESP_ERROR_CHECK(esp_wifi_init(&cfg)); +``` + +#### 2.2.3 Wi-Fi配置阶段 + +这里与AP模式类似,但是配置结构体里的内容有差异 + +``` c +............... +#define ESP_WIFI_STA_SSID "duruofu_win10" +#define ESP_WIFI_STA_PASSWD "1234567890" + +................. + +// 设置为STA模式 +ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + +// STA详细配置 +wifi_config_t sta_config = { + .sta = { + .ssid = ESP_WIFI_STA_SSID, + .password = ESP_WIFI_STA_PASSWD, + .bssid_set = false, + }, +} ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &sta_config)); + +``` + +这里的配置信息顾名思义就是要连接的wifi名称和密码(当然还有更多参数,可以参考ESP-IDF参数说明)。 + +#### 2.2.4 Wi-Fi 启动阶段 + +和AP模式不同,这里多了一个`esp_wifi_connect()`用于连接wifi。 + +```c +//----------------启动阶段------------------- +ESP_ERROR_CHECK(esp_wifi_start()); +ESP_ERROR_CHECK(esp_wifi_connect()); +``` + +#### 2.2.5 事件循环 + +同样的,我们可以像前面一样添加事件回调函数 + +这里监听了启动事件,连接失败事件,连接成功事件,代码很简单就不展开解释了。 + +```c + +void WIFI_CallBack(void *event_handler_arg,esp_event_base_t event_base,int32_t event_id,void *event_data) +{ + static uint8_t connect_count = 0; + // WIFI 启动成功 + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_START"); + ESP_ERROR_CHECK(esp_wifi_connect()); + } + // WIFI 连接失败 + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_DISCONNECTED"); + connect_count++; + if (connect_count < 6) + { + ESP_ERROR_CHECK(esp_wifi_connect()); + } + else{ + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_DISCONNECTED 10 times"); + } + } + // WIFI 连接成功(获取到了IP) + if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_GOT_IP"); + ip_event_got_ip_t *info = (ip_event_got_ip_t *)event_data; + ESP_LOGI("WIFI_EVENT", "got ip:" IPSTR "", IP2STR(&info->ip_info.ip)); + } +} + +void app_main(void) +{ + // ...... + + // 初始化默认事件循环 + ESP_ERROR_CHECK(esp_event_loop_create_default()); + // 注册事件(wifi启动成功) + ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_START, WIFI_CallBack, NULL, NULL)) + // 注册事件(wifi连接失败) + ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, WIFI_CallBack, NULL, NULL)) + // 注册事件(wifi连接失败) + ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, WIFI_CallBack, NULL, NULL)) + + // ...... +} + +``` + +### 2.3 补充 + +#### 2.3.1 WIFI的省电模式 + + +可以使用`esp_wifi_set_ps()`函数来配置WIFI的省电模式。 + +这个函数可以有三个参数可选: +``` c +typedef enum { + WIFI_PS_NONE, /**< No power save */ + WIFI_PS_MIN_MODEM, /**< Minimum modem power saving. In this mode, station wakes up to receive beacon every DTIM period */ + WIFI_PS_MAX_MODEM, /**< Maximum modem power saving. In this mode, interval to receive beacons is determined by the listen_interval parameter in wifi_sta_config_t */ +} wifi_ps_type_t; + +``` + +关于省电模式可以参考:[Wi-Fi场景如何选择低功耗模式](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-guides/low-power-mode.html#id8 "永久链接至标题") + +#### 2.3.2 静态IP配置 + +在`esp_netif_create_default_wifi_sta()`创建好DHCP客户端后使用函数`esp_err_t esp_netif_dhcpc_stop(esp_netif_t *esp_netif)`停止DHCP客户端。 + +使用`esp_netif_set_ip_info()`配置我们想要的静态IP,如下: + +```c +// 初始化STA设备 +esp_netif_t *esp_netif = esp_netif_create_default_wifi_sta(); + +// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ +//*******************配置静态IP************************* +esp_netif_dhcpc_stop(esp_netif); +esp_netif_ip_info_t ipInfo; +ipInfo.ip.addr = inet_addr("192.168.138.2"); +ipInfo.netmask = inet_addr("255.255.255.0"); +ipInfo.gw = inet_addr("192.168.138.2"); + +esp_netif_set_ip_info(esp_netif, &ipInfo); +esp_netif_dhcpc_start(esp_netif); +//*******************配置静态IP************************* +// ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ +``` + +### 2.3.1 WIFI Scan + +我们使用`esp_wifi_start`后,会根据配置,启动WiFI。如果不立即连接到设定的WIFI热点,我们也可以使用WIFI Scan来寻找可连接的设备。 + +`esp_wifi_scan_start`,扫描所有有效的AP +`esp_wifi_scan_get_ap_records`,获取上次扫描中找到的AP列表 `esp_wifi_scan_get_ap_num`,获取上次扫描中找到的AP数 + +这里也没什么复杂的,建议直接参考IDF官方文档:[ESP32 Wi-Fi 扫描](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-guides/wifi.html#id17 "永久链接至标题") +## 三、实例 + +### 3.1 WIFI AP模式 + +下面基于第二部分的教程实现了WIFI AP模式 +代码链接: + +> https://github.com/DuRuofu/ESP32_Learning/tree/master/05.wifi/wifi_ap + +```c +#include +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "esp_wifi.h" +#include "esp_log.h" +#include "esp_event.h" +#include "nvs_flash.h" +#include "esp_mac.h" + +#define ESP_WIFI_AP_SSID "DuRuofu_ESP32" +#define ESP_WIFI_AP_PASSWD "3.1415926" + +void WIFI_CallBack(void *event_handler_arg,esp_event_base_t event_base,int32_t event_id,void *event_data) +{ + // 连接事件 + if(event_base == IP_EVENT && event_id == IP_EVENT_AP_STAIPASSIGNED) + { + // 解析数据 + ip_event_ap_staipassigned_t* event_info = (ip_event_ap_staipassigned_t *)event_data; + ESP_LOGW("WIFI_AP", "设备已连接 MAC:"MACSTR"", MAC2STR(event_info->mac)); + } + + // 断开连接事件 + if(event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STADISCONNECTED) + { + wifi_event_ap_stadisconnected_t* event_info = (wifi_event_ap_stadisconnected_t*)event_data; + ESP_LOGW("WIFI_AP", "设备已断开 MAC:"MACSTR"", MAC2STR(event_info->mac)); + } +} + +void app_main(void) +{ + //----------------准备阶段------------------- + // Initialize NVS + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) + { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + //---------------初始化阶段------------------ + + ESP_ERROR_CHECK(esp_netif_init()); + + // 初始化默认事件循环 + ESP_ERROR_CHECK(esp_event_loop_create_default()); + // 注册设备连接事件回调 + esp_event_handler_instance_register(IP_EVENT, IP_EVENT_AP_STAIPASSIGNED, WIFI_CallBack, NULL, NULL); + // 注册设备断开连接设备回调 + esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_AP_STADISCONNECTED, WIFI_CallBack, NULL, NULL); + + // *esp_netif_ap 可以用来修改AP设置 + esp_netif_t *esp_netif_ap = esp_netif_create_default_wifi_ap(); + + /*Initialize WiFi */ + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + // WIFI_INIT_CONFIG_DEFAULT 是一个默认配置的宏 + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + //---------------配置阶段-------------------- + // 设置为AP模式 + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP)); + + // AP详细配置 + wifi_config_t wifi_ap_config = { + .ap = { + .ssid = ESP_WIFI_AP_SSID, // WIFI名称 + .ssid_len = strlen(ESP_WIFI_AP_SSID), // 名称长度 + .channel = 1, // WIFI信道 + .password = ESP_WIFI_AP_PASSWD, // WiFi密码 + .max_connection = 5, // 最大连接数,默认值是 10 + .authmode = WIFI_AUTH_WPA2_PSK, // WiFi认证方式 + }, + }; + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_ap_config)); + + //---------------启动阶段-------------------- + /* Start WiFi */ + ESP_ERROR_CHECK(esp_wifi_start()); +} +``` + +效果展示: + +![](attachments/20240314220318.png) + +连接wifi会打印连接设备的信息。 + +### 3.2 WIFI STA模式 + +下面基于第二部分的教程实现了WIFI STA模式 +代码链接: + +> https://github.com/DuRuofu/ESP32_Learning/tree/master/05.wifi/wifi_sta + +```c + +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "esp_wifi.h" +#include "esp_log.h" +#include "esp_event.h" +#include "nvs_flash.h" +#include "esp_mac.h" + +#define ESP_WIFI_STA_SSID "duruofu_win10" +#define ESP_WIFI_STA_PASSWD "1234567890" + +void WIFI_CallBack(void *event_handler_arg, esp_event_base_t event_base, int32_t event_id, void *event_data) +{ + static uint8_t connect_count = 0; + // WIFI 启动成功 + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_START"); + ESP_ERROR_CHECK(esp_wifi_connect()); + } + // WIFI 连接失败 + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_DISCONNECTED"); + connect_count++; + if (connect_count < 6) + { + ESP_ERROR_CHECK(esp_wifi_connect()); + } + else{ + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_DISCONNECTED 10 times"); + } + } + // WIFI 连接成功(获取到了IP) + if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_GOT_IP"); + ip_event_got_ip_t *info = (ip_event_got_ip_t *)event_data; + ESP_LOGI("WIFI_EVENT", "got ip:" IPSTR "", IP2STR(&info->ip_info.ip)); + } +} + +void app_main(void) +{ + //----------------准备阶段------------------- + // Initialize NVS + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) + { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + //----------------初始化阶段------------------- + ESP_ERROR_CHECK(esp_netif_init()); + + ESP_ERROR_CHECK(esp_event_loop_create_default()); + // 注册事件(wifi启动成功) + ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_START, WIFI_CallBack, NULL, NULL)); + // 注册事件(wifi连接失败) + ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, WIFI_CallBack, NULL, NULL)); + // 注册事件(wifi连接失败) + ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, WIFI_CallBack, NULL, NULL)); + + // 初始化STA设备 + esp_netif_create_default_wifi_sta(); + + /*Initialize WiFi */ + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + // WIFI_INIT_CONFIG_DEFAULT 是一个默认配置的宏 + + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + //----------------配置阶段------------------- + // 设置为STA模式 + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + + // STA详细配置 + wifi_config_t sta_config = { + .sta = { + .ssid = ESP_WIFI_STA_SSID, + .password = ESP_WIFI_STA_PASSWD, + .bssid_set = false, + }, + }; + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &sta_config)); + + //----------------启动阶段------------------- + ESP_ERROR_CHECK(esp_wifi_start()); + +} + +``` + +效果如下: + +![](attachments/20240315084947.png) + + +### 3.2 WIFI配置静态IP + +代码链接: + +> https://github.com/DuRuofu/ESP32_Learning/tree/master/05.wifi/wifi_sta_static_ip + +### 3.3 WIFI Scan + +代码链接: + +> https://github.com/DuRuofu/ESP32_Learning/tree/master/05.wifi/wifi_scan + +效果: + +![](attachments/20240315100518.png) +# 参考链接 + +1. https://www.bilibili.com/video/BV1au411E7K1?p=7&vd_source=ef5a0ab0106372751602034cdd9ab98e +2. https://www.bilibili.com/video/BV1au411E7K1?p=8&vd_source=ef5a0ab0106372751602034cdd9ab98e +3. https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-guides/wifi.html \ No newline at end of file diff --git a/docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/attachments/20240313184111.png b/docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/attachments/20240313184111.png new file mode 100644 index 0000000..fc32aaa Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/attachments/20240313184111.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/attachments/20240313184132.png b/docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/attachments/20240313184132.png new file mode 100644 index 0000000..b34531b Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/attachments/20240313184132.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/attachments/20240313190734.png b/docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/attachments/20240313190734.png new file mode 100644 index 0000000..4ca9afc Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/attachments/20240313190734.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/attachments/20240313191730.png b/docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/attachments/20240313191730.png new file mode 100644 index 0000000..06cb0a0 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/attachments/20240313191730.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/attachments/20240314165844.png b/docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/attachments/20240314165844.png new file mode 100644 index 0000000..85b940b Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/attachments/20240314165844.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/attachments/20240314220318.png b/docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/attachments/20240314220318.png new file mode 100644 index 0000000..b09e1da Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/attachments/20240314220318.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/attachments/20240314220947.png b/docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/attachments/20240314220947.png new file mode 100644 index 0000000..b4325ee Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/attachments/20240314220947.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/attachments/20240315084947.png b/docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/attachments/20240315084947.png new file mode 100644 index 0000000..84d68ee Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/attachments/20240315084947.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/attachments/20240315100518.png b/docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/attachments/20240315100518.png new file mode 100644 index 0000000..e1ad3d3 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.1-Wi-Fi功能入门/6.1.1-Wi-Fi模式与基础操作/attachments/20240315100518.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.1-TCP协议/TCP协议应用.md b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.1-TCP协议/TCP协议应用.md index e69de29..2f1992e 100644 --- a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.1-TCP协议/TCP协议应用.md +++ b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.1-TCP协议/TCP协议应用.md @@ -0,0 +1,825 @@ + +# ESP32网络入门-TCP协议 +## 一、介绍 + +在开始使用TCP协议之前,我们需要掌握一些基本的概念和前置知识: + +>最基本的一点:TCP/UDP工作在网络OSI的七层模型中的第四层——传输层,IP在第三层——网络层,WIFI(狭义上)在一二层-物理层和数据链路层。 + +### 1.1 套接字(socket) + +> 下面的部分搬运自:[Socket介绍](https://zhuanlan.zhihu.com/p/106271407#:~:text=Socket%E6%98%AF%E5%BA%94%E7%94%A8%E5%B1%82%E4%B8%8ETCP%2FIP%E5%8D%8F%E8%AE%AE%E6%97%8F%E9%80%9A%E4%BF%A1%E7%9A%84%E4%B8%AD%E9%97%B4%E8%BD%AF%E4%BB%B6%E6%8A%BD%E8%B1%A1%E5%B1%82%EF%BC%8C%E5%AE%83%E6%98%AF%E4%B8%80%E7%BB%84%E6%8E%A5%E5%8F%A3%E3%80%82,%E5%9C%A8%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E4%B8%AD%EF%BC%8CSocket%E5%85%B6%E5%AE%9E%E5%B0%B1%E6%98%AF%E4%B8%80%E4%B8%AA%E9%97%A8%E9%9D%A2%E6%A8%A1%E5%BC%8F%EF%BC%8C%E5%AE%83%E6%8A%8A%E5%A4%8D%E6%9D%82%E7%9A%84TCP%2FIP%E5%8D%8F%E8%AE%AE%E6%97%8F%E9%9A%90%E8%97%8F%E5%9C%A8Socket%E6%8E%A5%E5%8F%A3%E5%90%8E%E9%9D%A2%EF%BC%8C%E5%AF%B9%E7%94%A8%E6%88%B7%E6%9D%A5%E8%AF%B4%EF%BC%8C%E4%B8%80%E7%BB%84%E7%AE%80%E5%8D%95%E7%9A%84%E6%8E%A5%E5%8F%A3%E5%B0%B1%E6%98%AF%E5%85%A8%E9%83%A8%E3%80%82) *(如有侵权,请联系作者删除)* + +![](attachments/20240315105448.png) + +Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部。 + +![](attachments/20240315104749.png) + +网络协议是很复杂的,它的硬件接口可以是WIFI,网线,4G网卡等,我们开发网络程序,不可能亲自去了解这些物理层,链路层的网络协议和实现。我们通过抽象出统一的上层建筑(Socket)来完成代码编写,这样无论底层(链路层,网络层)是何种形式,我们需要考虑的东西都是相同的(Socket的概念是一样的)。 + +socket起源于Unix,而Unix/Linux 基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式 来操作。Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭) + +### 1.2 C/S模式 + +C/S分布式模式,是计算机用语。C是指Client,S是指Server,C/S模式就是指客户端/服务器模式。是计算机软件协同工作的一种模式,通常采取两层结构。服务器负责数据的管理,客户机负责完成与用户的交互任务。 + +![](attachments/20240315105755.png) + + + +### 1.3 TCP协议 + +>请自行了解 + +## 二、使用 + +>本节的工程基于[ESP32WIFI-1.WIFI连接](https://www.duruofu.top/2024/03/15/4.%E7%A1%AC%E4%BB%B6%E7%9B%B8%E5%85%B3/MCU/ESP32/05.ESP32WIFI%E5%85%A5%E9%97%A8/5.1-ESP32%E7%BD%91%E7%BB%9C%E5%85%A5%E9%97%A8-WIFI%E8%BF%9E%E6%8E%A5/ESP32%E7%BD%91%E7%BB%9C%E5%85%A5%E9%97%A8-WIFI%E8%BF%9E%E6%8E%A5/)中的[wifi_sta](https://github.com/DuRuofu/ESP32_Learning/tree/master/05.wifi/wifi_sta)历程 + +下图展示了TCP协议服务端和客户端的基本流程: + +![](attachments/20240315111126.png) + +### 2.1 TCP客户端 + +![](attachments/20240315110509.png) + +客户端程序流程:初始化-连接-数据交换-断开连接 + +``` mermaid +graph LR; +A(Initialize) --> B(Connect); +B --> C(Communicate); +C --> D(Disconnect); +``` + +#### 2.1.1 准备工作 + +准备工作主要是连接wifi,为下面的网络协议提供支持,可以参考:[ESP32WIFI-1.WIFI连接](https://www.duruofu.top/2024/03/15/4.%E7%A1%AC%E4%BB%B6%E7%9B%B8%E5%85%B3/MCU/ESP32/05.ESP32WIFI%E5%85%A5%E9%97%A8/5.1-ESP32%E7%BD%91%E7%BB%9C%E5%85%A5%E9%97%A8-WIFI%E8%BF%9E%E6%8E%A5/ESP32%E7%BD%91%E7%BB%9C%E5%85%A5%E9%97%A8-WIFI%E8%BF%9E%E6%8E%A5/) + +#### 2.1.2 创建套接字 + +使用函数`int socket(int domain,int type,int protocol)`创建套接字,参数分别为 + +- `domain`:指定协议家族或地址族,常用的有 `AF_INET`(IPv4 地址族)和 `AF_INET6`(IPv6 地址族)。 +- `type`:指定套接字类型,常见的有 `SOCK_STREAM`(流套接字,提供面向连接的、可靠的数据传输)和 `SOCK_DGRAM`(数据报套接字,提供无连接的、不可靠的数据传输)。 +- `protocol`:指定协议,一般为 0,默认由 `socket()` 函数根据前两个参数自动选择合适的协议。 + +```c +// 创建socket +int sock = socket(AF_INET, SOCK_STREAM, 0); +if (sock < 0) // 创建失败返回-1 +{ + ESP_LOGE(TAG, "Unable to create socket: errno %d", errno); + return; +} +``` +#### 2.1.3 配置并连接服务端 + +使用函数`connect(int s,const struct sockaddr *name,socklen_t namelen)`连接服务端,参数分别为: + +- `s`: 表示一个已经创建并绑定到本地地址的套接字描述符。 +- `name`: 是一个指向目标服务器地址结构体的指针,通常是 `struct sockaddr` 结构体或其派生结构体,用来指定要连接的远程服务器的地址信息。 +- `namelen`: 表示参数 `name` 指向的地址结构体的长度。 + +这里的` struct sockaddr `结构体用于配置IP协议,这里以IPV4为例,参数如下: + +- `sin_len`: 该字段表示结构体的长度,单位为字节。在这个结构体中,用一个字节来表示结构体的长度。 +- `sin_family`: 这是一个表示地址族(Address Family)的字段,用于指示地址的类型,如IPv4或IPv6。在这里,用`sa_family_t`类型来表示,可能是一个枚举值或整数值,用于指示IPv4地址族。(和上一步的`domain`参数相同) +- `sin_port`: 一个16位的整数,表示端口号。`in_port_t`类型通常被定义为一个16位的整数,用于存储端口号。 +- `sin_addr`: 一个`struct in_addr`类型的结构体,用于存储IPv4地址信息。通常`struct in_addr`包含一个32位的整数,表示IPv4地址。 + +代码如下: + +``` c +#define TCP_SREVER_ADDR "192.168.1.100" +#define TCP_SREVER_PORT 8080 + +// 设置服务器(IPV4) +struct sockaddr_in server_config; +server_config.sin_addr.s_addr = inet_addr(TCP_SREVER_ADDR); +server_config.sin_family = AF_INET; +server_config.sin_port = htons(TCP_SREVER_PORT); // 宏htons 用于将主机的无符号短整型数据转换成网络字节顺序(小端转大端) + +// 连接服务器 +int err = connect(sock, (struct sockaddr *)&server_config, sizeof(server_config)); +if (err != 0) +{ + ESP_LOGE(TAG, "Socket unable to connect: errno %d", errno); + break; +} +``` + +#### 2.1.4 发送消息 + +使用`send(int s,const void *dataptr,size_t size,int flags)`函数发送消息,参数为: +- `s`:指定的套接字描述符,即要发送消息的目标套接字。 +- `dataptr`:指向要发送数据的指针,可以是任意类型的数据。 +- `size`:要发送的数据大小,以字节为单位。 +- `flags`:用于指定发送操作的附加选项,通常可以设为0。 + +例如: + +```c +// 发送数据 +const char *data = "Hello World!"; +int len = send(sock, data, strlen(data), 0); +if (len < 0) +{ + ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno); + break; +} + +``` + +#### 2.1.5 接收消息 + +使用`recv(int s,void *mem,size_t len,int flags)`接收数据,参数为: + +- `s`:指定要接收数据的套接字描述符。 +- `mem`:指向存放接收数据的缓冲区的指针。 +- `len`:表示接收缓冲区的长度。 +- `flags`:指定接收操作的附加选项,通常可以设置为 0。 + +例如: +```c + +int len = recv(sock, rx_buffer, sizeof(rx_buffer), 0); +// Error occurred during receiving +if (len < 0) +{ + ESP_LOGE(TAG, "recv failed: errno %d", errno); + return; +} +// Data received +else +{ + ESP_LOGI(TAG, "Received %d bytes from %s:", len, TCP_SREVER_ADDR); + ESP_LOGI(TAG, "%.*s", len, rx_buffer); +} + +``` + +完整程序请看下面第三部分: +### 2.1 TCP服务器 + +#### 2.2.1 准备工作 + +初始化NVS、 连接WIFI + +#### 2.2.2 创建并配置socket + + +```c +// 创建套接字 +int listen_sock = socket(AF_INET, SOCK_STREAM, 0); +if (listen_sock < 0) +{ + ESP_LOGE(TAG, "Unable to create socket: errno %d", errno); + vTaskDelete(NULL); + return; +} +// 设置套接字属性 +int opt = 1; +setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); +ESP_LOGI(TAG, "Socket created"); + +``` + +这里使用了一个用于设置 socket 属性,用函数 `setsockopt()`,函数原形如下: + +```c +int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen); +``` + +- `sockfd`:指定要设置选项的套接字文件描述符。 +- `level`:指定选项的协议级别。常见的级别包括 `SOL_SOCKET`(通用套接字选项)和 `IPPROTO_TCP`(TCP 协议选项)等。 +- `optname`:指定要设置的选项名称,可以是下列之一或者协议特定的选项。常见的选项包括 `SO_REUSEADDR`(允许地址重用)、`SO_KEEPALIVE`(启用连接保活)、`SO_RCVBUF`(设置接收缓冲区大小)等。 +- `optval`:指向包含选项值的缓冲区的指针。 +- `optlen`:指定选项值的长度。 + +#### 2.2.3 配置服务器信息 + +代码: + +```c +// 设置服务器(IPV4) +struct sockaddr_storage dest_addr; +struct sockaddr_in *dest_addr_ip4 = (struct sockaddr_in *)&dest_addr; +dest_addr_ip4->sin_addr.s_addr = htonl(INADDR_ANY); +dest_addr_ip4->sin_family = AF_INET; +dest_addr_ip4->sin_port = htons(TCP_SREVER_PORT); + +// 绑定套接字 +int err = bind(listen_sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr)); +if (err != 0) +{ + ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno); + ESP_LOGE(TAG, "IPPROTO: %d", AF_INET); + goto CLEAN_UP; +} +ESP_LOGI(TAG, "Socket bound, port %d", TCP_SREVER_PORT); + +``` + +`bind` 函数用于将一个套接字与一个地址(通常是 IP 地址和端口号)绑定在一起,以便在该地址上监听连接或发送数据。它的原型如下: + +```c +int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); +``` + + +1. `sockfd`:指定要绑定地址的套接字文件描述符。 +2. `addr`:指向一个 `sockaddr` 结构体的指针,该结构体包含了要绑定的地址信息。在 IPv4 地址族中,可以使用 `sockaddr_in` 结构体;在 IPv6 地址族中,可以使用 `sockaddr_in6` 结构体。通常,你需要将地址信息转换为 `sockaddr` 结构体的形式,然后传递给 `bind` 函数。 +3. `addrlen`:指定地址结构体的长度。 + +#### 2.2.4 监听客户端连接 + +```c +// 监听套接字 +err = listen(listen_sock, 1); +if (err != 0) +{ + ESP_LOGE(TAG, "Error occurred during listen: errno %d", errno); + goto CLEAN_UP; +} +``` + +`listen()` 函数用于将一个套接字(通常是服务器套接字)转换为被动套接字,即用于接受连接请求 + +1. `sockfd`:指定要监听的套接字文件描述符。 +2. `backlog`:指定在内核中排队等待接受连接的最大连接数。这个参数限制了同时等待连接的数量,超过这个数量的连接请求将被拒绝。这并不是一个限制同时连接的数量,而是限制等待连接队列的长度。 +3. `listen()` 函数在成功时返回 0,失败时返回 -1 + +#### 2.2.4 建立接收 + +```c +ESP_LOGI(TAG, "Socket listening"); +struct sockaddr_storage source_addr; // Large enough for both IPv4 or IPv6 +socklen_t addr_len = sizeof(source_addr); +int sock = accept(listen_sock, (struct sockaddr *)&source_addr, &addr_len); +if (sock < 0) +{ + ESP_LOGE(TAG, "Unable to accept connection: errno %d", errno); + break; +} + + +// Set tcp keepalive option +int keepAlive = 1; +int keepIdle = 5; // TCP keep-alive idle time(s) +int keepInterval = 5; // TCP keep-alive interval time(s) +int keepCount = 3; // TCP keep-alive packet retry send counts + +setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &keepAlive, sizeof(int)); +setsockopt(sock, IPPROTO_TCP, TCP_KEEPIDLE, &keepIdle, sizeof(int)); +setsockopt(sock, IPPROTO_TCP, TCP_KEEPINTVL, &keepInterval, sizeof(int)); +setsockopt(sock, IPPROTO_TCP, TCP_KEEPCNT, &keepCount, sizeof(int)); + + +``` + +`sockaddr_storage` 是一个足够大的结构体,可用于存储任意地址族(IPv4 或 IPv6)的地址信息。 + +建立连接使用函数`accept()`,它用于接受传入的连接请求,并创建一个**新的套接字**来与客户端进行通信。它的原型如下: + +```c +int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); +``` + +参数: + +- `sockfd`:指定正在监听连接请求的套接字文件描述符。 +- `addr`:用于存储客户端地址信息的指针。当有连接请求到达时,`accept()` 函数会将客户端的地址信息填写到这个结构体中。 +- `addrlen`:指向一个整数的指针,表示传入的地址结构体的长度。在调用 `accept()` 函数之前,必须将这个参数设置为 `addr` 缓冲区的大小。当 `accept()` 函数返回时,这个参数会更新为实际填充到 `addr` 缓冲区中的地址结构体的长度。 + +建立连接成功后,通过调用 `setsockopt()` 函数,设置了套接字的 Keep-Alive 选项,以确保连接保持活跃状态。 + +- `SO_KEEPALIVE`:启用或禁用 TCP Keep-Alive 机制。 +- `TCP_KEEPIDLE`:设置 TCP Keep-Alive 空闲时间,即连接空闲多长时间后开始发送 Keep-Alive 消息。 +- `TCP_KEEPINTVL`:设置 TCP Keep-Alive 消息的发送间隔,即两次 Keep-Alive 消息之间的时间间隔。 +- `TCP_KEEPCNT`:设置 TCP Keep-Alive 消息的发送次数,即发送多少次 Keep-Alive 消息后仍未收到响应才认为连接失效。 +#### 2.2.6 接收/发送数据 + +接收和发送依然使用`recv`和`send`,下面实现了一个简单的数据接收,回传函数,参数为建立连接的套接字。 + +```c +// 数据接收与回传 +static void do_retransmit(const int sock) +{ +int len; +char rx_buffer[128]; + +do +{ + len = recv(sock, rx_buffer, sizeof(rx_buffer) - 1, 0); + if (len < 0) + { + ESP_LOGE(TAG, "Error occurred during receiving: errno %d", errno); + } + else if (len == 0) + { + ESP_LOGW(TAG, "Connection closed"); + } + else + { + rx_buffer[len] = 0; // Null-terminate whatever is received and treat it like a string + ESP_LOGI(TAG, "Received %d bytes: %s", len, rx_buffer); + + // send() can return less bytes than supplied length. + // Walk-around for robust implementation. + int to_write = len; + while (to_write > 0) + { + int written = send(sock, rx_buffer + (len - to_write), to_write, 0); + if (written < 0) + { + ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno); + // Failed to retransmit, giving up + return; + } + to_write -= written; + } + } +} while (len > 0); +} + +``` + +程序解释如下: + +- `recv()` 函数用于从套接字接收数据,并将接收到的数据存储在 `rx_buffer` 中。它的参数包括套接字文件描述符 `sock`、接收缓冲区 `rx_buffer`、缓冲区大小以及一些可选的标志参数。如果 `recv()` 返回值小于 0,则表示出现了错误;如果返回值为 0,则表示连接已关闭;否则,返回接收到的字节数。 +- 如果接收到的字节数小于 0,表示发生了接收错误,这时会记录错误信息到日志。 +- 如果接收到的字节数为 0,表示连接已关闭,这时会记录警告信息到日志。 +- 如果接收到了数据,会记录接收到的数据字节数和数据内容到日志,并通过 `send()` 函数将接收到的数据回传给客户端。由于 `send()` 函数可能一次未能发送完所有数据,所以在一个循环中,将剩余的数据继续发送,直到所有数据都被发送出去。 +- 如果在发送过程中出现了发送错误(`send()` 返回值小于 0),则会记录错误信息到日志,并返回函数,放弃继续回传数据。 +- 整个函数在循环中进行,直到 `recv()` 返回值小于等于 0,表示接收到的数据长度为 0(连接关闭)或出现了接收错误。 + +#### 2.2.6 关闭连接和销毁套接字 + +- `shutdown();`:这个函数调用会关闭套接字的一部分或者全部通信。第二个参数指定了关闭方式: + - 如果为 0,则表示关闭套接字的读取功能,即不能再从套接字中读取数据。 + - 如果为 1,则表示关闭套接字的写入功能,即不能再向套接字中写入数据。 + - 如果为 2,则表示关闭套接字的读取和写入功能,即完全关闭套接字的通信功能。 +- `close(sock);`:这个函数调用会彻底关闭套接字,释放它占用的资源。关闭套接字后,不能再对它进行任何操作。 + +以上就是基本的TCP服务的编程流程,关于服务器实例请参考第三部分 +## 三、示例 + +### 3.1 TCP客户端程序 + +>代码见: https://github.com/DuRuofu/ESP32_Learning/tree/master/05.wifi/wifi_tcp_client + +```c +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "esp_wifi.h" +#include "esp_log.h" +#include "esp_event.h" +#include "nvs_flash.h" +#include "esp_mac.h" +#include "esp_netif.h" +#include + +#define ESP_WIFI_STA_SSID "duruofu_win10" +#define ESP_WIFI_STA_PASSWD "1234567890" +#define TCP_SREVER_ADDR "192.168.137.1" +#define TCP_SREVER_PORT 8080 + +static const char *TAG = "main"; + +void WIFI_CallBack(void *event_handler_arg, esp_event_base_t event_base, int32_t event_id, void *event_data) +{ + static uint8_t connect_count = 0; + // WIFI 启动成功 + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_START"); + ESP_ERROR_CHECK(esp_wifi_connect()); + } + // WIFI 连接失败 + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_DISCONNECTED"); + connect_count++; + if (connect_count < 6) + { + vTaskDelay(1000 / portTICK_PERIOD_MS); + ESP_ERROR_CHECK(esp_wifi_connect()); + } + else + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_DISCONNECTED 10 times"); + } + } + // WIFI 连接成功(获取到了IP) + if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_GOT_IP"); + ip_event_got_ip_t *info = (ip_event_got_ip_t *)event_data; + ESP_LOGI("WIFI_EVENT", "got ip:" IPSTR "", IP2STR(&info->ip_info.ip)); + } +} + +static void tcp_client_task(void *pvParameters) +{ + + + // 等待wifi连接成功(暂时这样处理) + vTaskDelay(5000 / portTICK_PERIOD_MS); + + ESP_LOGI("tcp_client_task", "tcp_client_task start"); + + // 创建socket + int sock = socket(AF_INET, SOCK_STREAM, 0); + if (sock < 0) // 创建失败返回-1 + { + ESP_LOGE(TAG, "Unable to create socket: errno %d", errno); + return; + } + + // 设置服务器(IPV4) + struct sockaddr_in server_config; + server_config.sin_addr.s_addr = inet_addr(TCP_SREVER_ADDR); + server_config.sin_family = AF_INET; + server_config.sin_port = htons(TCP_SREVER_PORT); // 宏htons 用于将主机的无符号短整型数据转换成网络字节顺序(小端转大端) + + // 连接服务器 + int err = connect(sock, (struct sockaddr *)&server_config, sizeof(server_config)); + if (err != 0) + { + ESP_LOGE(TAG, "Socket unable to connect: errno %d", errno); + return; + } + + // 发送数据 + const char *data = "Hello World!"; + int len = send(sock, data, strlen(data), 0); + if (len < 0) + { + ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno); + return; + } + + char rx_buffer[1024]; + // 接收数据,并发回 + while(1) + { + int len = recv(sock, rx_buffer, sizeof(rx_buffer), 0); + // Error occurred during receiving + if (len < 0) + { + ESP_LOGE(TAG, "recv failed: errno %d", errno); + break; + } + // Data received + else + { + ESP_LOGI(TAG, "Received %d bytes from %s:", len, TCP_SREVER_ADDR); + ESP_LOGI(TAG, "%.*s", len, rx_buffer); + + // 发送数据 + int len_end = send(sock, rx_buffer, len, 0); + if (len_end < 0) + { + ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno); + break; + } + } + } + + vTaskDelete(NULL); +} + +// wifi初始化 +static void wifi_sta_init(void) +{ + ESP_ERROR_CHECK(esp_netif_init()); + + // 注册事件(wifi启动成功) + ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_START, WIFI_CallBack, NULL, NULL)); + // 注册事件(wifi连接失败) + ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, WIFI_CallBack, NULL, NULL)); + // 注册事件(wifi连接失败) + ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, WIFI_CallBack, NULL, NULL)); + + // 初始化STA设备 + esp_netif_create_default_wifi_sta(); + + /*Initialize WiFi */ + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + // WIFI_INIT_CONFIG_DEFAULT 是一个默认配置的宏 + + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + //----------------配置阶段------------------- + // 初始化WIFI设备( 为 WiFi 驱动初始化 WiFi 分配资源,如 WiFi 控制结构、RX/TX 缓冲区、WiFi NVS 结构等,这个 WiFi 也启动 WiFi 任务。必须先调用此API,然后才能调用所有其他WiFi API) + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + + // STA详细配置 + wifi_config_t sta_config = { + .sta = { + .ssid = ESP_WIFI_STA_SSID, + .password = ESP_WIFI_STA_PASSWD, + .bssid_set = false, + }, + }; + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &sta_config)); + + //----------------启动阶段------------------- + ESP_ERROR_CHECK(esp_wifi_start()); + + //----------------配置省电模式------------------- + // 不省电(数据传输会更快) + ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE)); +} + +void app_main(void) +{ + // Initialize NVS + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) + { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + // 创建默认事件循环 + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + // 配置启动WIFI + wifi_sta_init(); + + // 创建TCP客户端任务 + xTaskCreate(tcp_client_task, "tcp_client_task", 4096, NULL, 5, NULL); +} +``` + +程序效果如下,可以正常收发数据: + +![](attachments/20240315144045.png) + + +### 3.2 TCP服务端程序 + +>代码见: https://github.com/DuRuofu/ESP32_Learning/tree/master/05.wifi/wifi_tcp_server + + +```c +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "esp_wifi.h" +#include "esp_log.h" +#include "esp_event.h" +#include "nvs_flash.h" +#include "esp_mac.h" +#include "esp_netif.h" +#include + +#define ESP_WIFI_STA_SSID "duruofu_win10" +#define ESP_WIFI_STA_PASSWD "1234567890" + +#define TCP_SREVER_PORT 8080 + +static const char *TAG = "main"; + +void WIFI_CallBack(void *event_handler_arg, esp_event_base_t event_base, int32_t event_id, void *event_data) +{ + static uint8_t connect_count = 0; + // WIFI 启动成功 + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_START"); + ESP_ERROR_CHECK(esp_wifi_connect()); + } + // WIFI 连接失败 + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_DISCONNECTED"); + connect_count++; + if (connect_count < 6) + { + vTaskDelay(1000 / portTICK_PERIOD_MS); + ESP_ERROR_CHECK(esp_wifi_connect()); + } + else + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_DISCONNECTED 10 times"); + } + } + // WIFI 连接成功(获取到了IP) + if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_GOT_IP"); + ip_event_got_ip_t *info = (ip_event_got_ip_t *)event_data; + ESP_LOGI("WIFI_EVENT", "got ip:" IPSTR "", IP2STR(&info->ip_info.ip)); + } +} + +// 数据接收与回传 +static void do_retransmit(const int sock) +{ + int len; + char rx_buffer[128]; + + do + { + len = recv(sock, rx_buffer, sizeof(rx_buffer) - 1, 0); + if (len < 0) + { + ESP_LOGE(TAG, "Error occurred during receiving: errno %d", errno); + } + else if (len == 0) + { + ESP_LOGW(TAG, "Connection closed"); + } + else + { + rx_buffer[len] = 0; // Null-terminate whatever is received and treat it like a string + ESP_LOGI(TAG, "Received %d bytes: %s", len, rx_buffer); + + // send() can return less bytes than supplied length. + // Walk-around for robust implementation. + int to_write = len; + while (to_write > 0) + { + int written = send(sock, rx_buffer + (len - to_write), to_write, 0); + if (written < 0) + { + ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno); + // Failed to retransmit, giving up + return; + } + to_write -= written; + } + } + } while (len > 0); +} + +// tcp服务器任务 +static void tcp_server_task(void *pvParameters) +{ + // 创建套接字 + int listen_sock = socket(AF_INET, SOCK_STREAM, 0); + if (listen_sock < 0) + { + ESP_LOGE(TAG, "Unable to create socket: errno %d", errno); + vTaskDelete(NULL); + return; + } + // 设置套接字属性 + int opt = 1; + setsockopt(listen_sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); + ESP_LOGI(TAG, "Socket created"); + + // 设置服务器(IPV4) + struct sockaddr_storage dest_addr; + struct sockaddr_in *dest_addr_ip4 = (struct sockaddr_in *)&dest_addr; + dest_addr_ip4->sin_addr.s_addr = htonl(INADDR_ANY); + dest_addr_ip4->sin_family = AF_INET; + dest_addr_ip4->sin_port = htons(TCP_SREVER_PORT); + + // 绑定套接字 + int err = bind(listen_sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr)); + if (err != 0) + { + ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno); + ESP_LOGE(TAG, "IPPROTO: %d", AF_INET); + goto CLEAN_UP; + } + ESP_LOGI(TAG, "Socket bound, port %d", TCP_SREVER_PORT); + + // 监听套接字 (阻塞) + err = listen(listen_sock, 1); + if (err != 0) + { + ESP_LOGE(TAG, "Error occurred during listen: errno %d", errno); + goto CLEAN_UP; + } + + while (1) + { + char addr_str[128]; + + ESP_LOGI(TAG, "Socket listening"); + struct sockaddr_storage source_addr; // Large enough for both IPv4 or IPv6 + socklen_t addr_len = sizeof(source_addr); + int sock = accept(listen_sock, (struct sockaddr *)&source_addr, &addr_len); + if (sock < 0) + { + ESP_LOGE(TAG, "Unable to accept connection: errno %d", errno); + break; + } + + // Set tcp keepalive option + int keepAlive = 1; + int keepIdle = 5; // TCP keep-alive idle time(s) + int keepInterval = 5; // TCP keep-alive interval time(s) + int keepCount = 3; // TCP keep-alive packet retry send counts + + setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, &keepAlive, sizeof(int)); + setsockopt(sock, IPPROTO_TCP, TCP_KEEPIDLE, &keepIdle, sizeof(int)); + setsockopt(sock, IPPROTO_TCP, TCP_KEEPINTVL, &keepInterval, sizeof(int)); + setsockopt(sock, IPPROTO_TCP, TCP_KEEPCNT, &keepCount, sizeof(int)); + + // Convert ip address to string + if (source_addr.ss_family == PF_INET) + { + inet_ntoa_r(((struct sockaddr_in *)&source_addr)->sin_addr, addr_str, sizeof(addr_str) - 1); + } + + ESP_LOGI(TAG, "Socket accepted ip address: %s", addr_str); + + do_retransmit(sock); + + shutdown(sock, 0); + close(sock); + } + +CLEAN_UP: + close(listen_sock); + vTaskDelete(NULL); +} + +// wifi初始化 +static void wifi_sta_init(void) +{ + ESP_ERROR_CHECK(esp_netif_init()); + + // 注册事件(wifi启动成功) + ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_START, WIFI_CallBack, NULL, NULL)); + // 注册事件(wifi连接失败) + ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, WIFI_CallBack, NULL, NULL)); + // 注册事件(wifi连接失败) + ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, WIFI_CallBack, NULL, NULL)); + + // 初始化STA设备 + esp_netif_create_default_wifi_sta(); + + /*Initialize WiFi */ + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + // WIFI_INIT_CONFIG_DEFAULT 是一个默认配置的宏 + + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + //----------------配置阶段------------------- + // 初始化WIFI设备( 为 WiFi 驱动初始化 WiFi 分配资源,如 WiFi 控制结构、RX/TX 缓冲区、WiFi NVS 结构等,这个 WiFi 也启动 WiFi 任务。必须先调用此API,然后才能调用所有其他WiFi API) + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + + // STA详细配置 + wifi_config_t sta_config = { + .sta = { + .ssid = ESP_WIFI_STA_SSID, + .password = ESP_WIFI_STA_PASSWD, + .bssid_set = false, + }, + }; + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &sta_config)); + + //----------------启动阶段------------------- + ESP_ERROR_CHECK(esp_wifi_start()); + + //----------------配置省电模式------------------- + // 不省电(数据传输会更快) + ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE)); +} + +void app_main(void) +{ + // Initialize NVS + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) + { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + // 创建默认事件循环 + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + // 配置启动WIFI + wifi_sta_init(); + + // 创建TCP服务器任务 + xTaskCreate(tcp_server_task, "tcp_server_task", 4096, NULL, 5, NULL); +} + +``` + + +效果演示: + +![](attachments/20240315180322.png) + +>值得注意的一点:这里将整个tcpserver的流程放在一个task里,以至于他只能一对一通信,若要连接多个,则需要将连接,接收的部分也作为task来编写。 +>每次建立连接就会创建一个新的套接字,将这个新的套接字放到一个新的线程进行通信,就能实现多个客户端连接。 + +# 参考链接 + +1. https://github.com/espressif/esp-idf/blob/release/v5.1/examples/protocols/sockets/tcp_server/main/tcp_server.c \ No newline at end of file diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.1-TCP协议/attachments/20240315104749.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.1-TCP协议/attachments/20240315104749.png new file mode 100644 index 0000000..2276c3a Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.1-TCP协议/attachments/20240315104749.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.1-TCP协议/attachments/20240315105448.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.1-TCP协议/attachments/20240315105448.png new file mode 100644 index 0000000..c568651 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.1-TCP协议/attachments/20240315105448.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.1-TCP协议/attachments/20240315105755.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.1-TCP协议/attachments/20240315105755.png new file mode 100644 index 0000000..cf97db2 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.1-TCP协议/attachments/20240315105755.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.1-TCP协议/attachments/20240315110509.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.1-TCP协议/attachments/20240315110509.png new file mode 100644 index 0000000..f6bcc17 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.1-TCP协议/attachments/20240315110509.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.1-TCP协议/attachments/20240315111126.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.1-TCP协议/attachments/20240315111126.png new file mode 100644 index 0000000..355b6c0 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.1-TCP协议/attachments/20240315111126.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.1-TCP协议/attachments/20240315144045.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.1-TCP协议/attachments/20240315144045.png new file mode 100644 index 0000000..5fa3276 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.1-TCP协议/attachments/20240315144045.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.1-TCP协议/attachments/20240315180322.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.1-TCP协议/attachments/20240315180322.png new file mode 100644 index 0000000..0407175 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.1-TCP协议/attachments/20240315180322.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.2-UDP协议/UDP协议应用.md b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.2-UDP协议/UDP协议应用.md index e69de29..c8f6106 100644 --- a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.2-UDP协议/UDP协议应用.md +++ b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.2-UDP协议/UDP协议应用.md @@ -0,0 +1,577 @@ + + +# ESP32网络入门-UDP协议 + +## 一、概述 + +上一节教程[ESP32WIFI-2.TCP协议](https://www.duruofu.top/2024/03/15/4.%E7%A1%AC%E4%BB%B6%E7%9B%B8%E5%85%B3/MCU/ESP32/05.ESP32WIFI%E5%85%A5%E9%97%A8/5.2-ESP32%E7%BD%91%E7%BB%9C%E5%85%A5%E9%97%A8-TCP%E5%8D%8F%E8%AE%AE/ESP32%E7%BD%91%E7%BB%9C%E5%85%A5%E9%97%A8-TCP%E5%8D%8F%E8%AE%AE/)详细介绍了TCP协议,本机我们介绍UDP的使用。 + +UDP和TCP在程序上的区别,主要如下图: + +>图来自[Michael_ee](https://space.bilibili.com/1338335828)的UDP教程。 + +![](attachments/20240317142714.png) + +最大的区别就是UDP没有建立连接的过程。 + +UDP和TCP的对比如下: + +![](attachments/20240317143536.png) + +## 二、使用 + +> 学习了上一节TCP这部分就会很简单,足够简单就不多描述了。 + +### 2.1 UDP客户端 + +首先我们要初始化WIFI,连接WIFI,这是编写UDP程序的基础,连接WIFI在此不再赘述。 + +>后面的部分,默认已经连接好网络 + +#### 2.1.1 创建socket + +```c +// 创建socket +int sock = socket(AF_INET, SOCK_DGRAM, 0); +if (sock < 0) // 创建失败返回-1 +{ + ESP_LOGE(TAG, "Unable to create socket: errno %d", errno); + return; +} +``` +#### 2.1.2 配置发送目标服务器信息 + +```c +// 目标服务器地址端口 +#define UDP_SREVER_ADDR "255.255.255.255" +#define UDP_SREVER_PORT 8080 +// 设置服务器(IPV4) +struct sockaddr_in server_config; +server_config.sin_addr.s_addr = inet_addr(UDP_SREVER_ADDR); +server_config.sin_family = AF_INET; +server_config.sin_port = htons(UDP_SREVER_PORT); // 宏htons 用于将主机的无符号短整型数据转换成网络字节顺序(小端转大端) +``` + +服务器的地址选择 `255.255.255.255`,意思是不指定局域网内的某一设备,局域网所有的设备如果监听了这个端口号,那么都可以收到ESP32发来的消息 + +#### 2.1.3 发送数据 +```c +// 发送数据 +const char *data = "Hello World!"; +int err = sendto(sock, data, strlen(data), 0, (struct sockaddr *)&server_config, sizeof(server_config)); +if (err < 0) +{ + ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno); + close(sock); +} +``` +#### 2.1.4 接收数据 + +```c +char rx_buffer[1024]; +// 接收数据,并发回 +while(1) +{ + // 清空缓存 + memset(rx_buffer, 0, sizeof(rx_buffer)); + // 接收数据 + + struct sockaddr_in source_addr; + socklen_t socklen = sizeof(source_addr); + int len = recvfrom(sock, rx_buffer, sizeof(rx_buffer) - 1, 0, (struct sockaddr *)&source_addr, &socklen); + if (len < 0) + { + ESP_LOGE(TAG, "Error occurred during receiving: errno %d", errno); + break; + } + else + { + // 打印接收到的数据 + ESP_LOGI(TAG, "Received %d bytes: %s", len, rx_buffer); + } +} +``` + + +### 2.2 UDP服务端 + +>后面的部分,默认已经连接好网络 +#### 2.2.1 创建socket + +```c +// 创建socket +int sock = socket(AF_INET, SOCK_DGRAM, 0); +if (sock < 0) // 创建失败返回-1 +{ + ESP_LOGE(TAG, "Unable to create socket: errno %d", errno); + return; +} +``` +#### 2.2.2 配置服务器信息 + +``` c +// 服务器地址端口 +#define UDP_SREVER_PORT 8080 +// 设置服务器(IPV4) +struct sockaddr_in server_config; +server_config.sin_addr.s_addr = htonl(INADDR_ANY); +server_config.sin_family = AF_INET; +server_config.sin_port = htons(UDP_SREVER_PORT); // 宏htons 用于将主机的无符号短整型数据转换成网络字节顺序(小端转大端) +``` + +这里就不需要配置服务器IP了,使用ESP32自身分配到的IP。 +#### 2.2.3 绑定端口 + +```c +// 绑定端口 +int err = bind(sock, (struct sockaddr *)&server_config, sizeof(server_config)); +if (err < 0) +{ + ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno); +} +ESP_LOGI(TAG, "Socket bound, port %d", UDP_SREVER_PORT); + +``` + +#### 2.2.4 接收数据 + +```c +char rx_buffer[1024]; +// 接收数据,并发回 +while(1) +{ + // 清空缓存 + memset(rx_buffer, 0, sizeof(rx_buffer)); + // 接收数据 + + struct sockaddr_in source_addr; + socklen_t socklen = sizeof(source_addr); + int len = recvfrom(sock, rx_buffer, sizeof(rx_buffer) - 1, 0, (struct sockaddr *)&source_addr, &socklen); + if (len < 0) + { + ESP_LOGE(TAG, "Error occurred during receiving: errno %d", errno); + break; + } + else + { + // 打印接收到的数据 + ESP_LOGI(TAG, "Received %d bytes: %s", len, rx_buffer); + } +} +``` + +#### 2.2.5 发送数据 + +```c +// 发送数据 +int err = sendto(sock, rx_buffer, len, 0, (struct sockaddr *)&source_addr, sizeof(source_addr)); +if (err < 0) +{ + ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno); + break; +} +ESP_LOGI(TAG, "Send success"); +``` + + +>有没有发现这服务端代码相比UDP客户端除了多一个使用`bind`函数绑定地址,剩下都一模一样。(可不是我在水字数😀) + +这就涉及到UDP本身的知识了,从套接字编程的角度来看,它是完全相同的。由于UDP是无连接的,因此服务器和客户端都将使用相同的sendto和recvfrom方法。 + +但是我们在通信时,一般来说以主从通信为主,我们更喜欢把主机称作为服务端。相比客户端,在DUP里服务端唯一的不同就是服务端的端口和地址是确定的。这样其他客户端才能准确无误向服务端发送消息。 + +所以上面的示例代码了,服务端多了一个绑定端口的步骤。其余的都是一样的。 + +使用我们使用网络调试助手会发现,协议类型一栏不分客户端和服务端。 + +![](attachments/20240317155554.png) +## 三、案例(完整代码) + +### 3.1 UDP客户端 + +>代码:https://github.com/DuRuofu/ESP32_Learning/tree/master/05.wifi/wifi_udp_client + +```c +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "esp_wifi.h" +#include "esp_log.h" +#include "esp_event.h" +#include "nvs_flash.h" +#include "esp_mac.h" +#include "esp_netif.h" +#include + +// 要连接的WIFI +#define ESP_WIFI_STA_SSID "duruofu_win10" +#define ESP_WIFI_STA_PASSWD "1234567890" + +// 目标服务器地址端口 +#define UDP_SREVER_ADDR "255.255.255.255" +#define UDP_SREVER_PORT 8080 + +static const char *TAG = "main"; + +void WIFI_CallBack(void *event_handler_arg, esp_event_base_t event_base, int32_t event_id, void *event_data) +{ + static uint8_t connect_count = 0; + // WIFI 启动成功 + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_START"); + ESP_ERROR_CHECK(esp_wifi_connect()); + } + // WIFI 连接失败 + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_DISCONNECTED"); + connect_count++; + if (connect_count < 6) + { + vTaskDelay(1000 / portTICK_PERIOD_MS); + ESP_ERROR_CHECK(esp_wifi_connect()); + } + else + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_DISCONNECTED 10 times"); + } + } + // WIFI 连接成功(获取到了IP) + if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_GOT_IP"); + ip_event_got_ip_t *info = (ip_event_got_ip_t *)event_data; + ESP_LOGI("WIFI_EVENT", "got ip:" IPSTR "", IP2STR(&info->ip_info.ip)); + } +} + +// udp客户端 +static void udp_client_task(void *pvParameters) +{ + // 等待wifi连接成功(暂时这样处理) + vTaskDelay(5000 / portTICK_PERIOD_MS); + ESP_LOGI("udp_client_task", "udp_client_task start"); + + // 创建socket + int sock = socket(AF_INET, SOCK_DGRAM, 0); + if (sock < 0) // 创建失败返回-1 + { + ESP_LOGE(TAG, "Unable to create socket: errno %d", errno); + return; + } + + // 设置服务器(IPV4) + struct sockaddr_in server_config; + server_config.sin_addr.s_addr = inet_addr(UDP_SREVER_ADDR); + server_config.sin_family = AF_INET; + server_config.sin_port = htons(UDP_SREVER_PORT); // 宏htons 用于将主机的无符号短整型数据转换成网络字节顺序(小端转大端) + + + // 发送数据 + const char *data = "Hello World!"; + int err = sendto(sock, data, strlen(data), 0, (struct sockaddr *)&server_config, sizeof(server_config)); + if (err < 0) + { + ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno); + close(sock); + } + + char rx_buffer[1024]; + // 接收数据,并发回 + while(1) + { + // 清空缓存 + memset(rx_buffer, 0, sizeof(rx_buffer)); + // 接收数据 + + struct sockaddr_in source_addr; + socklen_t socklen = sizeof(source_addr); + int len = recvfrom(sock, rx_buffer, sizeof(rx_buffer) - 1, 0, (struct sockaddr *)&source_addr, &socklen); + if (len < 0) + { + ESP_LOGE(TAG, "Error occurred during receiving: errno %d", errno); + break; + } + else + { + // 打印接收到的数据 + ESP_LOGI(TAG, "Received %d bytes: %s", len, rx_buffer); + } + } + // 关闭socket + close(sock); + vTaskDelete(NULL); +} + +// wifi初始化 +static void wifi_sta_init(void) +{ + ESP_ERROR_CHECK(esp_netif_init()); + + // 注册事件(wifi启动成功) + ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_START, WIFI_CallBack, NULL, NULL)); + // 注册事件(wifi连接失败) + ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, WIFI_CallBack, NULL, NULL)); + // 注册事件(wifi连接失败) + ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, WIFI_CallBack, NULL, NULL)); + + // 初始化STA设备 + esp_netif_create_default_wifi_sta(); + + /*Initialize WiFi */ + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + // WIFI_INIT_CONFIG_DEFAULT 是一个默认配置的宏 + + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + //----------------配置阶段------------------- + // 初始化WIFI设备( 为 WiFi 驱动初始化 WiFi 分配资源,如 WiFi 控制结构、RX/TX 缓冲区、WiFi NVS 结构等,这个 WiFi 也启动 WiFi 任务。必须先调用此API,然后才能调用所有其他WiFi API) + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + + // STA详细配置 + wifi_config_t sta_config = { + .sta = { + .ssid = ESP_WIFI_STA_SSID, + .password = ESP_WIFI_STA_PASSWD, + .bssid_set = false, + }, + }; + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &sta_config)); + + //----------------启动阶段------------------- + ESP_ERROR_CHECK(esp_wifi_start()); + + //----------------配置省电模式------------------- + // 不省电(数据传输会更快) + ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE)); +} + +void app_main(void) +{ + // Initialize NVS + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) + { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + // 创建默认事件循环 + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + // 配置启动WIFI + wifi_sta_init(); + + // 创建TCP客户端任务 + xTaskCreate(udp_client_task, "udp_client_task", 4096, NULL, 5, NULL); +} + +``` + +效果: + +![](attachments/20240317151255.png) +### 3.2 UDP服务端 + +>代码:https://github.com/DuRuofu/ESP32_Learning/tree/master/05.wifi/wifi_udp_server + +```c +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "esp_wifi.h" +#include "esp_log.h" +#include "esp_event.h" +#include "nvs_flash.h" +#include "esp_mac.h" +#include "esp_netif.h" +#include +#include "lwip/err.h" +#include "lwip/sys.h" +#include + + +// 要连接的WIFI +#define ESP_WIFI_STA_SSID "duruofu_win10" +#define ESP_WIFI_STA_PASSWD "1234567890" + +// 服务器端口 +#define UDP_SREVER_PORT 8080 + +static const char *TAG = "main"; + +void WIFI_CallBack(void *event_handler_arg, esp_event_base_t event_base, int32_t event_id, void *event_data) +{ + static uint8_t connect_count = 0; + // WIFI 启动成功 + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_START"); + ESP_ERROR_CHECK(esp_wifi_connect()); + } + // WIFI 连接失败 + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_DISCONNECTED"); + connect_count++; + if (connect_count < 6) + { + vTaskDelay(1000 / portTICK_PERIOD_MS); + ESP_ERROR_CHECK(esp_wifi_connect()); + } + else + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_DISCONNECTED 10 times"); + } + } + // WIFI 连接成功(获取到了IP) + if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_GOT_IP"); + ip_event_got_ip_t *info = (ip_event_got_ip_t *)event_data; + ESP_LOGI("WIFI_EVENT", "got ip:" IPSTR "", IP2STR(&info->ip_info.ip)); + } +} + +// udp客户端 +static void udp_server_task(void *pvParameters) +{ + // 等待wifi连接成功(暂时这样处理) + vTaskDelay(5000 / portTICK_PERIOD_MS); + ESP_LOGI("udp_server_task", "udp_server_task start"); + + // 创建socket + int sock = socket(AF_INET, SOCK_DGRAM, 0); + if (sock < 0) // 创建失败返回-1 + { + ESP_LOGE(TAG, "Unable to create socket: errno %d", errno); + return; + } + + // 设置服务器(IPV4) + struct sockaddr_in server_config; + server_config.sin_addr.s_addr = htonl(INADDR_ANY); + server_config.sin_family = AF_INET; + server_config.sin_port = htons(UDP_SREVER_PORT); // 宏htons 用于将主机的无符号短整型数据转换成网络字节顺序(小端转大端) + + // 绑定端口 + int err = bind(sock, (struct sockaddr *)&server_config, sizeof(server_config)); + if (err < 0) + { + ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno); + } + ESP_LOGI(TAG, "Socket bound, port %d", UDP_SREVER_PORT); + + char rx_buffer[1024]; + // 接收数据,并发回 + while(1) + { + // 清空缓存 + memset(rx_buffer, 0, sizeof(rx_buffer)); + // 接收数据 + + struct sockaddr_in source_addr; + socklen_t socklen = sizeof(source_addr); + int len = recvfrom(sock, rx_buffer, sizeof(rx_buffer) - 1, 0, (struct sockaddr *)&source_addr, &socklen); + if (len < 0) + { + ESP_LOGE(TAG, "Error occurred during receiving: errno %d", errno); + break; + } + else + { + // 打印接收到的数据 + ESP_LOGI(TAG, "Received %d bytes: %s", len, rx_buffer); + // 发送数据 + int err = sendto(sock, rx_buffer, len, 0, (struct sockaddr *)&source_addr, sizeof(source_addr)); + if (err < 0) + { + ESP_LOGE(TAG, "Error occurred during sending: errno %d", errno); + break; + } + ESP_LOGI(TAG, "Send success"); + } + } + // 关闭socket + close(sock); + vTaskDelete(NULL); +} + +// wifi初始化 +static void wifi_sta_init(void) +{ + ESP_ERROR_CHECK(esp_netif_init()); + + // 注册事件(wifi启动成功) + ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_START, WIFI_CallBack, NULL, NULL)); + // 注册事件(wifi连接失败) + ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, WIFI_CallBack, NULL, NULL)); + // 注册事件(wifi连接失败) + ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, WIFI_CallBack, NULL, NULL)); + + // 初始化STA设备 + esp_netif_create_default_wifi_sta(); + + /*Initialize WiFi */ + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + // WIFI_INIT_CONFIG_DEFAULT 是一个默认配置的宏 + + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + //----------------配置阶段------------------- + // 初始化WIFI设备( 为 WiFi 驱动初始化 WiFi 分配资源,如 WiFi 控制结构、RX/TX 缓冲区、WiFi NVS 结构等,这个 WiFi 也启动 WiFi 任务。必须先调用此API,然后才能调用所有其他WiFi API) + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + + // STA详细配置 + wifi_config_t sta_config = { + .sta = { + .ssid = ESP_WIFI_STA_SSID, + .password = ESP_WIFI_STA_PASSWD, + .bssid_set = false, + }, + }; + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &sta_config)); + + //----------------启动阶段------------------- + ESP_ERROR_CHECK(esp_wifi_start()); + + //----------------配置省电模式------------------- + // 不省电(数据传输会更快) + ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE)); +} + +void app_main(void) +{ + // Initialize NVS + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) + { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + // 创建默认事件循环 + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + // 配置启动WIFI + wifi_sta_init(); + + // 创建TCP客户端任务 + xTaskCreate(udp_server_task, "udp_server_task", 4096, NULL, 5, NULL); +} + +``` + +效果: + +![](attachments/20240317155445.png) + + +# 参考链接 + +1. https://www.bilibili.com/video/BV1iG411j7Gq/?spm_id_from=333.788&vd_source=ef5a0ab0106372751602034cdd9ab98e \ No newline at end of file diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.2-UDP协议/attachments/20240317142714.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.2-UDP协议/attachments/20240317142714.png new file mode 100644 index 0000000..16f1f3a Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.2-UDP协议/attachments/20240317142714.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.2-UDP协议/attachments/20240317143536.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.2-UDP协议/attachments/20240317143536.png new file mode 100644 index 0000000..bcf48f0 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.2-UDP协议/attachments/20240317143536.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.2-UDP协议/attachments/20240317151255.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.2-UDP协议/attachments/20240317151255.png new file mode 100644 index 0000000..5cb2600 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.2-UDP协议/attachments/20240317151255.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.2-UDP协议/attachments/20240317155445.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.2-UDP协议/attachments/20240317155445.png new file mode 100644 index 0000000..755cbe4 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.2-UDP协议/attachments/20240317155445.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.2-UDP协议/attachments/20240317155554.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.2-UDP协议/attachments/20240317155554.png new file mode 100644 index 0000000..f31edf8 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.2-UDP协议/attachments/20240317155554.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3- HTTP协议/HTTP协议应用.md b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3- HTTP协议/HTTP协议应用.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(客户端)/HTTP协议(客户端).md b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(客户端)/HTTP协议(客户端).md new file mode 100644 index 0000000..068048d --- /dev/null +++ b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(客户端)/HTTP协议(客户端).md @@ -0,0 +1,451 @@ + + +# ESP32网络入门-HTTP协议 + + +### 一、概述 + +#### 1.1 HTTP协议介绍 + +`HTTP`协议(超文本传输协议HyperText Transfer Protocol),它是基于TCP协议的应用层传输协议,简单来说就是客户端和服务端进行数据传输的一种规则。 + +特点: + +- 支持客户/服务器模式 +- 简单快速:客户向服务器请求服务时,只需传送请求方法和路径,请求方法常用GET,HEAD,POST。每种方法规定了客户与服务器联系的类型不同,由于HTTP协议简单,是的HTTP服务器的程序规模小,因而通信速度很快。 +- 灵活:HTTP允许传输任意类型的数据对象,正在传输的类型有Content-Type加以标记。 +- 无连接:无连接的含义是限制每次连接只处理一个请求,服务器处理完客户的请求,并受到客户的应答后,即断开连接,采用这种方式可以节省传输时间。 +- 无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可以导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。 + + +#### 1.2 HTTP内容 + +>关于HTTP内容比较简单,这里建议直接阅读计算机网络有关部分,在此不再赘述。 + + +![](attachments/20240324115232.png) + +HTTP协议使用客户端/服务器模型。客户端向服务器发送HTTP请求,服务器接收到请求后,根据请求的内容进行一定的处理,并将处理结果封装在HTTP响应中返回给客户端。HTTP请求和响应都有一定的结构,通常包含三部分:起始行、首部和主体。 + +可以参考: + +>[前后端交互之 HTTP 协议](https://www.bilibili.com/video/BV1KV411o7u5/?spm_id_from=333.1007.0.0&vd_source=ef5a0ab0106372751602034cdd9ab98e) + + +我们在本节只需要知道发送HTTP请求的格式和接收的格式即可: + +##### 发送格式: + +![](attachments/20240324115334.png) + + +HTTP的起始行包含了请求方法或响应状态码等信息,如GET、POST等请求方法,200 OK、404 Not Found等响应状态码。HTTP的首部包含了请求或响应的各项属性,如Accept、Content-Type、Set-Cookie等。HTTP的主体则包含了请求或响应的具体内容,如HTML、JSON等文本数据或二进制数据。 + + + +##### 接收格式: +![](attachments/20240324115801.png) + + + +##### URL格式: +URL(Uniform Resource Locator)是用于定位互联网上资源的地址,常见的资源包括网页、图片、视频等。URL由多个部分组成,包括协议、主机名、端口号、路径和查询字符串等。 + +一个标准的URL格式如下: + +复制代码 + +``` +<协议>://<主机名>:<端口>/<路径>?<查询字符串> +``` + +其中,协议表示访问资源所采用的协议,如HTTP、HTTPS、FTP等;主机名表示资源所在的主机名或IP地址;端口号表示与主机通信的端口号,默认情况下使用协议默认的端口;路径表示请求的资源路径,可以是一个具体的文件路径,也可以是一个文件夹路径;查询字符串表示请求参数,以问号(?)开头,多个参数之间用&符号分隔。 + +例如,以下是一个标准的URL格式: + +复制代码 + +``` +https://www.example.com:80/index.html?id=123&name=test +``` + +其中,协议为HTTPS,[主机名为www.example.com](http://xn--www-c88de450g138a.example.com/),端口号为80(默认端口号可省略),路径为/index.html,查询字符串为id=123&name=test。 + +> 请确保明白上面的内容,再开始学习ESP32的HTTP协议使用。 + +### 二、使用 + +下面我们使用ESP32充当浏览器,向HTTP服务器发送Request请求。 + +#### 2.1 整体介绍 + +`esp_http_client` 提供了一组 API,用于从 ESP-IDF 应用程序中发起 HTTP/S 请求,具体的使用步骤如下: + +- 首先调用 [`esp_http_client_init()`](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-reference/protocols/esp_http_client.html#_CPPv420esp_http_client_initPK24esp_http_client_config_t "esp_http_client_init"),创建一个 [`esp_http_client_handle_t`](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-reference/protocols/esp_http_client.html#_CPPv424esp_http_client_handle_t "esp_http_client_handle_t") 实例,即基于给定的 [`esp_http_client_config_t`](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-reference/protocols/esp_http_client.html#_CPPv424esp_http_client_config_t "esp_http_client_config_t") 配置创建 HTTP 客户端句柄。此函数必须第一个被调用。若用户未明确定义参数的配置值,则使用默认值。 + +- 其次调用 [`esp_http_client_perform()`](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-reference/protocols/esp_http_client.html#_CPPv423esp_http_client_perform24esp_http_client_handle_t "esp_http_client_perform"),执行 `esp_http_client` 的所有操作,包括打开连接、交换数据、关闭连接(如需要),同时在当前任务完成前阻塞该任务。所有相关的事件(在 [`esp_http_client_config_t`](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-reference/protocols/esp_http_client.html#_CPPv424esp_http_client_config_t "esp_http_client_config_t") 中指定)将通过事件处理程序被调用。 + +- 最后调用 [`esp_http_client_cleanup()`](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-reference/protocols/esp_http_client.html#_CPPv423esp_http_client_cleanup24esp_http_client_handle_t "esp_http_client_cleanup") 来关闭连接(如有),并释放所有分配给 HTTP 客户端实例的内存。此函数必须在操作完成后最后一个被调用。 + +#### 2.2 详细步骤 + +首先我们要初始化WIFI,连接WIFI,这是编写HTTP程序的基础,连接WIFI在此不再赘述。 + +>后面的部分,默认已经连接好网络. + +##### 2.2.1 创建`esp_http_client` 实例 + +这里要使用[`esp_http_client_init()`](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-reference/protocols/esp_http_client.html#_CPPv420esp_http_client_initPK24esp_http_client_config_t "esp_http_client_init")创建HTTP客户端句柄实例,这个函数是 ESP32/ESP-IDF 中用于初始化 HTTP 客户端会话的函数。它接受一个指向 `esp_http_client_config_t` 结构的指针作为参数,该结构包含了 HTTP 客户端的配置信息。函数返回一个 esp_http_client_handle_t 类型的句柄,需要将这个句柄作为其他接口函数的输入参数来使用。在调用这个函数之后,必须在操作完成后调用 esp_http_client_cleanup 函数来清理资源。 + +关于`esp_http_client_config_t` 结构的参数如下: + +| 成员 | 描述 | +| --------------------------- | -------------------------------------------------------------------------- | +| url | HTTP URL,如果设置了该字段,则会覆盖其他字段 | +| host | 域名或 IP 地址,以字符串表示 | +| port | 连接的端口,默认取决于 `esp_http_client_transport_t`(80 或 443) | +| username | HTTP 身份验证的用户名 | +| password | HTTP 身份验证的密码 | +| auth_type | HTTP 身份验证类型,参见 `esp_http_client_auth_type_t` | +| path | HTTP 路径,默认为 `/` 如果未设置 | +| query | HTTP 查询参数 | +| cert_pem | SSL 服务器证书,PEM 格式的字符串,如果客户端需要验证服务器 | +| cert_len | `cert_pem` 缓冲区的长度。对于以 null 结尾的 PEM,可能为 0 | +| client_cert_pem | SSL 客户端证书,PEM 格式的字符串,如果服务器需要验证客户端 | +| client_cert_len | `client_cert_pem` 缓冲区的长度。对于以 null 结尾的 PEM,可能为 0 | +| client_key_pem | SSL 客户端私钥,PEM 格式的字符串,如果服务器需要验证客户端 | +| client_key_len | `client_key_pem` 缓冲区的长度。对于以 null 结尾的 PEM,可能为 0 | +| client_key_password | 客户端密钥解密密码字符串 | +| client_key_password_len | `client_key_password` 字符串的长度 | +| tls_version | 连接的 TLS 协议版本,例如 TLS 1.2、TLS 1.3(默认 - 无偏好) | +| user_agent | 发送 HTTP 请求时的用户代理字符串 | +| method | HTTP 方法 | +| timeout_ms | 网络超时时间(毫秒) | +| disable_auto_redirect | 禁用 HTTP 自动重定向 | +| max_redirection_count | 在接收到 HTTP 重定向状态码时的最大重定向次数,如果为零,则使用默认值 | +| max_authorization_retries | 在接收到 HTTP 未经授权状态码时的最大连接重试次数,如果为零,则使用默认值。如果为 -1,则禁用授权重试 | +| event_handler | HTTP 事件处理器 | +| transport_type | HTTP 传输类型,参见 `esp_http_client_transport_t` | +| buffer_size | HTTP 接收缓冲区大小 | +| buffer_size_tx | HTTP 发送缓冲区大小 | +| user_data | HTTP 用户数据上下文 | +| is_async | 设置异步模式,目前仅支持 HTTPS | +| use_global_ca_store | 使用全局 CA 存储 | +| skip_cert_common_name_check | 跳过对服务器证书 CN 字段的验证 | +| common_name | 指向包含服务器证书公共名称的字符串的指针。如果非 NULL,则服务器证书 CN 必须匹配此名称;如果为 NULL,则服务器证书 CN 必须匹配主机名 | +| crt_bundle_attach | 指向 esp_crt_bundle_attach 函数的函数指针。启用证书包以进行服务器验证,必须在 menuconfig 中启用 | +| keep_alive_enable | 启用 keep-alive 超时 | +| keep_alive_idle | keep-alive 空闲时间,默认为 5 秒 | +| keep_alive_interval | keep-alive 间隔时间,默认为 5 秒 | +| keep_alive_count | keep-alive 数据包重试发送计数,默认为 3 次 | +| if_name | 数据通过的接口名称。如果不设置,则使用默认接口 | + +示例代码: + +```c +// 初始化HTTP客户端 +char local_response_buffer[MAX_HTTP_OUTPUT_BUFFER + 1] = {0}; +esp_http_client_config_t config = { + .host = CONFIG_EXAMPLE_HTTP_ENDPOINT, + .path = "/get", + .query = "esp", + .event_handler = _http_event_handler, + .user_data = local_response_buffer, // Pass address of local buffer to get response + .disable_auto_redirect = true, +}; +esp_http_client_handle_t client = esp_http_client_init(&config); +``` + +解释: + +1. `char local_response_buffer[MAX_HTTP_OUTPUT_BUFFER + 1] = {0};`:这一行定义了一个字符数组 `local_response_buffer`,用来存储 HTTP 响应内容。`MAX_HTTP_OUTPUT_BUFFER` 是预先定义的常量,表示了 buffer 的最大大小。 + +2. `esp_http_client_config_t config = { ... };`:这里创建了一个 `esp_http_client_config_t` 结构体变量 `config`,并使用大括号内的初始化列表对其进行初始化。初始化列表中的字段包括: + - `host`:设置为宏 `CONFIG_EXAMPLE_HTTP_ENDPOINT`,表示 HTTP 请求的目标主机。 + - `path`:设置为 `"/get"`,表示 HTTP 请求的路径。 + - `query`:设置为 `"esp"`,表示 HTTP 请求的查询参数。 + - `event_handler`:设置为 `_http_event_handler`,这是一个处理 HTTP 事件的回调函数。 + - `user_data`:设置为 `local_response_buffer` 的地址,以便在 HTTP 请求完成后将响应内容存储到 `local_response_buffer` 中。 + - `disable_auto_redirect`:设置为 `true`,禁用 HTTP 的自动重定向功能。 +3. `esp_http_client_handle_t client = esp_http_client_init(&config);`:这一行调用了 `esp_http_client_init` 函数来初始化一个 HTTP 客户端,并将上述配置传递给该函数。函数返回一个 `esp_http_client_handle_t` 类型的客户端句柄,以供后续的 HTTP 请求使用。 + +##### 2.2.2 执行HTTP客户端的各种操作 + +>具体使用就很简单了: + +###### GET请求 +esp_http_client_perform():esp_http_client需要使用init函数创建的参数。此函数执行esp_http_client的所有操作,从打开连接,发送数据,下载数据和关闭连接(如有必要)。所有相关事件都将在event_handle(由定义esp_http_client_config_t)中调用。此功能执行其工作并阻止当前任务,直到完成 + +```c +esp_err_t err = esp_http_client_perform(client); +if (err == ESP_OK) +{ + ESP_LOGI(TAG, "HTTP GET Status = %d, content_length = %" PRId64, + esp_http_client_get_status_code(client), + esp_http_client_get_content_length(client)); +} +else +{ + ESP_LOGE(TAG, "HTTP GET request failed: %s", esp_err_to_name(err)); +} +ESP_LOG_BUFFER_HEX(TAG, local_response_buffer, strlen(local_response_buffer)); + +``` + +上面这段代码执行了一个HTTP GET请求,并根据执行结果打印相应的日志信息。 +###### POST请求 + +```c +// POST +const char *post_data = "{\"field1\":\"value1\"}"; +esp_http_client_set_url(client, "http://" CONFIG_EXAMPLE_HTTP_ENDPOINT "/post"); +esp_http_client_set_method(client, HTTP_METHOD_POST); +esp_http_client_set_header(client, "Content-Type", "application/json"); +esp_http_client_set_post_field(client, post_data, strlen(post_data)); +err = esp_http_client_perform(client); +if (err == ESP_OK) +{ + ESP_LOGI(TAG, "HTTP POST Status = %d, content_length = %" PRId64, + esp_http_client_get_status_code(client), + esp_http_client_get_content_length(client)); +} +else +{ + ESP_LOGE(TAG, "HTTP POST request failed: %s", esp_err_to_name(err)); +} +``` + + +##### 2.2.3 关闭链接,并释放系统资源 + +```c +esp_http_client_cleanup(client); +``` + +完成**esp_http_client的**任务后,这是最后一个要调用的函数。它将关闭连接(如果有)并释放分配给HTTP客户端的所有内存 + + +### 三、案例 + +> 使用HTTP get请求获取我的博客首页 + +```c + +#include +#include +#include +#include +#include "esp_log.h" +#include "nvs_flash.h" +#include "esp_event.h" +#include "esp_netif.h" +#include "esp_tls.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_system.h" +#include "esp_wifi.h" +#include "esp_mac.h" +#include +#include "esp_http_client.h" + +// 要连接的WIFI +#define ESP_WIFI_STA_SSID "duruofu_win10" +#define ESP_WIFI_STA_PASSWD "1234567890" +static const char *TAG = "main"; + +// 最大http输出缓冲区 +#define MAX_HTTP_OUTPUT_BUFFER 2048 + +// WIFI事件回调 +void WIFI_CallBack(void *event_handler_arg, esp_event_base_t event_base, int32_t event_id, void *event_data) +{ + static uint8_t connect_count = 0; + // WIFI 启动成功 + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_START"); + ESP_ERROR_CHECK(esp_wifi_connect()); + } + // WIFI 连接失败 + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_DISCONNECTED"); + connect_count++; + if (connect_count < 6) + { + vTaskDelay(1000 / portTICK_PERIOD_MS); + ESP_ERROR_CHECK(esp_wifi_connect()); + } + else + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_DISCONNECTED 10 times"); + } + } + // WIFI 连接成功(获取到了IP) + if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_GOT_IP"); + ip_event_got_ip_t *info = (ip_event_got_ip_t *)event_data; + ESP_LOGI("WIFI_EVENT", "got ip:" IPSTR "", IP2STR(&info->ip_info.ip)); + } +} + +// 连接WIFI +static void wifi_sta_init(void) +{ + ESP_ERROR_CHECK(esp_netif_init()); + + // 注册事件(wifi启动成功) + ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_START, WIFI_CallBack, NULL, NULL)); + // 注册事件(wifi连接失败) + ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, WIFI_CallBack, NULL, NULL)); + // 注册事件(wifi连接失败) + ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, WIFI_CallBack, NULL, NULL)); + + // 初始化STA设备 + esp_netif_create_default_wifi_sta(); + + /*Initialize WiFi */ + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + // WIFI_INIT_CONFIG_DEFAULT 是一个默认配置的宏 + + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + //----------------配置阶段------------------- + // 初始化WIFI设备( 为 WiFi 驱动初始化 WiFi 分配资源,如 WiFi 控制结构、RX/TX 缓冲区、WiFi NVS 结构等,这个 WiFi 也启动 WiFi 任务。必须先调用此API,然后才能调用所有其他WiFi API) + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + + // STA详细配置 + wifi_config_t sta_config = { + .sta = { + .ssid = ESP_WIFI_STA_SSID, + .password = ESP_WIFI_STA_PASSWD, + .bssid_set = false, + }, + }; + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &sta_config)); + + //----------------启动阶段------------------- + ESP_ERROR_CHECK(esp_wifi_start()); + + //----------------配置省电模式------------------- + // 不省电(数据传输会更快) + ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE)); +} + + +// HTTP事件回调 +esp_err_t _http_event_handler(esp_http_client_event_t *evt) +{ + switch (evt->event_id) + { + case HTTP_EVENT_ERROR: // 错误事件 + ESP_LOGI(TAG, "HTTP_EVENT_ERROR"); + break; + case HTTP_EVENT_ON_CONNECTED: // 连接成功事件 + ESP_LOGI(TAG, "HTTP_EVENT_ON_CONNECTED"); + break; + case HTTP_EVENT_HEADER_SENT: // 发送头事件 + ESP_LOGI(TAG, "HTTP_EVENT_HEADER_SENT"); + break; + case HTTP_EVENT_ON_HEADER: // 接收头事件 + ESP_LOGI(TAG, "HTTP_EVENT_ON_HEADER"); + printf("%.*s", evt->data_len, (char *)evt->data); + break; + case HTTP_EVENT_ON_DATA: // 接收数据事件 + ESP_LOGI(TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len); + if (!esp_http_client_is_chunked_response(evt->client)) + { + printf("%.*s", evt->data_len, (char *)evt->data); + } + break; + case HTTP_EVENT_ON_FINISH: // 会话完成事件 + ESP_LOGI(TAG, "HTTP_EVENT_ON_FINISH"); + break; + case HTTP_EVENT_DISCONNECTED: // 断开事件 + ESP_LOGI(TAG, "HTTP_EVENT_DISCONNECTED"); + break; + case HTTP_EVENT_REDIRECT: + ESP_LOGD(TAG, "HTTP_EVENT_REDIRECT"); + } + return ESP_OK; +} + + +// HTTP客户端任务 +static void http_client_task(void) +{ + // 等待wifi连接成功(暂时这样处理) + vTaskDelay(5000 / portTICK_PERIOD_MS); + ESP_LOGI("http_client_task", "http_client_task start"); + + // 初始化HTTP客户端 + char local_response_buffer[MAX_HTTP_OUTPUT_BUFFER + 1] = {0}; + + // 配置HTTP客户端目标 + esp_http_client_config_t config = { + //.method = HTTP_METHOD_GET, // get请求 + .url = "http://www.duruofu.top", // 请求url + .event_handler = _http_event_handler, // 事件回调 + .user_data = local_response_buffer, // Pass address of local buffer to get response + }; + esp_http_client_handle_t client = esp_http_client_init(&config); + + // GET + esp_http_client_set_method(client, HTTP_METHOD_GET); + esp_err_t err = esp_http_client_perform(client); + if (err == ESP_OK) + { + ESP_LOGI(TAG, "HTTP GET Status = %d, content_length = %" PRId64, + esp_http_client_get_status_code(client), + esp_http_client_get_content_length(client)); + } + else + { + ESP_LOGE(TAG, "HTTP GET request failed: %s", esp_err_to_name(err)); + } + ESP_LOG_BUFFER_HEX(TAG, local_response_buffer, strlen(local_response_buffer)); + + esp_http_client_cleanup(client); + + vTaskDelete(NULL); +} + +void app_main(void) +{ + // Initialize NVS + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) + { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + // 创建默认事件循环 + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + // 配置启动WIFI + wifi_sta_init(); + + // 创建HTTP客户端任务 + xTaskCreate(http_client_task, "http_client_task", 1024*8, NULL, 5, NULL); +} + + +``` + +效果: +成功获取到了博客网站的内容: + +![](attachments/20240325151411.png) + + +内容一致: + +![](attachments/20240325151513.png) + +# 参考链接 + +1. https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-reference/protocols/esp_http_client.html#_CPPv424esp_http_client_config_t +2. diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(客户端)/attachments/20240324115232.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(客户端)/attachments/20240324115232.png new file mode 100644 index 0000000..db23c6a Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(客户端)/attachments/20240324115232.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(客户端)/attachments/20240324115334.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(客户端)/attachments/20240324115334.png new file mode 100644 index 0000000..30d98bb Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(客户端)/attachments/20240324115334.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(客户端)/attachments/20240324115801.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(客户端)/attachments/20240324115801.png new file mode 100644 index 0000000..c68b749 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(客户端)/attachments/20240324115801.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(客户端)/attachments/20240325151411.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(客户端)/attachments/20240325151411.png new file mode 100644 index 0000000..1638b34 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(客户端)/attachments/20240325151411.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(客户端)/attachments/20240325151513.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(客户端)/attachments/20240325151513.png new file mode 100644 index 0000000..b0ea0b7 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(客户端)/attachments/20240325151513.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/HTTP协议(服务端).md b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/HTTP协议(服务端).md new file mode 100644 index 0000000..fa0f97a --- /dev/null +++ b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/HTTP协议(服务端).md @@ -0,0 +1,907 @@ + + +# ESP32网络入门-HTTP协议(服务端) + +## 一、介绍 + +>关于HTTP协议的内容在上一节教程里提过,在此不再赘述。 + +ESP-IDF的HTTP Server 组件提供了在 ESP32 上运行轻量级 Web 服务器的功能,下面介绍使用 HTTP Server 组件 API 的详细步骤: + +> - [`httpd_start()`](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-reference/protocols/esp_http_server.html#_CPPv411httpd_startP14httpd_handle_tPK14httpd_config_t "httpd_start"): 创建 HTTP 服务器的实例,根据具体的配置为其分配内存和资源,并返回该服务器实例的句柄。服务器使用了两个套接字,一个用来监听 HTTP 流量(TCP 类型),另一个用来处理控制信号(UDP 类型),它们在服务器的任务循环中轮流使用。通过向 `httpd_start()` 传递 `httpd_config_t` 结构体,可以在创建服务器实例时配置任务的优先级和堆栈的大小。TCP 流量被解析为 HTTP 请求,根据请求的 URI 来调用用户注册的处理程序,在处理程序中需要发送回 HTTP 响应数据包。 +> +> - [`httpd_stop()`](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-reference/protocols/esp_http_server.html#_CPPv410httpd_stop14httpd_handle_t "httpd_stop"): 根据传入的句柄停止服务器,并释放相关联的内存和资源。这是一个阻塞函数,首先给服务器任务发送停止信号,然后等待其终止。期间服务器任务会关闭所有已打开的连接,删除已注册的 URI 处理程序,并将所有会话的上下文数据重置为空。 +> +> - [`httpd_register_uri_handler()`](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-reference/protocols/esp_http_server.html#_CPPv426httpd_register_uri_handler14httpd_handle_tPK11httpd_uri_t "httpd_register_uri_handler"): 通过传入 `httpd_uri_t` 结构体类型的对象来注册 URI 处理程序。该结构体包含如下成员:`uri` 名字,`method` 类型(比如 `HTTPD_GET/HTTPD_POST/HTTPD_PUT` 等等), `esp_err_t *handler (httpd_req_t *req)` 类型的函数指针,指向用户上下文数据的 `user_ctx` 指针。 +> +## 二、使用 + +首先我们要初始化WIFI,连接WIFI,这是编写HTTP程序的基础,连接WIFI在此不再赘述。 + +>后面的部分,默认已经连接好网络. + +### 一、创建 HTTP 服务器的实例 + +```c +/* 启动 Web 服务器的函数 */ +httpd_handle_t start_webserver(void) +{ + /* 生成默认的配置参数 */ + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + + /* 置空 esp_http_server 的实例句柄 */ + httpd_handle_t server = NULL; + + /* 启动 httpd server */ + if (httpd_start(&server, &config) == ESP_OK) { + /* 注册 URI 处理程序 */ + httpd_register_uri_handler(server, &uri_get); + httpd_register_uri_handler(server, &uri_post); + } + /* 如果服务器启动失败,返回的句柄是 NULL */ + return server; +} + +``` + +这段代码是一个基于ESP32的HTTP服务器程序,通过调用httpd_start()函数启动HTTP服务器,并注册URI处理函数,来实现Web服务的功能。主要包括以下几个步骤: + +1. 初始化HTTPD配置,使用HTTPD_DEFAULT_CONFIG()函数可以获得默认的HTTPD配置对象,这里设置了LRU清理使能; + +2. 启动HTTPD服务器,通过调用httpd_start()函数启动HTTPD服务器,参数server指向创建后的HTTPD句柄,config包含HTTPD的配置信息; + +3. 注册URI处理函数,通过调用httpd_register_uri_handler()函数将URI处理函数添加到HTTP服务器的URI处理列表中,可以实现不同URI地址的访问和处理; + +4. 注册基本认证机制,如果开启了CONFIG_EXAMPLE_BASIC_AUTH宏定义,则通过httpd_register_basic_auth()函数在HTTPD服务器上注册基本的用户名密码验证机制。 + + +这里的uri_get、uri_post、都是处理URI请求的处理函数,分别对应不同的URI地址。httpd_handle_t类型是HTTPD服务器的句柄类型,可以用于维护HTTPD服务器的状态等信息. + +### 二、注册 URI 处理程序 + +下面对上面提到的路由处理函数进行分析: + +#### get方法 + +注册get路由: + +```c +/* 注册路由 */ +static const httpd_uri_t hello = { + .uri = "/hello", + .method = HTTP_GET, + .handler = hello_get_handler, + /* Let's pass response string in user + * context to demonstrate it's usage */ + .user_ctx = "Hello World!"}; +``` + +这段代码是一个基于ESP32的HTTP服务器程序中的URI处理函数,对应的URI地址为/hello。具体来说: + +1. .uri表示了要处理的URI地址,即当客户端请求该地址时,调用相应的处理函数进行处理; +2. .method表示了该URI地址所支持的HTTP请求方法,本例中为HTTP_GET,即只支持GET请求; +3. .handler表示了处理函数,当客户端发起请求时,调用该处理函数进行响应; +4. .user_ctx是一个指向用户数据的指针,允许在处理函数中使用该指针来存储一些用户自定义的数据,这里将"Hello World!"传递给处理函数作为响应内容。 + +在这个例子中,当客户端通过GET方法请求URI地址/hello时,会调用hello_get_handler函数进行处理,函数的实现可以根据需求自行编写。 + +```c +/* URI 处理函数,在客户端发起 GET /hello 请求时被调用 */ + +/* An HTTP GET handler */ +static esp_err_t hello_get_handler(httpd_req_t *req) +{ + char *buf; + size_t buf_len; + + /* Get header value string length and allocate memory for length + 1, + * extra byte for null termination */ + buf_len = httpd_req_get_hdr_value_len(req, "Host") + 1; + if (buf_len > 1) + { + buf = malloc(buf_len); + ESP_RETURN_ON_FALSE(buf, ESP_ERR_NO_MEM, TAG, "buffer alloc failed"); + /* Copy null terminated value string into buffer */ + if (httpd_req_get_hdr_value_str(req, "Host", buf, buf_len) == ESP_OK) + { + ESP_LOGI(TAG, "Found header => Host: %s", buf); + } + free(buf); + } + + buf_len = httpd_req_get_hdr_value_len(req, "Test-Header-2") + 1; + if (buf_len > 1) + { + buf = malloc(buf_len); + ESP_RETURN_ON_FALSE(buf, ESP_ERR_NO_MEM, TAG, "buffer alloc failed"); + if (httpd_req_get_hdr_value_str(req, "Test-Header-2", buf, buf_len) == ESP_OK) + { + ESP_LOGI(TAG, "Found header => Test-Header-2: %s", buf); + } + free(buf); + } + + buf_len = httpd_req_get_hdr_value_len(req, "Test-Header-1") + 1; + if (buf_len > 1) + { + buf = malloc(buf_len); + ESP_RETURN_ON_FALSE(buf, ESP_ERR_NO_MEM, TAG, "buffer alloc failed"); + if (httpd_req_get_hdr_value_str(req, "Test-Header-1", buf, buf_len) == ESP_OK) + { + ESP_LOGI(TAG, "Found header => Test-Header-1: %s", buf); + } + free(buf); + } + + /* Read URL query string length and allocate memory for length + 1, + * extra byte for null termination */ + buf_len = httpd_req_get_url_query_len(req) + 1; + if (buf_len > 1) + { + buf = malloc(buf_len); + ESP_RETURN_ON_FALSE(buf, ESP_ERR_NO_MEM, TAG, "buffer alloc failed"); + if (httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) + { + ESP_LOGI(TAG, "Found URL query => %s", buf); + char param[EXAMPLE_HTTP_QUERY_KEY_MAX_LEN], dec_param[EXAMPLE_HTTP_QUERY_KEY_MAX_LEN] = {0}; + /* Get value of expected key from query string */ + if (httpd_query_key_value(buf, "query1", param, sizeof(param)) == ESP_OK) + { + ESP_LOGI(TAG, "Found URL query parameter => query1=%s", param); + example_uri_decode(dec_param, param, strnlen(param, EXAMPLE_HTTP_QUERY_KEY_MAX_LEN)); + ESP_LOGI(TAG, "Decoded query parameter => %s", dec_param); + } + if (httpd_query_key_value(buf, "query3", param, sizeof(param)) == ESP_OK) + { + ESP_LOGI(TAG, "Found URL query parameter => query3=%s", param); + example_uri_decode(dec_param, param, strnlen(param, EXAMPLE_HTTP_QUERY_KEY_MAX_LEN)); + ESP_LOGI(TAG, "Decoded query parameter => %s", dec_param); + } + if (httpd_query_key_value(buf, "query2", param, sizeof(param)) == ESP_OK) + { + ESP_LOGI(TAG, "Found URL query parameter => query2=%s", param); + example_uri_decode(dec_param, param, strnlen(param, EXAMPLE_HTTP_QUERY_KEY_MAX_LEN)); + ESP_LOGI(TAG, "Decoded query parameter => %s", dec_param); + } + } + free(buf); + } + + /* Set some custom headers */ + httpd_resp_set_hdr(req, "Custom-Header-1", "Custom-Value-1"); + httpd_resp_set_hdr(req, "Custom-Header-2", "Custom-Value-2"); + + /* Send response with custom headers and body set as the + * string passed in user context*/ + const char *resp_str = (const char *)req->user_ctx; + httpd_resp_send(req, resp_str, HTTPD_RESP_USE_STRLEN); + + /* After sending the HTTP response the old HTTP request + * headers are lost. Check if HTTP request headers can be read now. */ + if (httpd_req_get_hdr_value_len(req, "Host") == 0) + { + ESP_LOGI(TAG, "Request headers lost"); + } + return ESP_OK; +} + + +``` +上面这段代码是ESP32的HTTP服务器程序中的处理GET请求的函数,对应的URI地址为/hello。具体来说: + +1. 首先定义了一个名为hello_get_handler的函数,该函数接收一个指向httpd_req_t类型的指针参数req,表示HTTP请求的结构体实例; +2. 函数中通过httpd_req_get_hdr_value_len和httpd_req_get_hdr_value_str等函数获取HTTP请求头中特定字段的值,比如Host、Test-Header-1、Test-Header-2等; +3. 接着通过httpd_req_get_url_query_len和httpd_req_get_url_query_str等函数获取URL查询字符串,并从中解析出特定键值对的值,比如query1、query2、query3等; +4. 通过httpd_resp_set_hdr设置一些自定义的响应头; +5. 最后调用httpd_resp_send发送HTTP响应,响应内容为req->user_ctx所指向的字符串,即在URI处理函数中传入的"Hello World!"。 + +>代码很多,但实际并不复杂运行效果如下: + +![](attachments/Pasted%20image%2020240325174743.png) + +![](attachments/Pasted%20image%2020240325174900.png) + +get参数解析: + +![](attachments/Pasted%20image%2020240325175024.png) + +![](attachments/Pasted%20image%2020240325175043.png) + + +我们还可以使用get返回一个网页,用于数据交互: + +新增一个HTML网页的GET路由: + +```c +const httpd_uri_t html = { + .uri = "/html", + .method = HTTP_GET, + .handler = html_get_handler, + .user_ctx = NULL}; +``` + +注册: +```c +    httpd_register_uri_handler(server, &html); +``` + +`html_get_handler`函数: + +```c +//定义HTML页面 +char form_html[] = R"( + + + +ESP32 Web Server + + +

ESP32 Web Server

+
+
+
+
+

+ +
+ + +)"; + +static esp_err_t html_get_handler(httpd_req_t *req) +{ + // 指向 HTML 页面字符串的指针 + const char *html_content = form_html; + + // 设置 Content-Type 头 + httpd_resp_set_type(req, "text/html"); + + // 发送 HTML 页面作为 HTTP 响应的正文部分 + httpd_resp_send(req, html_content, strlen(html_content)); + + return ESP_OK; +} + +``` + +访问效果如图所示: + +![](attachments/Pasted%20image%2020240326085636.png) + +#### POST方法 + +我们接着刚才写好的表单,发起一个psot请求 +![](attachments/Pasted%20image%2020240326090740.png) + +这里的URL的IP需要填写自己ESP32联网分配到的IP。这样我们点击提交表单就可以直接发起POST请求。 + +注册post路由: + +```c +const httpd_uri_t echo = { + .uri = "/echo", + .method = HTTP_POST, + .handler = echo_post_handler, + .user_ctx = NULL}; +``` + +post处理方法: + +```c +/* An HTTP POST handler */ +static esp_err_t echo_post_handler(httpd_req_t *req) +{ + /* 定义 HTTP POST 请求数据的目标缓存区 + * httpd_req_recv() 只接收 char* 数据,但也可以是 + * 任意二进制数据(需要类型转换) + * 对于字符串数据,null 终止符会被省略, + * content_len 会给出字符串的长度 */ + char buf[100]; + int ret, remaining = req->content_len; + + while (remaining > 0) + { + /* Read the data for the request */ + if ((ret = httpd_req_recv(req, buf, + MIN(remaining, sizeof(buf)))) <= 0) + { + if (ret == HTTPD_SOCK_ERR_TIMEOUT) + { + /* Retry receiving if timeout occurred */ + continue; + } + /* 如果发生了错误,返回 ESP_FAIL 可以确保 + * 底层套接字被关闭 */ + return ESP_FAIL; + } + + /* Send back the same data */ + httpd_resp_send_chunk(req, buf, ret); + remaining -= ret; + + /* Log data received */ + ESP_LOGI(TAG, "=========== RECEIVED DATA =========="); + ESP_LOGI(TAG, "%.*s", ret, buf); + ESP_LOGI(TAG, "===================================="); + } + + // End response + httpd_resp_send_chunk(req, NULL, 0); + return ESP_OK; +} +``` + +效果如下: + +在表单填写内容: + +![](attachments/Pasted%20image%2020240326091157.png) + +提交: + +![](attachments/Pasted%20image%2020240326091216.png) + +数据回显,POST请求验证完成。 + +>PS:如果出现下面的报错:Header fields are too long +>![](attachments/Pasted%20image%2020240326091539.png) +>需要在配置文件里修改: +>![](attachments/Pasted%20image%2020240326091652.png) + +#### PUT方法 + +>put用于修改数据 + +注册路由: + +```c +const httpd_uri_t ctrl = { + .uri = "/ctrl", + .method = HTTP_PUT, + .handler = ctrl_put_handler, + .user_ctx = NULL}; + +``` + +路由处理: + +```c +esp_err_t ctrl_put_handler(httpd_req_t *req) +{ + char buf; + int ret; + + if ((ret = httpd_req_recv(req, &buf, 1)) <= 0) + { + if (ret == HTTPD_SOCK_ERR_TIMEOUT) + { + httpd_resp_send_408(req); + } + return ESP_FAIL; + } + + if (buf == '0') + { + /* URI handlers can be unregistered using the uri string */ + ESP_LOGI(TAG, "Unregistering /hello and /echo URIs"); + httpd_unregister_uri(req->handle, "/hello"); + httpd_unregister_uri(req->handle, "/echo"); + /* Register the custom error handler */ + httpd_register_err_handler(req->handle, HTTPD_404_NOT_FOUND, http_404_error_handler); + } + else + { + ESP_LOGI(TAG, "Registering /hello and /echo URIs"); + httpd_register_uri_handler(req->handle, &hello); + httpd_register_uri_handler(req->handle, &echo); + /* Unregister custom error handler */ + httpd_register_err_handler(req->handle, HTTPD_404_NOT_FOUND, NULL); + } + + /* Respond with empty body */ + httpd_resp_send(req, NULL, 0); + return ESP_OK; +} + + +``` + +1. 从请求中接收一个字节的数据到 `buf` 中。 +2. 如果接收失败或超时,则返回 HTTP 408 请求超时错误。 +3. 如果接收成功且接收的数据为 '0',则取消注册 "/hello" 和 "/echo" 的 URI 处理器,并注册自定义的 404 错误处理函数 `http_404_error_handler`。 +4. 如果接收的数据不为 '0',则注册 "/hello" 和 "/echo" 的 URI 处理器,并取消注册自定义的 404 错误处理函数。 +5. 最后,回复空的 HTTP 响应体并返回 ESP_OK。 + + +测试效果: + +使用下面的js代码在浏览器控制台发送PUT请求: +```js +var xhr = new XMLHttpRequest(); +xhr.open("PUT","http://192.168.137.55/ctrl",true); +xhr.send("0"); +``` + +![](attachments/Pasted%20image%2020240326094235.png) + +![](attachments/Pasted%20image%2020240326094304.png) + + +这时hello路由就被取消注册了: + +![](attachments/Pasted%20image%2020240326094552.png) + + +发送1,hello被重新注册: +![](attachments/Pasted%20image%2020240326094621.png) + + +### 三、停止服务器,释放资源 + +```c +/* 停止 Web 服务器的函数 */ +void stop_webserver(httpd_handle_t server) +{ + if (server) { + /* 停止 httpd server */ + httpd_stop(server); + } +} +``` + +## 三、示例 + + +>下面是本节全部代码,效果上面展示过,就不再演示 + +```c +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "esp_wifi.h" +#include "esp_log.h" +#include "esp_event.h" +#include +#include "nvs_flash.h" +#include "esp_mac.h" +#include "esp_netif.h" +#include +#include "esp_tls_crypto.h" +#include +#include "esp_tls.h" +#include "esp_check.h" +#include +#include +#include +#include +#include +#include +#include "esp_netif.h" +#include "esp_tls_crypto.h" +#include +#include "esp_event.h" +#include "esp_netif.h" +#include "esp_tls.h" +#include "esp_check.h" + +// 要连接的WIFI +#define ESP_WIFI_STA_SSID "duruofu_win10" +#define ESP_WIFI_STA_PASSWD "1234567890" + +static const char *TAG = "main"; + +#define EXAMPLE_HTTP_QUERY_KEY_MAX_LEN (64) + +char form_html[] = R"( + + + +ESP32 Web Server + + +

ESP32 Web Server

+
+
+
+
+

+ +
+ +)"; + +// WIFI回调 +void WIFI_CallBack(void *event_handler_arg, esp_event_base_t event_base, int32_t event_id, void *event_data) +{ + static uint8_t connect_count = 0; + // WIFI 启动成功 + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_START"); + ESP_ERROR_CHECK(esp_wifi_connect()); + } + // WIFI 连接失败 + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_DISCONNECTED"); + connect_count++; + if (connect_count < 6) + { + vTaskDelay(1000 / portTICK_PERIOD_MS); + ESP_ERROR_CHECK(esp_wifi_connect()); + } + else + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_DISCONNECTED 10 times"); + } + } + // WIFI 连接成功(获取到了IP) + if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_GOT_IP"); + ip_event_got_ip_t *info = (ip_event_got_ip_t *)event_data; + ESP_LOGI("WIFI_EVENT", "got ip:" IPSTR "", IP2STR(&info->ip_info.ip)); + } +} + +// wifi初始化 +static void wifi_sta_init(void) +{ + ESP_ERROR_CHECK(esp_netif_init()); + + // 注册事件(wifi启动成功) + ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_START, WIFI_CallBack, NULL, NULL)); + // 注册事件(wifi连接失败) + ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, WIFI_CallBack, NULL, NULL)); + // 注册事件(wifi连接失败) + ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, WIFI_CallBack, NULL, NULL)); + + // 初始化STA设备 + esp_netif_create_default_wifi_sta(); + + /*Initialize WiFi */ + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + // WIFI_INIT_CONFIG_DEFAULT 是一个默认配置的宏 + + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + //----------------配置阶段------------------- + // 初始化WIFI设备( 为 WiFi 驱动初始化 WiFi 分配资源,如 WiFi 控制结构、RX/TX 缓冲区、WiFi NVS 结构等,这个 WiFi 也启动 WiFi 任务。必须先调用此API,然后才能调用所有其他WiFi API) + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + + // STA详细配置 + wifi_config_t sta_config = { + .sta = { + .ssid = ESP_WIFI_STA_SSID, + .password = ESP_WIFI_STA_PASSWD, + .bssid_set = false, + }, + }; + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &sta_config)); + + //----------------启动阶段------------------- + ESP_ERROR_CHECK(esp_wifi_start()); + + //----------------配置省电模式------------------- + // 不省电(数据传输会更快) + ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE)); +} + + +/*路由处理程序*/ + +/* An HTTP GET handler */ +static esp_err_t hello_get_handler(httpd_req_t *req) +{ + char *buf; + size_t buf_len; + + /* Get header value string length and allocate memory for length + 1, + * extra byte for null termination */ + buf_len = httpd_req_get_hdr_value_len(req, "Host") + 1; + if (buf_len > 1) + { + buf = malloc(buf_len); + ESP_RETURN_ON_FALSE(buf, ESP_ERR_NO_MEM, TAG, "buffer alloc failed"); + /* Copy null terminated value string into buffer */ + if (httpd_req_get_hdr_value_str(req, "Host", buf, buf_len) == ESP_OK) + { + ESP_LOGI(TAG, "Found header => Host: %s", buf); + } + free(buf); + } + + buf_len = httpd_req_get_hdr_value_len(req, "Test-Header-2") + 1; + if (buf_len > 1) + { + buf = malloc(buf_len); + ESP_RETURN_ON_FALSE(buf, ESP_ERR_NO_MEM, TAG, "buffer alloc failed"); + if (httpd_req_get_hdr_value_str(req, "Test-Header-2", buf, buf_len) == ESP_OK) + { + ESP_LOGI(TAG, "Found header => Test-Header-2: %s", buf); + } + free(buf); + } + + buf_len = httpd_req_get_hdr_value_len(req, "Test-Header-1") + 1; + if (buf_len > 1) + { + buf = malloc(buf_len); + ESP_RETURN_ON_FALSE(buf, ESP_ERR_NO_MEM, TAG, "buffer alloc failed"); + if (httpd_req_get_hdr_value_str(req, "Test-Header-1", buf, buf_len) == ESP_OK) + { + ESP_LOGI(TAG, "Found header => Test-Header-1: %s", buf); + } + free(buf); + } + + /* Read URL query string length and allocate memory for length + 1, + * extra byte for null termination */ + buf_len = httpd_req_get_url_query_len(req) + 1; + if (buf_len > 1) + { + buf = malloc(buf_len); + ESP_RETURN_ON_FALSE(buf, ESP_ERR_NO_MEM, TAG, "buffer alloc failed"); + if (httpd_req_get_url_query_str(req, buf, buf_len) == ESP_OK) + { + ESP_LOGI(TAG, "Found URL query => %s", buf); + char param[EXAMPLE_HTTP_QUERY_KEY_MAX_LEN], dec_param[EXAMPLE_HTTP_QUERY_KEY_MAX_LEN] = {0}; + /* Get value of expected key from query string */ + if (httpd_query_key_value(buf, "query1", param, sizeof(param)) == ESP_OK) + { + ESP_LOGI(TAG, "Found URL query parameter => query1=%s", param); + //example_uri_decode(dec_param, param, strnlen(param, EXAMPLE_HTTP_QUERY_KEY_MAX_LEN)); + ESP_LOGI(TAG, "Decoded query parameter => %s", dec_param); + } + if (httpd_query_key_value(buf, "query2", param, sizeof(param)) == ESP_OK) + { + ESP_LOGI(TAG, "Found URL query parameter => query2=%s", param); + //example_uri_decode(dec_param, param, strnlen(param, EXAMPLE_HTTP_QUERY_KEY_MAX_LEN)); + ESP_LOGI(TAG, "Decoded query parameter => %s", dec_param); + } + if (httpd_query_key_value(buf, "query3", param, sizeof(param)) == ESP_OK) + { + ESP_LOGI(TAG, "Found URL query parameter => query3=%s", param); + //example_uri_decode(dec_param, param, strnlen(param, EXAMPLE_HTTP_QUERY_KEY_MAX_LEN)); + ESP_LOGI(TAG, "Decoded query parameter => %s", dec_param); + } + } + free(buf); + } + + /* Set some custom headers */ + httpd_resp_set_hdr(req, "Custom-Header-1", "Custom-Value-1"); + httpd_resp_set_hdr(req, "Custom-Header-2", "Custom-Value-2"); + + /* Send response with custom headers and body set as the + * string passed in user context*/ + const char *resp_str = (const char *)req->user_ctx; + httpd_resp_send(req, resp_str, HTTPD_RESP_USE_STRLEN); + + /* After sending the HTTP response the old HTTP request + * headers are lost. Check if HTTP request headers can be read now. */ + if (httpd_req_get_hdr_value_len(req, "Host") == 0) + { + ESP_LOGI(TAG, "Request headers lost"); + } + return ESP_OK; +} + +static esp_err_t html_get_handler(httpd_req_t *req) +{ + // 指向 HTML 页面字符串的指针 + const char *html_content = form_html; + + // 设置 Content-Type 头 + httpd_resp_set_type(req, "text/html"); + + // 发送 HTML 页面作为 HTTP 响应的正文部分 + httpd_resp_send(req, html_content, strlen(html_content)); + + return ESP_OK; +} + +/* An HTTP POST handler */ +static esp_err_t echo_post_handler(httpd_req_t *req) +{ + /* 定义 HTTP POST 请求数据的目标缓存区 + * httpd_req_recv() 只接收 char* 数据,但也可以是 + * 任意二进制数据(需要类型转换) + * 对于字符串数据,null 终止符会被省略, + * content_len 会给出字符串的长度 */ + char buf[100]; + int ret, remaining = req->content_len; + + while (remaining > 0) + { + /* Read the data for the request */ + if ((ret = httpd_req_recv(req, buf, + MIN(remaining, sizeof(buf)))) <= 0) + { + if (ret == HTTPD_SOCK_ERR_TIMEOUT) + { + /* Retry receiving if timeout occurred */ + continue; + } + /* 如果发生了错误,返回 ESP_FAIL 可以确保 + * 底层套接字被关闭 */ + return ESP_FAIL; + } + + /* Send back the same data */ + httpd_resp_send_chunk(req, buf, ret); + remaining -= ret; + + /* Log data received */ + ESP_LOGI(TAG, "=========== RECEIVED DATA =========="); + ESP_LOGI(TAG, "%.*s", ret, buf); + ESP_LOGI(TAG, "===================================="); + } + + // End response + httpd_resp_send_chunk(req, NULL, 0); + return ESP_OK; +} + +/* 注册路由 */ +const httpd_uri_t hello = { + .uri = "/hello", + .method = HTTP_GET, + .handler = hello_get_handler, + .user_ctx = "Hello World!"}; + +const httpd_uri_t html = { + .uri = "/html", + .method = HTTP_GET, + .handler = html_get_handler, + .user_ctx = NULL}; + +const httpd_uri_t echo = { + .uri = "/echo", + .method = HTTP_POST, + .handler = echo_post_handler, + .user_ctx = NULL}; + + +/* This handler allows the custom error handling functionality to be + * tested from client side. For that, when a PUT request 0 is sent to + * URI /ctrl, the /hello and /echo URIs are unregistered and following + * custom error handler http_404_error_handler() is registered. + * Afterwards, when /hello or /echo is requested, this custom error + * handler is invoked which, after sending an error message to client, + * either closes the underlying socket (when requested URI is /echo) + * or keeps it open (when requested URI is /hello). This allows the + * client to infer if the custom error handler is functioning as expected + * by observing the socket state. + */ +esp_err_t http_404_error_handler(httpd_req_t *req, httpd_err_code_t err) +{ + if (strcmp("/hello", req->uri) == 0) + { + httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "/hello URI is not available"); + /* Return ESP_OK to keep underlying socket open */ + return ESP_OK; + } + else if (strcmp("/echo", req->uri) == 0) + { + httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "/echo URI is not available"); + /* Return ESP_FAIL to close underlying socket */ + return ESP_FAIL; + } + /* For any other URI send 404 and close socket */ + httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Some 404 error message"); + return ESP_FAIL; +} + +/* An HTTP PUT handler. This demonstrates realtime + * registration and deregistration of URI handlers + */ +esp_err_t ctrl_put_handler(httpd_req_t *req) +{ + char buf; + int ret; + + if ((ret = httpd_req_recv(req, &buf, 1)) <= 0) + { + if (ret == HTTPD_SOCK_ERR_TIMEOUT) + { + httpd_resp_send_408(req); + } + return ESP_FAIL; + } + + if (buf == '0') + { + /* URI handlers can be unregistered using the uri string */ + ESP_LOGI(TAG, "Unregistering /hello and /echo URIs"); + httpd_unregister_uri(req->handle, "/hello"); + httpd_unregister_uri(req->handle, "/echo"); + /* Register the custom error handler */ + httpd_register_err_handler(req->handle, HTTPD_404_NOT_FOUND, http_404_error_handler); + } + else + { + ESP_LOGI(TAG, "Registering /hello and /echo URIs"); + httpd_register_uri_handler(req->handle, &hello); + httpd_register_uri_handler(req->handle, &echo); + /* Unregister custom error handler */ + httpd_register_err_handler(req->handle, HTTPD_404_NOT_FOUND, NULL); + } + + /* Respond with empty body */ + httpd_resp_send(req, NULL, 0); + return ESP_OK; +} + + +static const httpd_uri_t ctrl = { + .uri = "/ctrl", + .method = HTTP_PUT, + .handler = ctrl_put_handler, + .user_ctx = NULL}; + +/* 启动 Web 服务器的函数 */ +httpd_handle_t start_webserver(void) +{ + /* 生成默认的配置参数 */ + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + + /* 置空 esp_http_server 的实例句柄 */ + httpd_handle_t server = NULL; + + ESP_LOGI(TAG, "Starting server on port: '%d'", config.server_port); + /* 启动 httpd server */ + if (httpd_start(&server, &config) == ESP_OK) + { + /* 注册 URI 处理程序 */ + ESP_LOGI(TAG, "Registering URI handlers"); + httpd_register_uri_handler(server, &hello); + httpd_register_uri_handler(server, &html); + httpd_register_uri_handler(server, &echo); + httpd_register_uri_handler(server, &ctrl); + } + /* 如果服务器启动失败,返回的句柄是 NULL */ + return server; +} + + +void app_main(void) +{ + static httpd_handle_t server = NULL; + + // Initialize NVS + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) + { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + // 创建默认事件循环 + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + // 配置启动WIFI + wifi_sta_init(); + + vTaskDelay(3000 / portTICK_PERIOD_MS); + + /* Start the server for the first time */ + server = start_webserver(); + + while (server) + { + sleep(5); + } +} + + + +``` + + +# 参考链接 + +1. https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-reference/protocols/esp_http_server.html +2. https://space.bilibili.com/1338335828 \ No newline at end of file diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240325174743.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240325174743.png new file mode 100644 index 0000000..9fdb78f Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240325174743.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240325174900.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240325174900.png new file mode 100644 index 0000000..fcb2dd6 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240325174900.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240325175024.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240325175024.png new file mode 100644 index 0000000..1528ec8 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240325175024.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240325175043.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240325175043.png new file mode 100644 index 0000000..eaa0013 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240325175043.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240326085636.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240326085636.png new file mode 100644 index 0000000..593fcc8 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240326085636.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240326090740.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240326090740.png new file mode 100644 index 0000000..23b99ee Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240326090740.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240326091157.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240326091157.png new file mode 100644 index 0000000..89c7c39 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240326091157.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240326091216.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240326091216.png new file mode 100644 index 0000000..9e860a0 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240326091216.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240326091539.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240326091539.png new file mode 100644 index 0000000..e65c873 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240326091539.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240326091652.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240326091652.png new file mode 100644 index 0000000..6ec5bee Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240326091652.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240326094235.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240326094235.png new file mode 100644 index 0000000..47c3558 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240326094235.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240326094304.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240326094304.png new file mode 100644 index 0000000..deaa325 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240326094304.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240326094552.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240326094552.png new file mode 100644 index 0000000..833219f Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240326094552.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240326094621.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240326094621.png new file mode 100644 index 0000000..c6883d3 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.3-HTTP协议/HTTP协议(服务端)/attachments/20240326094621.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.4-WebSocket协议/WebSocket协议.md b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.4-WebSocket协议/WebSocket协议.md index e69de29..221e8fd 100644 --- a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.4-WebSocket协议/WebSocket协议.md +++ b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.4-WebSocket协议/WebSocket协议.md @@ -0,0 +1,1040 @@ + +# ESP32网络入门-WebSocket协议 + +## 一、介绍 + +### 1.1 WebSocket介绍 + +> 详细介绍可以参考: [万字长文,一篇吃透WebSocket:概念、原理、易错常识、动手实践](https://cloud.tencent.com/developer/article/1887095) + +WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它的主要特点是可以实现实时双向通信,并且可以在客户端和服务器之间传输任意数据,而不受 HTTP 语义限制。 + +![](attachments/20240326100058.png) + +与传统的 HTTP 请求-响应模式不同,WebSocket 采用了类似握手的方法来建立连接。客户端向服务器发送一个 HTTP 请求头,其中包含 Upgrade、Connection、Sec-WebSocket-Key 等字段,通知服务器要进行连接升级。服务器返回一个 HTTP 响应头,其中也包含 Upgrade、Connection 和 Sec-WebSocket-Accept 等字段,表示已同意升级连接。之后,客户端和服务器之间的数据就可以以帧为单位进行传输,每个帧包含一个信息片段,可以是文本也可以是二进制。 + +![](attachments/20240326100128.png) + +WebSocket 协议的优点是支持实时通信,具有较低的延迟,可减少网络传输的开销,有助于提高应用程序的性能。常见的应用场景包括聊天应用、在线游戏、实时数据监控等。 + +### webSocket数据帧格式 + +WebSocket 数据帧是 WebSocket 协议中传输数据的基本单位,每个帧可以包含一个或多个信息片段(Message Fragment),可以是文本也可以是二进制,由 Header 和 Payload 两部分组成。 + +![](attachments/20240326100219.png) + +Header 一般包括了以下几个字段: +1. FIN:1 位,表示当前帧是否为消息的最后一帧,值为 1 表示是,值为 0 表示不是。 +2. RSV1、RSV2、RSV3:各占 1 位,用于扩展协议。通常情况下,这三位都被置为 0。 +3. Opcode:4 位,用于表示消息类型,如 0x1 表示文本消息,0x2 表示二进制消息,0x8 表示关闭连接等。 +4. Mask:1 位,表示负载数据是否被掩码处理过,值为 1 表示被掩码处理,值为 0 表示未被掩码处理。 +5. Payload length:7 位或 7+16 位或 7+64 位,表示负载数据的长度。当值为 0~126 时,表示负载数据的长度就是该值;当值为 127 时,额外有两个字节表示长度,当值大于等于 2^16 时,额外有 4 个字节表示长度。 +如果 Mask 位被设置为 1,则 Header 中还要包含一个 4 字节的掩码码值(Masking Key),用于对负载数据进行反掩码操作。 +Payload 是实际的数据内容,长度由 Header 中的 Payload length 字段表示,可能是文本也可能是二进制。 +在整个 WebSocket 连接过程中,客户端和服务器会相互发送一些数据帧,包括握手请求、握手响应、消息片段等,根据不同的功能要求分别使用不同的 Opcode 来表示。 + +## 二、使用 + +### 2.1 服务端 + +下面我们参考:[ Websocket echo server](https://github.com/espressif/esp-idf/tree/be06a6f5ffe36f9554cfc91fe2036e0fc85fea60/examples/protocols/http_server/ws_echo_server)这个历程来学习WebSocket服务端程序 + +#### 2.1.1 创建服务实例 + +```c +static httpd_handle_t start_webserver(void) +{ + httpd_handle_t server = NULL; + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + + // Start the httpd server + ESP_LOGI(TAG, "Starting server on port: '%d'", config.server_port); + if (httpd_start(&server, &config) == ESP_OK) + { + // Registering the ws handler + ESP_LOGI(TAG, "Registering URI handlers"); + httpd_register_uri_handler(server, &ws); + return server; + } + + ESP_LOGI(TAG, "Error starting server!"); + return NULL; +} +``` + +这段代码用于启动 HTTP 服务器。它首先创建一个 HTTP 服务器实例,并使用默认配置进行初始化。然后尝试启动 HTTP 服务器,如果启动成功则打印服务器监听的端口号,并注册 WebSocket 处理器(ws handler)。 +#### 2.1.2 注册路由 + +```c +static const httpd_uri_t ws = { + .uri = "/ws", + .method = HTTP_GET, + .handler = echo_handler, + .user_ctx = NULL, + .is_websocket = true}; + +``` +#### 2.1.3 路由处理 + +```c +/* + * This handler echos back the received ws data + * and triggers an async send if certain message received + */ +static esp_err_t echo_handler(httpd_req_t *req) +{ + if (req->method == HTTP_GET) + { + ESP_LOGI(TAG, "Handshake done, the new connection was opened"); + return ESP_OK; + } + httpd_ws_frame_t ws_pkt; + uint8_t *buf = NULL; + memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); + ws_pkt.type = HTTPD_WS_TYPE_TEXT; + /* Set max_len = 0 to get the frame len */ + // 读取数据 + esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 0); + if (ret != ESP_OK) + { + ESP_LOGE(TAG, "httpd_ws_recv_frame failed to get frame len with %d", ret); + return ret; + } + ESP_LOGI(TAG, "frame len is %d", ws_pkt.len); + if (ws_pkt.len) + { + /* ws_pkt.len + 1 is for NULL termination as we are expecting a string */ + buf = calloc(1, ws_pkt.len + 1); + if (buf == NULL) + { + ESP_LOGE(TAG, "Failed to calloc memory for buf"); + return ESP_ERR_NO_MEM; + } + ws_pkt.payload = buf; + /* Set max_len = ws_pkt.len to get the frame payload */ + ret = httpd_ws_recv_frame(req, &ws_pkt, ws_pkt.len); + if (ret != ESP_OK) + { + ESP_LOGE(TAG, "httpd_ws_recv_frame failed with %d", ret); + free(buf); + return ret; + } + ESP_LOGI(TAG, "Got packet with message: %s", ws_pkt.payload); + } + ESP_LOGI(TAG, "Packet type: %d", ws_pkt.type); + if (ws_pkt.type == HTTPD_WS_TYPE_TEXT && + strcmp((char *)ws_pkt.payload, "Trigger async") == 0) + { + free(buf); + return trigger_async_send(req->handle, req); + } + + ret = httpd_ws_send_frame(req, &ws_pkt); + if (ret != ESP_OK) + { + ESP_LOGE(TAG, "httpd_ws_send_frame failed with %d", ret); + } + free(buf); + return ret; +} +``` + +这段代码用于处理通过 WebSocket 接收到的数据并作出相应的响应。 + +1. 当接收到 HTTP GET 请求时,打印日志并返回 ESP_OK,表示握手成功,连接已打开。 +2. 当接收到 WebSocket 数据帧时,首先获取帧的长度,并根据长度分配内存。 +3. 再次接收 WebSocket 数据帧,获取帧的有效载荷(payload),并将其打印出来。 +4. 根据接收到的数据帧类型,如果是文本类型且内容为 "Trigger async",则调用 `trigger_async_send` 函数进行异步发送处理,并释放内存。 +5. 否则,将接收到的数据帧原样返回给客户端,并释放内存。 + +#### 2.1.4 服务器发送数据 + + +```c +/* + * Structure holding server handle + * and internal socket fd in order + * to use out of request send + */ +struct async_resp_arg +{ + httpd_handle_t hd; + int fd; +}; + +/* + * async send function, which we put into the httpd work queue + */ +static void ws_async_send(void *arg) +{ + static const char *data = "Async data"; + struct async_resp_arg *resp_arg = arg; + httpd_handle_t hd = resp_arg->hd; + int fd = resp_arg->fd; + httpd_ws_frame_t ws_pkt; + memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); + ws_pkt.payload = (uint8_t *)data; + ws_pkt.len = strlen(data); + ws_pkt.type = HTTPD_WS_TYPE_TEXT; + + httpd_ws_send_frame_async(hd, fd, &ws_pkt); + free(resp_arg); +} + +static esp_err_t trigger_async_send(httpd_handle_t handle, httpd_req_t *req) +{ + struct async_resp_arg *resp_arg = malloc(sizeof(struct async_resp_arg)); + if (resp_arg == NULL) + { + return ESP_ERR_NO_MEM; + } + resp_arg->hd = req->handle; + resp_arg->fd = httpd_req_to_sockfd(req); + esp_err_t ret = httpd_queue_work(handle, ws_async_send, resp_arg); + if (ret != ESP_OK) + { + free(resp_arg); + } + return ret; +} +``` + +异步发送 +```c +trigger_async_send(req->handle, req); +``` + +同步发送: +``` +httpd_ws_send_frame(req, &ws_pkt); +``` + +这两个函数都调用`esp_err_t httpd_ws_send_frame_async(httpd_handle_t hd, int fd, httpd_ws_frame_t *frame)` + +`httpd_ws_send_frame_async`的内容如下: + + >了解即可: + +```c +esp_err_t httpd_ws_send_frame_async(httpd_handle_t hd, int fd, httpd_ws_frame_t *frame) +{ +    if (!frame) { +        ESP_LOGW(TAG, LOG_FMT("Argument is invalid")); +        return ESP_ERR_INVALID_ARG; +    } +    /* Prepare Tx buffer - maximum length is 14, which includes 2 bytes header, 8 bytes length, 4 bytes mask key */ +    uint8_t tx_len = 0; +    uint8_t header_buf[10] = {0 }; +    /* Set the `FIN` bit by default if message is not fragmented. Else, set it as per the `final` field */ +    header_buf[0] |= (!frame->fragmented) ? HTTPD_WS_FIN_BIT : (frame->final? HTTPD_WS_FIN_BIT: HTTPD_WS_CONTINUE); +    header_buf[0] |= frame->type; /* Type (opcode): 4 bits */ +    if (frame->len <= 125) { +        header_buf[1] = frame->len & 0x7fU; /* Length for 7 bits */ +        tx_len = 2; +    } else if (frame->len > 125 && frame->len < UINT16_MAX) { +        header_buf[1] = 126;                /* Length for 16 bits */ +        header_buf[2] = (frame->len >> 8U) & 0xffU; +        header_buf[3] = frame->len & 0xffU; +        tx_len = 4; +    } else { +        header_buf[1] = 127;                /* Length for 64 bits */ +        uint8_t shift_idx = sizeof(uint64_t) - 1; /* Shift index starts at 7 */ +        uint64_t len64 = frame->len; /* Raise variable size to make sure we won't shift by more bits +                                      * than the length has (to avoid undefined behaviour) */ +        for (int8_t idx = 2; idx <= 9; idx++) { +            /* Now do shifting (be careful of endianness, i.e. when buffer index is 2, frame length shift index is 7) */ +            header_buf[idx] = (len64 >> (shift_idx * 8)) & 0xffU; +            shift_idx--; +        } +        tx_len = 10; +    } +    /* WebSocket server does not required to mask response payload, so leave the MASK bit as 0. */ +    header_buf[1] &= (~HTTPD_WS_MASK_BIT); +    struct sock_db *sess = httpd_sess_get(hd, fd); +    if (!sess) { +        return ESP_ERR_INVALID_ARG; +    } +    /* Send off header */ +    if (sess->send_fn(hd, fd, (const char *)header_buf, tx_len, 0) < 0) { +        ESP_LOGW(TAG, LOG_FMT("Failed to send WS header")); +        return ESP_FAIL; +    } +    /* Send off payload */ +    if(frame->len > 0 && frame->payload != NULL) { +        if (sess->send_fn(hd, fd, (const char *)frame->payload, frame->len, 0) < 0) { +            ESP_LOGW(TAG, LOG_FMT("Failed to send WS payload")); +            return ESP_FAIL; +        } +    } +    return ESP_OK; +} +``` + + +这是一段 ESP32 上通过 WebSocket 发送帧的代码,该函数会向指定的 WebSocket 连接发送加密后的帧数据。主要实现如下: + +1. 首先对 frame 参数进行校验,如果为空则返回 ESP_ERR_INVALID_ARG 错误。 +2. 根据 WebSocket 数据帧格式,设置 Header 的 FIN、Opcode 以及 Payload length 字段。同时根据负载长度的大小决定 Header 的长度,最多为 10 个字节,包括了 2 个字节的 Header,2 个字节的 Payload length,4 个字节的 Masking key(在此代码中未使用,因为 WebSocket Server 不需要对 Payload 进行掩码处理)。 +3. 通过 httpd_sess_get() 函数获取对应 WebSocket 连接的套接字描述符(socket descriptor),若获取失败则返回 ESP_ERR_INVALID_ARG 错误。 +4. 调用 socket 的 send_fn() 函数,将 Header 和 Payload 发送给客户端。 +5. 函数返回 ESP_OK 表示发送数据成功,返回 ESP_FAIL 表示发送数据失败。 + +### 2.2 客户端 + +> 5.0以上的IDF里,客户端程序以扩展组件包的形式存在,需要使用组件管理器 + +使用下面的命令将客户端组件添加到工程: + +``` +idf.py add-dependency 'espressif/esp_websocket_client^1.2.2' +``` + +文档连接:[ ESP WebSocket 客户端](https://docs.espressif.com/projects/esp-protocols/esp_websocket_client/docs/latest/index.html#esp-websocket-client "Permalink to this headline") + + +```c +static void log_error_if_nonzero(const char *message, int error_code) +{ + if (error_code != 0) + { + ESP_LOGE(TAG, "Last error %s: 0x%x", message, error_code); + } +} + +static void shutdown_signaler(TimerHandle_t xTimer) +{ + ESP_LOGI(TAG, "No data received for %d seconds, signaling shutdown", NO_DATA_TIMEOUT_SEC); + xSemaphoreGive(shutdown_sema); +} + +// WebSocket客户端事件处理 +static void websocket_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) +{ + esp_websocket_event_data_t *data = (esp_websocket_event_data_t *)event_data; + switch (event_id) + { + // 连接成功 + case WEBSOCKET_EVENT_CONNECTED: + ESP_LOGI(TAG, "WEBSOCKET_EVENT_CONNECTED"); + break; + // 连接断开 + case WEBSOCKET_EVENT_DISCONNECTED: + ESP_LOGI(TAG, "WEBSOCKET_EVENT_DISCONNECTED"); + log_error_if_nonzero("HTTP status code", data->error_handle.esp_ws_handshake_status_code); + if (data->error_handle.error_type == WEBSOCKET_ERROR_TYPE_TCP_TRANSPORT) + { + log_error_if_nonzero("reported from esp-tls", data->error_handle.esp_tls_last_esp_err); + log_error_if_nonzero("reported from tls stack", data->error_handle.esp_tls_stack_err); + log_error_if_nonzero("captured as transport's socket errno", data->error_handle.esp_transport_sock_errno); + } + break; + // 收到数据 + case WEBSOCKET_EVENT_DATA: + ESP_LOGI(TAG, "WEBSOCKET_EVENT_DATA"); + ESP_LOGI(TAG, "Received opcode=%d", data->op_code); + if (data->op_code == 0x08 && data->data_len == 2) + { + ESP_LOGW(TAG, "Received closed message with code=%d", 256 * data->data_ptr[0] + data->data_ptr[1]); + } + else + { + ESP_LOGW(TAG, "Received=%.*s\n\n", data->data_len, (char *)data->data_ptr); + } + + // If received data contains json structure it succeed to parse + cJSON *root = cJSON_Parse(data->data_ptr); + if (root) + { + for (int i = 0; i < cJSON_GetArraySize(root); i++) + { + cJSON *elem = cJSON_GetArrayItem(root, i); + cJSON *id = cJSON_GetObjectItem(elem, "id"); + cJSON *name = cJSON_GetObjectItem(elem, "name"); + ESP_LOGW(TAG, "Json={'id': '%s', 'name': '%s'}", id->valuestring, name->valuestring); + } + cJSON_Delete(root); + } + + ESP_LOGW(TAG, "Total payload length=%d, data_len=%d, current payload offset=%d\r\n", data->payload_len, data->data_len, data->payload_offset); + + // 定时器复位 + xTimerReset(shutdown_signal_timer, portMAX_DELAY); + break; + // 错误 + case WEBSOCKET_EVENT_ERROR: + ESP_LOGI(TAG, "WEBSOCKET_EVENT_ERROR"); + log_error_if_nonzero("HTTP status code", data->error_handle.esp_ws_handshake_status_code); + if (data->error_handle.error_type == WEBSOCKET_ERROR_TYPE_TCP_TRANSPORT) + { + log_error_if_nonzero("reported from esp-tls", data->error_handle.esp_tls_last_esp_err); + log_error_if_nonzero("reported from tls stack", data->error_handle.esp_tls_stack_err); + log_error_if_nonzero("captured as transport's socket errno", data->error_handle.esp_transport_sock_errno); + } + break; + } +} + +// WebSocket客户端 +static void websocket_app_start(void) +{ + esp_websocket_client_config_t websocket_cfg = {}; + + // 创建定时器 + shutdown_signal_timer = xTimerCreate("Websocket shutdown timer", NO_DATA_TIMEOUT_SEC * 1000 / portTICK_PERIOD_MS, + pdFALSE, NULL, shutdown_signaler); + // 创建信号量 + shutdown_sema = xSemaphoreCreateBinary(); + + // 配置目标服务器 + websocket_cfg.uri = CONFIG_WEBSOCKET_URI; + + ESP_LOGI(TAG, "Connecting to %s...", websocket_cfg.uri); + + // 创建WebSocket客户端 + esp_websocket_client_handle_t client = esp_websocket_client_init(&websocket_cfg); + // 注册事件 + esp_websocket_register_events(client, WEBSOCKET_EVENT_ANY, websocket_event_handler, (void *)client); + // 启动WebSocket客户端 + esp_websocket_client_start(client); + xTimerStart(shutdown_signal_timer, portMAX_DELAY); + char data[32]; + int i = 0; + // 发送5次数据 + while (i < 5) + { + if (esp_websocket_client_is_connected(client)) + { + int len = sprintf(data, "hello %04d", i++); + ESP_LOGI(TAG, "Sending %s", data); + // 发送文本数据 + esp_websocket_client_send_text(client, data, len, portMAX_DELAY); + } + vTaskDelay(1000 / portTICK_PERIOD_MS); + } + + ESP_LOGI(TAG, "Sending fragmented message"); + vTaskDelay(1000 / portTICK_PERIOD_MS); + memset(data, 'a', sizeof(data)); + esp_websocket_client_send_text_partial(client, data, sizeof(data), portMAX_DELAY); + memset(data, 'b', sizeof(data)); + esp_websocket_client_send_cont_msg(client, data, sizeof(data), portMAX_DELAY); + esp_websocket_client_send_fin(client, portMAX_DELAY); + + // 等待信号量 + xSemaphoreTake(shutdown_sema, portMAX_DELAY); + // 关闭WebSocket客户端 + esp_websocket_client_close(client, portMAX_DELAY); + ESP_LOGI(TAG, "Websocket Stopped"); + // 销毁WebSocket客户端 + esp_websocket_client_destroy(client); +} + +``` + +- `websocket_app_start`函数初始化WebSocket客户端配置,并设置要连接的WebSocket服务器的URI。然后使用`esp_websocket_client_init`初始化WebSocket客户端,并使用`esp_websocket_register_events`注册事件处理程序。最后,启动WebSocket客户端。 +- `websocket_event_handler`函数处理各种WebSocket事件,如连接、断开连接、接收数据和错误。它为每个事件记录相关信息,并在接收数据时重置一个定时器以防止由于不活动而关闭连接。 + +程序流程可以总结为下图(图源:Michael_ee): + +![](attachments/20240327102246.png) + + +## 三、示例 + +### 3.1 服务端 +```c +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "esp_wifi.h" +#include "esp_log.h" +#include "esp_event.h" +#include "nvs_flash.h" +#include "esp_mac.h" +#include "esp_netif.h" +#include +#include +#include "esp_eth.h" + +// 要连接的WIFI +#define ESP_WIFI_STA_SSID "duruofu_win10" +#define ESP_WIFI_STA_PASSWD "1234567890" + +static const char *TAG = "main"; + +void WIFI_CallBack(void *event_handler_arg, esp_event_base_t event_base, int32_t event_id, void *event_data) +{ + static uint8_t connect_count = 0; + // WIFI 启动成功 + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_START"); + ESP_ERROR_CHECK(esp_wifi_connect()); + } + // WIFI 连接失败 + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_DISCONNECTED"); + connect_count++; + if (connect_count < 6) + { + vTaskDelay(1000 / portTICK_PERIOD_MS); + ESP_ERROR_CHECK(esp_wifi_connect()); + } + else + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_DISCONNECTED 10 times"); + } + } + // WIFI 连接成功(获取到了IP) + if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_GOT_IP"); + ip_event_got_ip_t *info = (ip_event_got_ip_t *)event_data; + ESP_LOGI("WIFI_EVENT", "got ip:" IPSTR "", IP2STR(&info->ip_info.ip)); + } +} + +// wifi初始化 +static void wifi_sta_init(void) +{ + ESP_ERROR_CHECK(esp_netif_init()); + + // 注册事件(wifi启动成功) + ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_START, WIFI_CallBack, NULL, NULL)); + // 注册事件(wifi连接失败) + ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, WIFI_CallBack, NULL, NULL)); + // 注册事件(wifi连接失败) + ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, WIFI_CallBack, NULL, NULL)); + + // 初始化STA设备 + esp_netif_create_default_wifi_sta(); + + /*Initialize WiFi */ + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + // WIFI_INIT_CONFIG_DEFAULT 是一个默认配置的宏 + + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + //----------------配置阶段------------------- + // 初始化WIFI设备( 为 WiFi 驱动初始化 WiFi 分配资源,如 WiFi 控制结构、RX/TX 缓冲区、WiFi NVS 结构等,这个 WiFi 也启动 WiFi 任务。必须先调用此API,然后才能调用所有其他WiFi API) + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + + // STA详细配置 + wifi_config_t sta_config = { + .sta = { + .ssid = ESP_WIFI_STA_SSID, + .password = ESP_WIFI_STA_PASSWD, + .bssid_set = false, + }, + }; + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &sta_config)); + + //----------------启动阶段------------------- + ESP_ERROR_CHECK(esp_wifi_start()); + + //----------------配置省电模式------------------- + // 不省电(数据传输会更快) + ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE)); +} + +/* + * Structure holding server handle + * and internal socket fd in order + * to use out of request send + */ +struct async_resp_arg +{ + httpd_handle_t hd; + int fd; +}; + +/* + * async send function, which we put into the httpd work queue + */ +static void ws_async_send(void *arg) +{ + static const char *data = "Async data"; + struct async_resp_arg *resp_arg = arg; + httpd_handle_t hd = resp_arg->hd; + int fd = resp_arg->fd; + httpd_ws_frame_t ws_pkt; + memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); + ws_pkt.payload = (uint8_t *)data; + ws_pkt.len = strlen(data); + ws_pkt.type = HTTPD_WS_TYPE_TEXT; + + httpd_ws_send_frame_async(hd, fd, &ws_pkt); + free(resp_arg); +} + +static esp_err_t trigger_async_send(httpd_handle_t handle, httpd_req_t *req) +{ + struct async_resp_arg *resp_arg = malloc(sizeof(struct async_resp_arg)); + if (resp_arg == NULL) + { + return ESP_ERR_NO_MEM; + } + resp_arg->hd = req->handle; + resp_arg->fd = httpd_req_to_sockfd(req); + esp_err_t ret = httpd_queue_work(handle, ws_async_send, resp_arg); + if (ret != ESP_OK) + { + free(resp_arg); + } + return ret; +} + +/* + * This handler echos back the received ws data + * and triggers an async send if certain message received + */ +static esp_err_t echo_handler(httpd_req_t *req) +{ + if (req->method == HTTP_GET) + { + ESP_LOGI(TAG, "Handshake done, the new connection was opened"); + return ESP_OK; + } + httpd_ws_frame_t ws_pkt; + uint8_t *buf = NULL; + memset(&ws_pkt, 0, sizeof(httpd_ws_frame_t)); + ws_pkt.type = HTTPD_WS_TYPE_TEXT; + /* Set max_len = 0 to get the frame len */ + // 读取数据 + esp_err_t ret = httpd_ws_recv_frame(req, &ws_pkt, 0); + if (ret != ESP_OK) + { + ESP_LOGE(TAG, "httpd_ws_recv_frame failed to get frame len with %d", ret); + return ret; + } + ESP_LOGI(TAG, "frame len is %d", ws_pkt.len); + if (ws_pkt.len) + { + /* ws_pkt.len + 1 is for NULL termination as we are expecting a string */ + buf = calloc(1, ws_pkt.len + 1); + if (buf == NULL) + { + ESP_LOGE(TAG, "Failed to calloc memory for buf"); + return ESP_ERR_NO_MEM; + } + ws_pkt.payload = buf; + /* Set max_len = ws_pkt.len to get the frame payload */ + ret = httpd_ws_recv_frame(req, &ws_pkt, ws_pkt.len); + if (ret != ESP_OK) + { + ESP_LOGE(TAG, "httpd_ws_recv_frame failed with %d", ret); + free(buf); + return ret; + } + ESP_LOGI(TAG, "Got packet with message: %s", ws_pkt.payload); + } + ESP_LOGI(TAG, "Packet type: %d", ws_pkt.type); + // 发送数据 + if (ws_pkt.type == HTTPD_WS_TYPE_TEXT && + strcmp((char *)ws_pkt.payload, "Trigger async") == 0) + { + free(buf); + return trigger_async_send(req->handle, req); + } + + ret = httpd_ws_send_frame(req, &ws_pkt); + if (ret != ESP_OK) + { + ESP_LOGE(TAG, "httpd_ws_send_frame failed with %d", ret); + } + free(buf); + return ret; +} + +static const httpd_uri_t ws = { + .uri = "/ws", + .method = HTTP_GET, + .handler = echo_handler, + .user_ctx = NULL, + .is_websocket = true}; + +static httpd_handle_t start_webserver(void) +{ + httpd_handle_t server = NULL; + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + + // Start the httpd server + ESP_LOGI(TAG, "Starting server on port: '%d'", config.server_port); + if (httpd_start(&server, &config) == ESP_OK) + { + // Registering the ws handler + ESP_LOGI(TAG, "Registering URI handlers"); + httpd_register_uri_handler(server, &ws); + return server; + } + + ESP_LOGI(TAG, "Error starting server!"); + return NULL; +} + +static esp_err_t stop_webserver(httpd_handle_t server) +{ + // Stop the httpd server + return httpd_stop(server); +} + +void app_main(void) +{ + static httpd_handle_t server = NULL; + + // Initialize NVS + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) + { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + // 创建默认事件循环 + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + // 配置启动WIFI + wifi_sta_init(); + + // 等待wifi连接成功(暂时这样处理) + vTaskDelay(5000 / portTICK_PERIOD_MS); + + // 创建WebSocket服务器 + server = start_webserver(); +} + +``` + +> 注意需要设置项目HTTP组件的下面两项配置 + +![](attachments/20240327095321.png) + +然后随便打开一个WebSocket测试网站:http://www.jsons.cn/websocket/ + +![](attachments/20240327095439.png) + +服务器程序正常。 + +### 3.2 客户端 + + +我们使用python编写一个简单的回声服务器程序: + +```python +from flask import Flask +from flask_sock import Sock + +app = Flask(__name__) +sock = Sock(app) + +@sock.route('/') +def echo(ws): + while True: + data = ws.receive() + ws.send(data) + +if __name__ == '__main__': + # To run your Flask + WebSocket server in production you can use Gunicorn: + # gunicorn -b 0.0.0.0:5000 --workers 4 --threads 100 module:app + app.run(host="0.0.0.0", debug=True) +``` + +下载依赖: +``` +pip install flask-sock +``` + +运行程序: + +![](attachments/20240327111836.png) + +在代码里定义目标服务器(写自己电脑的ip): + +```c +// 目标服务器 +#define CONFIG_WEBSOCKET_URI "ws://192.168.137.1:5000" +``` + +```c +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "esp_wifi.h" +#include "esp_log.h" +#include "esp_event.h" +#include "nvs_flash.h" +#include "esp_mac.h" +#include "esp_netif.h" +#include +#include "esp_eth.h" +#include "esp_websocket_client.h" +#include + +// 要连接的WIFI +#define ESP_WIFI_STA_SSID "duruofu_win10" +#define ESP_WIFI_STA_PASSWD "1234567890" + +// 目标服务器 +#define CONFIG_WEBSOCKET_URI "ws://demo.piesocket.com/v3/channel_123?api_key=VCXCEuvhGcBDP7XhiJJUDvR1e1D3eiVjgZ9VRiaV¬ify_self" + +#define NO_DATA_TIMEOUT_SEC 5 + +static const char *TAG = "main"; + +static TimerHandle_t shutdown_signal_timer; +static SemaphoreHandle_t shutdown_sema; + +void WIFI_CallBack(void *event_handler_arg, esp_event_base_t event_base, int32_t event_id, void *event_data) +{ + static uint8_t connect_count = 0; + // WIFI 启动成功 + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_START"); + ESP_ERROR_CHECK(esp_wifi_connect()); + } + // WIFI 连接失败 + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_DISCONNECTED"); + connect_count++; + if (connect_count < 6) + { + vTaskDelay(1000 / portTICK_PERIOD_MS); + ESP_ERROR_CHECK(esp_wifi_connect()); + } + else + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_DISCONNECTED 10 times"); + } + } + // WIFI 连接成功(获取到了IP) + if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_GOT_IP"); + ip_event_got_ip_t *info = (ip_event_got_ip_t *)event_data; + ESP_LOGI("WIFI_EVENT", "got ip:" IPSTR "", IP2STR(&info->ip_info.ip)); + } +} + +// wifi初始化 +static void wifi_sta_init(void) +{ + ESP_ERROR_CHECK(esp_netif_init()); + + // 注册事件(wifi启动成功) + ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_START, WIFI_CallBack, NULL, NULL)); + // 注册事件(wifi连接失败) + ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, WIFI_CallBack, NULL, NULL)); + // 注册事件(wifi连接失败) + ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, WIFI_CallBack, NULL, NULL)); + + // 初始化STA设备 + esp_netif_create_default_wifi_sta(); + + /*Initialize WiFi */ + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + // WIFI_INIT_CONFIG_DEFAULT 是一个默认配置的宏 + + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + //----------------配置阶段------------------- + // 初始化WIFI设备( 为 WiFi 驱动初始化 WiFi 分配资源,如 WiFi 控制结构、RX/TX 缓冲区、WiFi NVS 结构等,这个 WiFi 也启动 WiFi 任务。必须先调用此API,然后才能调用所有其他WiFi API) + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + + // STA详细配置 + wifi_config_t sta_config = { + .sta = { + .ssid = ESP_WIFI_STA_SSID, + .password = ESP_WIFI_STA_PASSWD, + .bssid_set = false, + }, + }; + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &sta_config)); + + //----------------启动阶段------------------- + ESP_ERROR_CHECK(esp_wifi_start()); + + //----------------配置省电模式------------------- + // 不省电(数据传输会更快) + ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE)); +} + +static void log_error_if_nonzero(const char *message, int error_code) +{ + if (error_code != 0) + { + ESP_LOGE(TAG, "Last error %s: 0x%x", message, error_code); + } +} + +static void shutdown_signaler(TimerHandle_t xTimer) +{ + ESP_LOGI(TAG, "No data received for %d seconds, signaling shutdown", NO_DATA_TIMEOUT_SEC); + xSemaphoreGive(shutdown_sema); +} + +// WebSocket客户端事件处理 +static void websocket_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) +{ + esp_websocket_event_data_t *data = (esp_websocket_event_data_t *)event_data; + switch (event_id) + { + // 连接成功 + case WEBSOCKET_EVENT_CONNECTED: + ESP_LOGI(TAG, "WEBSOCKET_EVENT_CONNECTED"); + break; + // 连接断开 + case WEBSOCKET_EVENT_DISCONNECTED: + ESP_LOGI(TAG, "WEBSOCKET_EVENT_DISCONNECTED"); + log_error_if_nonzero("HTTP status code", data->error_handle.esp_ws_handshake_status_code); + if (data->error_handle.error_type == WEBSOCKET_ERROR_TYPE_TCP_TRANSPORT) + { + log_error_if_nonzero("reported from esp-tls", data->error_handle.esp_tls_last_esp_err); + log_error_if_nonzero("reported from tls stack", data->error_handle.esp_tls_stack_err); + log_error_if_nonzero("captured as transport's socket errno", data->error_handle.esp_transport_sock_errno); + } + break; + // 收到数据 + case WEBSOCKET_EVENT_DATA: + ESP_LOGI(TAG, "WEBSOCKET_EVENT_DATA"); + ESP_LOGI(TAG, "Received opcode=%d", data->op_code); + if (data->op_code == 0x08 && data->data_len == 2) + { + ESP_LOGW(TAG, "Received closed message with code=%d", 256 * data->data_ptr[0] + data->data_ptr[1]); + } + else + { + ESP_LOGW(TAG, "Received=%.*s\n\n", data->data_len, (char *)data->data_ptr); + } + + // If received data contains json structure it succeed to parse + cJSON *root = cJSON_Parse(data->data_ptr); + if (root) + { + for (int i = 0; i < cJSON_GetArraySize(root); i++) + { + cJSON *elem = cJSON_GetArrayItem(root, i); + cJSON *id = cJSON_GetObjectItem(elem, "id"); + cJSON *name = cJSON_GetObjectItem(elem, "name"); + ESP_LOGW(TAG, "Json={'id': '%s', 'name': '%s'}", id->valuestring, name->valuestring); + } + cJSON_Delete(root); + } + + ESP_LOGW(TAG, "Total payload length=%d, data_len=%d, current payload offset=%d\r\n", data->payload_len, data->data_len, data->payload_offset); + + // 定时器复位 + xTimerReset(shutdown_signal_timer, portMAX_DELAY); + break; + // 错误 + case WEBSOCKET_EVENT_ERROR: + ESP_LOGI(TAG, "WEBSOCKET_EVENT_ERROR"); + log_error_if_nonzero("HTTP status code", data->error_handle.esp_ws_handshake_status_code); + if (data->error_handle.error_type == WEBSOCKET_ERROR_TYPE_TCP_TRANSPORT) + { + log_error_if_nonzero("reported from esp-tls", data->error_handle.esp_tls_last_esp_err); + log_error_if_nonzero("reported from tls stack", data->error_handle.esp_tls_stack_err); + log_error_if_nonzero("captured as transport's socket errno", data->error_handle.esp_transport_sock_errno); + } + break; + } +} + +// WebSocket客户端 +static void websocket_app_start(void) +{ + esp_websocket_client_config_t websocket_cfg = {}; + + // 创建定时器 + shutdown_signal_timer = xTimerCreate("Websocket shutdown timer", NO_DATA_TIMEOUT_SEC * 1000 / portTICK_PERIOD_MS, + pdFALSE, NULL, shutdown_signaler); + // 创建信号量 + shutdown_sema = xSemaphoreCreateBinary(); + + // 配置目标服务器 + websocket_cfg.uri = CONFIG_WEBSOCKET_URI; + + ESP_LOGI(TAG, "Connecting to %s...", websocket_cfg.uri); + + // 创建WebSocket客户端 + esp_websocket_client_handle_t client = esp_websocket_client_init(&websocket_cfg); + // 注册事件 + esp_websocket_register_events(client, WEBSOCKET_EVENT_ANY, websocket_event_handler, (void *)client); + // 启动WebSocket客户端 + esp_websocket_client_start(client); + xTimerStart(shutdown_signal_timer, portMAX_DELAY); + char data[32]; + int i = 0; + // 发送5次数据 + while (i < 5) + { + if (esp_websocket_client_is_connected(client)) + { + int len = sprintf(data, "hello %04d", i++); + ESP_LOGI(TAG, "Sending %s", data); + // 发送文本数据 + esp_websocket_client_send_text(client, data, len, portMAX_DELAY); + } + vTaskDelay(1000 / portTICK_PERIOD_MS); + } + + ESP_LOGI(TAG, "Sending fragmented message"); + vTaskDelay(1000 / portTICK_PERIOD_MS); + memset(data, 'a', sizeof(data)); + esp_websocket_client_send_text_partial(client, data, sizeof(data), portMAX_DELAY); + memset(data, 'b', sizeof(data)); + esp_websocket_client_send_cont_msg(client, data, sizeof(data), portMAX_DELAY); + esp_websocket_client_send_fin(client, portMAX_DELAY); + + // 等待信号量 + xSemaphoreTake(shutdown_sema, portMAX_DELAY); + // 关闭WebSocket客户端 + esp_websocket_client_close(client, portMAX_DELAY); + ESP_LOGI(TAG, "Websocket Stopped"); + // 销毁WebSocket客户端 + esp_websocket_client_destroy(client); +} + +void app_main(void) +{ + + // Initialize NVS + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) + { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + // 创建默认事件循环 + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + // 配置启动WIFI + wifi_sta_init(); + + // 等待wifi连接成功(暂时这样处理) + vTaskDelay(5000 / portTICK_PERIOD_MS); + + // 创建WebSocket客户端 + websocket_app_start(); +} + +``` + +效果: + +![](attachments/20240327112254.png) + +成功收到回显数据,客户端程序测试完成 + +# 参考链接 + +1. https://space.bilibili.com/1338335828 +2. https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-reference/protocols/esp_http_server.html?highlight=websocket +3. https://docs.espressif.com/projects/esp-protocols/esp_websocket_client/docs/latest/index.html# \ No newline at end of file diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.4-WebSocket协议/attachments/20240326100058.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.4-WebSocket协议/attachments/20240326100058.png new file mode 100644 index 0000000..2d171fd Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.4-WebSocket协议/attachments/20240326100058.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.4-WebSocket协议/attachments/20240326100128.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.4-WebSocket协议/attachments/20240326100128.png new file mode 100644 index 0000000..7276868 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.4-WebSocket协议/attachments/20240326100128.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.4-WebSocket协议/attachments/20240326100219.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.4-WebSocket协议/attachments/20240326100219.png new file mode 100644 index 0000000..baa6678 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.4-WebSocket协议/attachments/20240326100219.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.4-WebSocket协议/attachments/20240327095321.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.4-WebSocket协议/attachments/20240327095321.png new file mode 100644 index 0000000..872c9ba Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.4-WebSocket协议/attachments/20240327095321.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.4-WebSocket协议/attachments/20240327095439.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.4-WebSocket协议/attachments/20240327095439.png new file mode 100644 index 0000000..2ef3d1c Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.4-WebSocket协议/attachments/20240327095439.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.4-WebSocket协议/attachments/20240327102246.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.4-WebSocket协议/attachments/20240327102246.png new file mode 100644 index 0000000..b38f56f Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.4-WebSocket协议/attachments/20240327102246.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.4-WebSocket协议/attachments/20240327111836.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.4-WebSocket协议/attachments/20240327111836.png new file mode 100644 index 0000000..fc21dc5 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.4-WebSocket协议/attachments/20240327111836.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.4-WebSocket协议/attachments/20240327112254.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.4-WebSocket协议/attachments/20240327112254.png new file mode 100644 index 0000000..df794b9 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.4-WebSocket协议/attachments/20240327112254.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.5-MQTT协议/MQTT协议应用.md b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.5-MQTT协议/MQTT协议应用.md index e69de29..838fc9e 100644 --- a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.5-MQTT协议/MQTT协议应用.md +++ b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.5-MQTT协议/MQTT协议应用.md @@ -0,0 +1,450 @@ + + +# ESP32网络入门-MQTT协议 + + +## 一、介绍 + +> MQTT 与 HTTP 一样,MQTT 运行在传输控制协议/互联网协议 (TCP/IP) 堆栈之上。 +### 1.1 MQTT介绍 + +> [MQTT官网](https://mqtt.org/) +> [MQTT 协议入门](https://www.emqx.com/zh/blog/the-easiest-guide-to-getting-started-with-mqtt) + +MQTT(Message Queuing Telemetry Transport)是一种轻量级的、基于发布-订阅模式的通信协议,主要应用于物联网(IoT)领域中设备间的通信。 + +![](attachments/20240327113720.png) + +MQTT 协议采用了简单的二进制编码格式,使得它适用于网络带宽较小、延迟较高、网络不稳定的环境下进行通信。同时,MQTT 提供了 QoS(Quality of Service)服务质量保证机制,支持三种不同的 QoS 等级: + +- QoS 0:最多发送一次消息,不提供可靠性保证。 +- QoS 1:至少发送一次消息,确保消息能到达接收方,但可能会重复。 +- QoS 2:恰好发送一次消息,确保消息能到达接收方且只接收一次。 + +MQTT 协议的特点包括:可扩展性好、开销小、易于实现、开源、支持多种编程语言等。 +在 MQTT 协议中,存在两个主要的参与者:发布者和订阅者。发布者将消息发布到一个或多个主题(Topic)中,订阅者可以订阅感兴趣的主题,从而接收到发布者发送的消息。主题可以看作是某一个特定类型的消息的分类标准。 +MQTT 协议的使用场景包括但不限于:智能家居、智能灯光、智能安防、农业物联网、工业物联网等。 + +##### 优点 + +1. 轻巧高效: MQTT 客户端非常小,需要的资源最少,因此可以在小型微控制器上使用。MQTT 消息头很小,可以优化网络带宽。 +2. 双向通信:MQTT 允许在设备到云和云到设备之间进行消息传递。这使得向事物组广播消息变得容易。 +3. 扩展到数百万个事物:MQTT 可以扩展以连接数百万个物联网设备。 +4. 可靠的消息传递:消息传递的可靠性对于许多物联网用例都很重要。这就是为什么 MQTT 有 3 个定义的服务质量级别:0 - 最多一次,1 - 至少一次,2 - 恰好一次 +5. 支持不可靠的网络:许多物联网设备通过不可靠的蜂窝网络连接。MQTT 对持久会话的支持减少了将客户端与代理重新连接的时间。 +6. 已启用安全性 :MQTT 使使用 TLS 加密消息和使用现代身份验证协议(如 OAuth)对客户端进行身份验证变得容易。 + +### 1.2 MQTT 数据包结构 + +- `固定头(Fixed header)`,存在于所有`MQTT`数据包中,表示数据包类型及数据包的分组类标识; +- `可变头(Variable header)`,存在于部分`MQTT`数据包中,数据包类型决定了可变头是否存在及其具体内容; +- `消息体(Payload)`,存在于部分`MQTT`数据包中,表示客户端收到的具体内容; + + ![](attachments/20240327114242.png) + +### 1.3 MQTT 其他概念 + +**一、订阅(Subscription)** + +订阅包含主题筛选器(Topic Filter)和最大服务质量(QoS)。订阅会与一个会话(Session)关联。一个会话可以包含多个订阅。每一个会话中的每个订阅都有一个不同的主题筛选器。 + +**二、会话(Session)** + +每个客户端与服务器建立连接后就是一个会话,客户端和服务器之间有状态交互。会话存在于一个网络之间,也可能在客户端和服务器之间跨越多个连续的网络连接。 + +**三、主题名(Topic Name)** + +连接到一个应用程序消息的标签,该标签与服务器的订阅相匹配。服务器会将消息发送给订阅所匹配标签的每个客户端。 + +**四、主题筛选器(Topic Filter)** + +一个对主题名通配符筛选器,在订阅表达式中使用,表示订阅所匹配到的多个主题。 + +**五、负载(Payload)** + +消息订阅者所具体接收的内容。 + + +详细内容可以查看MQTT协议文档:[MQTT Version 5.0](extension://bfdogplmndidlpjfhoijckpakkdjkkil/pdf/viewer.html?file=https%3A%2F%2Fdocs.oasis-open.org%2Fmqtt%2Fmqtt%2Fv5.0%2Fmqtt-v5.0.pdf) + +### 1.4 ESP32的MQTT支持 + +>https://docs.espressif.com/projects/esp-idf/zh_CN/v5.1.3/esp32/api-reference/protocols/mqtt.html + +- 支持基于 TCP 的 MQTT、基于 Mbed TLS 的 SSL、基于 WebSocket 的 MQTT 以及基于 WebSocket Secure 的 MQTT +- 通过 URI 简化配置流程 +- 多个实例(一个应用程序中有多个客户端) +- 支持订阅、发布、认证、遗嘱消息、保持连接心跳机制以及 3 个服务质量 (QoS) 级别(组成全功能客户端) + + +## 二、使用 + +>示例代码参考:https://github.com/espressif/esp-idf/tree/master/examples/protocols/mqtt/tcp + +#### 2.1 创建一个简单的MQTT服务器: + +```c +// MQTT客户端 +static void mqtt_app_start(void) +{ + esp_mqtt_client_config_t mqtt_cfg = { + .broker.address.uri = CONFIG_BROKER_URL, + }; + + esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqtt_cfg); + /* The last argument may be used to pass data to the event handler, in this example mqtt_event_handler */ + // 注册事件处理函数 + esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL); + // 启动MQTT客户端 + esp_mqtt_client_start(client); +} +``` + +代码很简单这里就不展开说了。 + +#### 2.2 事件处理 + +事件处理部分是重点: + +代码: +```c +// MQTT客户端事件处理 +/* + * @brief Event handler registered to receive MQTT events + * + * This function is called by the MQTT client event loop. + * + * @param handler_args user data registered to the event. + * @param base Event base for the handler(always MQTT Base in this example). + * @param event_id The id for the received event. + * @param event_data The data for the event, esp_mqtt_event_handle_t. + */ +static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) +{ + ESP_LOGD(TAG, "Event dispatched from event loop base=%s, event_id=%" PRIi32 "", base, event_id); + esp_mqtt_event_handle_t event = event_data; + esp_mqtt_client_handle_t client = event->client; + int msg_id; + switch ((esp_mqtt_event_id_t)event_id) + { + // MQTT连接成功 + case MQTT_EVENT_CONNECTED: + ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED"); + // 发布消息 + msg_id = esp_mqtt_client_publish(client, "/topic/qos1", "data_3", 0, 1, 0); + ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id); + // 订阅消息 + msg_id = esp_mqtt_client_subscribe(client, "/topic/qos0", 0); + ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id); + msg_id = esp_mqtt_client_subscribe(client, "/topic/qos1", 1); + ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id); + // 取消订阅消息 + msg_id = esp_mqtt_client_unsubscribe(client, "/topic/qos1"); + ESP_LOGI(TAG, "sent unsubscribe successful, msg_id=%d", msg_id); + break; + // MQTT连接断开 + case MQTT_EVENT_DISCONNECTED: + ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED"); + break; + // MQTT订阅成功 + case MQTT_EVENT_SUBSCRIBED: + ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id); + // 发布消息 + msg_id = esp_mqtt_client_publish(client, "/topic/qos0", "data", 0, 0, 0); + ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id); + break; + // MQTT取消订阅成功 + case MQTT_EVENT_UNSUBSCRIBED: + ESP_LOGI(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id); + break; + // MQTT发布成功 + case MQTT_EVENT_PUBLISHED: + ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id); + break; + // MQTT收到数据 + case MQTT_EVENT_DATA: + ESP_LOGI(TAG, "MQTT_EVENT_DATA"); + printf("TOPIC=%.*s\r\n", event->topic_len, event->topic); + printf("DATA=%.*s\r\n", event->data_len, event->data); + break; + // MQTT错误 + case MQTT_EVENT_ERROR: + ESP_LOGI(TAG, "MQTT_EVENT_ERROR"); + if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) + { + log_error_if_nonzero("reported from esp-tls", event->error_handle->esp_tls_last_esp_err); + log_error_if_nonzero("reported from tls stack", event->error_handle->esp_tls_stack_err); + log_error_if_nonzero("captured as transport's socket errno", event->error_handle->esp_transport_sock_errno); + ESP_LOGI(TAG, "Last errno string (%s)", strerror(event->error_handle->esp_transport_sock_errno)); + } + break; + default: + ESP_LOGI(TAG, "Other event id:%d", event->event_id); + break; + } +} +``` + +- `MQTT_EVENT_CONNECTED`: 处理MQTT客户端成功连接到代理时的事件。在此情况下,它展示了如何发布、订阅和取消订阅MQTT主题。 +- `MQTT_EVENT_DISCONNECTED`: 处理MQTT客户端与代理断开连接时的事件。 +- `MQTT_EVENT_SUBSCRIBED`: 处理MQTT客户端成功订阅主题时的事件。 +- `MQTT_EVENT_UNSUBSCRIBED`: 处理MQTT客户端成功取消订阅主题时的事件。 +- `MQTT_EVENT_PUBLISHED`: 处理MQTT客户端成功发布消息时的事件。 +- `MQTT_EVENT_DATA`: 处理收到来自代理的数据时的事件。在此情况下,它打印了主题和数据。 +- `MQTT_EVENT_ERROR`: 处理MQTT客户端发生错误时的事件。 + +## 三、实例 + + +这里使用我自己的MQTT服务器测试:`mqtt://www.duruofu.com:1883` + +```c +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "esp_wifi.h" +#include "esp_log.h" +#include "esp_event.h" +#include "nvs_flash.h" +#include "esp_mac.h" +#include "esp_netif.h" +#include +#include "esp_eth.h" +#include "lwip/sockets.h" +#include "lwip/dns.h" +#include "lwip/netdb.h" +#include "mqtt_client.h" + + +// 要连接的WIFI +#define ESP_WIFI_STA_SSID "duruofu_win10" +#define ESP_WIFI_STA_PASSWD "1234567890" + +static const char *TAG = "main"; + + + +void WIFI_CallBack(void *event_handler_arg, esp_event_base_t event_base, int32_t event_id, void *event_data) +{ + static uint8_t connect_count = 0; + // WIFI 启动成功 + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_START"); + ESP_ERROR_CHECK(esp_wifi_connect()); + } + // WIFI 连接失败 + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_DISCONNECTED"); + connect_count++; + if (connect_count < 6) + { + vTaskDelay(1000 / portTICK_PERIOD_MS); + ESP_ERROR_CHECK(esp_wifi_connect()); + } + else + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_DISCONNECTED 10 times"); + } + } + // WIFI 连接成功(获取到了IP) + if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_GOT_IP"); + ip_event_got_ip_t *info = (ip_event_got_ip_t *)event_data; + ESP_LOGI("WIFI_EVENT", "got ip:" IPSTR "", IP2STR(&info->ip_info.ip)); + } +} + +// wifi初始化 +static void wifi_sta_init(void) +{ + ESP_ERROR_CHECK(esp_netif_init()); + + // 注册事件(wifi启动成功) + ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_START, WIFI_CallBack, NULL, NULL)); + // 注册事件(wifi连接失败) + ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, WIFI_CallBack, NULL, NULL)); + // 注册事件(wifi连接失败) + ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, WIFI_CallBack, NULL, NULL)); + + // 初始化STA设备 + esp_netif_create_default_wifi_sta(); + + /*Initialize WiFi */ + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + // WIFI_INIT_CONFIG_DEFAULT 是一个默认配置的宏 + + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + //----------------配置阶段------------------- + // 初始化WIFI设备( 为 WiFi 驱动初始化 WiFi 分配资源,如 WiFi 控制结构、RX/TX 缓冲区、WiFi NVS 结构等,这个 WiFi 也启动 WiFi 任务。必须先调用此API,然后才能调用所有其他WiFi API) + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + + // STA详细配置 + wifi_config_t sta_config = { + .sta = { + .ssid = ESP_WIFI_STA_SSID, + .password = ESP_WIFI_STA_PASSWD, + .bssid_set = false, + }, + }; + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &sta_config)); + + //----------------启动阶段------------------- + ESP_ERROR_CHECK(esp_wifi_start()); + + //----------------配置省电模式------------------- + // 不省电(数据传输会更快) + ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE)); +} + +static void log_error_if_nonzero(const char *message, int error_code) +{ + if (error_code != 0) + { + ESP_LOGE(TAG, "Last error %s: 0x%x", message, error_code); + } +} + + +// MQTT客户端事件处理 +/* + * @brief Event handler registered to receive MQTT events + * + * This function is called by the MQTT client event loop. + * + * @param handler_args user data registered to the event. + * @param base Event base for the handler(always MQTT Base in this example). + * @param event_id The id for the received event. + * @param event_data The data for the event, esp_mqtt_event_handle_t. + */ +static void mqtt_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data) +{ + ESP_LOGD(TAG, "Event dispatched from event loop base=%s, event_id=%" PRIi32 "", base, event_id); + esp_mqtt_event_handle_t event = event_data; + esp_mqtt_client_handle_t client = event->client; + int msg_id; + switch ((esp_mqtt_event_id_t)event_id) + { + // MQTT连接成功 + case MQTT_EVENT_CONNECTED: + ESP_LOGI(TAG, "MQTT_EVENT_CONNECTED"); + // 发布消息 + msg_id = esp_mqtt_client_publish(client, "/topic/qos1", "data_3", 0, 1, 0); + ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id); + // 订阅消息 + msg_id = esp_mqtt_client_subscribe(client, "/topic/qos0", 0); + ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id); + msg_id = esp_mqtt_client_subscribe(client, "/topic/qos1", 1); + ESP_LOGI(TAG, "sent subscribe successful, msg_id=%d", msg_id); + // 取消订阅消息 + msg_id = esp_mqtt_client_unsubscribe(client, "/topic/qos1"); + ESP_LOGI(TAG, "sent unsubscribe successful, msg_id=%d", msg_id); + break; + // MQTT连接断开 + case MQTT_EVENT_DISCONNECTED: + ESP_LOGI(TAG, "MQTT_EVENT_DISCONNECTED"); + break; + // MQTT订阅成功 + case MQTT_EVENT_SUBSCRIBED: + ESP_LOGI(TAG, "MQTT_EVENT_SUBSCRIBED, msg_id=%d", event->msg_id); + // 发布消息 + msg_id = esp_mqtt_client_publish(client, "/topic/qos0", "data", 0, 0, 0); + ESP_LOGI(TAG, "sent publish successful, msg_id=%d", msg_id); + break; + // MQTT取消订阅成功 + case MQTT_EVENT_UNSUBSCRIBED: + ESP_LOGI(TAG, "MQTT_EVENT_UNSUBSCRIBED, msg_id=%d", event->msg_id); + break; + // MQTT发布成功 + case MQTT_EVENT_PUBLISHED: + ESP_LOGI(TAG, "MQTT_EVENT_PUBLISHED, msg_id=%d", event->msg_id); + break; + // MQTT收到数据 + case MQTT_EVENT_DATA: + ESP_LOGI(TAG, "MQTT_EVENT_DATA"); + printf("TOPIC=%.*s\r\n", event->topic_len, event->topic); + printf("DATA=%.*s\r\n", event->data_len, event->data); + break; + // MQTT错误 + case MQTT_EVENT_ERROR: + ESP_LOGI(TAG, "MQTT_EVENT_ERROR"); + if (event->error_handle->error_type == MQTT_ERROR_TYPE_TCP_TRANSPORT) + { + log_error_if_nonzero("reported from esp-tls", event->error_handle->esp_tls_last_esp_err); + log_error_if_nonzero("reported from tls stack", event->error_handle->esp_tls_stack_err); + log_error_if_nonzero("captured as transport's socket errno", event->error_handle->esp_transport_sock_errno); + ESP_LOGI(TAG, "Last errno string (%s)", strerror(event->error_handle->esp_transport_sock_errno)); + } + break; + default: + ESP_LOGI(TAG, "Other event id:%d", event->event_id); + break; + } +} + +// MQTT客户端 +static void mqtt_app_start(void) +{ + esp_mqtt_client_config_t mqtt_cfg = { + .broker.address.uri = "mqtt://www.duruofu.top:1883", + }; + + esp_mqtt_client_handle_t client = esp_mqtt_client_init(&mqtt_cfg); + /* The last argument may be used to pass data to the event handler, in this example mqtt_event_handler */ + // 注册事件处理函数 + esp_mqtt_client_register_event(client, ESP_EVENT_ANY_ID, mqtt_event_handler, NULL); + // 启动MQTT客户端 + esp_mqtt_client_start(client); +} + +void app_main(void) +{ + + // Initialize NVS + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) + { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + // 创建默认事件循环 + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + // 配置启动WIFI + wifi_sta_init(); + + // 等待wifi连接成功(暂时这样处理) + vTaskDelay(5000 / portTICK_PERIOD_MS); + + // 创建MQTT客户端 + mqtt_app_start(); +} + +``` + +使用MQTTX来充当另一个客户端: + +![](attachments/20240327123152.png) + +成功收到消息,如上图 + +尝试发送数据,ESP会将数据返回: + +![](attachments/20240327123309.png) + +![](attachments/20240327123341.png) + + +这样就完成了简单的MQTT客户端程序。 +# 参考链接 + +1. https://docs.espressif.com/projects/esp-idf/zh_CN/v5.1.3/esp32/api-reference/protocols/mqtt.html +2. https://github.com/espressif/esp-idf/blob/master/examples/protocols/mqtt/tcp/main/app_main.c \ No newline at end of file diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.5-MQTT协议/attachments/20240327113720.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.5-MQTT协议/attachments/20240327113720.png new file mode 100644 index 0000000..6fe4b7d Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.5-MQTT协议/attachments/20240327113720.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.5-MQTT协议/attachments/20240327114242.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.5-MQTT协议/attachments/20240327114242.png new file mode 100644 index 0000000..900a9fb Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.5-MQTT协议/attachments/20240327114242.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.5-MQTT协议/attachments/20240327123152.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.5-MQTT协议/attachments/20240327123152.png new file mode 100644 index 0000000..08ed791 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.5-MQTT协议/attachments/20240327123152.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.5-MQTT协议/attachments/20240327123309.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.5-MQTT协议/attachments/20240327123309.png new file mode 100644 index 0000000..e863c79 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.5-MQTT协议/attachments/20240327123309.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.5-MQTT协议/attachments/20240327123341.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.5-MQTT协议/attachments/20240327123341.png new file mode 100644 index 0000000..726fb25 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.5-MQTT协议/attachments/20240327123341.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.6-ESP-NOW协议/ESP-NOW协议应用.md b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.6-ESP-NOW协议/ESP-NOW协议应用.md index e69de29..b20de55 100644 --- a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.6-ESP-NOW协议/ESP-NOW协议应用.md +++ b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.6-ESP-NOW协议/ESP-NOW协议应用.md @@ -0,0 +1,291 @@ + +# ESP32网络入门-ESP-NOW协议 + +## 一、概述 + + +### 1.1 ESP-NOW介绍 + +>视频介绍:https://www.espressif.com/sites/default/files/esp-now-zh.mp4 + +ESP-NOW 是一种由乐鑫公司定义的无连接 Wi-Fi 通信协议。在 ESP-NOW 中,应用程序数据被封装在各个供应商的动作帧中,然后在无连接的情况下,从一个 Wi-Fi 设备传输到另一个 Wi-Fi 设备。 CTR 与 CBC-MAC 协议 (CCMP) 可用来保护动作帧的安全。ESP-NOW 广泛应用于智能照明、远程控制、传感器等领域。 + +ESP-NOW 是基于数据链路层的无线通信协议,它将五层 OSI 上层协议精简为一层,数据传输时无需依次经过网络层、传输层、会话层、表示层、应用层等复杂的层级,也无需层层增加包头和解包,大大缓解了网络拥挤时因为丢包而导致的卡顿和延迟,拥有更高的响应速度。 + +![](attachments/20240327133912.png) + + +### 1.2 ESP-NOW 数据帧格式 + +> 搬运自官方教程 + +ESP-NOW 使用各个供应商的动作帧传输数据,默认比特率为 1 Mbps。各个供应商的动作帧格式为: + +| MAC 报头 | 分类代码 | 组织标识符 | 随机值 | 供应商特定内容 | FCS | +| ------ | ---- | ----- | ---- | -------- | ---- | +| 24 字节 | 1 字节 | 3 字节 | 4 字节 | 7~257 字节 | 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的基本条件是要知道ESP32的MAC地址 + +可以使用[base_mac_address](https://github.com/espressif/esp-idf/tree/v5.1.3/examples/system/base_mac_address)历程获取芯片MAC地址: + +![](attachments/20240327141511.png) + +### 2.1 初始化 + +调用 [`esp_now_init()`](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32c3/api-reference/network/esp_now.html#_CPPv412esp_now_initv "esp_now_init") 初始化 ESP-NOW,调用 [`esp_now_deinit()`](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32c3/api-reference/network/esp_now.html#_CPPv414esp_now_deinitv "esp_now_deinit") 反初始化 ESP-NOW。ESP-NOW 数据必须在 Wi-Fi 启动后传输,因此建议在初始化 ESP-NOW 之前启动 Wi-Fi,并在反初始化 ESP-NOW 之后停止 Wi-Fi。 + +当调用 [`esp_now_deinit()`](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32c3/api-reference/network/esp_now.html#_CPPv414esp_now_deinitv "esp_now_deinit") 时,配对设备的所有信息都将被删除。 +### 2.2 设备配对 + +在将数据发送到其他设备之前,请先调用 [`esp_now_add_peer()`](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32c3/api-reference/network/esp_now.html#_CPPv416esp_now_add_peerPK19esp_now_peer_info_t "esp_now_add_peer") 将其添加到配对设备列表中。如果启用了加密,则必须设置 LMK。ESP-NOW 数据可以从 Station 或 SoftAP 接口发送。确保在发送 ESP-NOW 数据之前已启用该接口。 +配对设备的最大数量是 20,其中加密设备的数量不超过 17,默认值是 7。 +### 2.3 发送数据 + +调用 [`esp_now_send()`](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32c3/api-reference/network/esp_now.html#_CPPv412esp_now_sendPK7uint8_tPK7uint8_t6size_t "esp_now_send") 发送 ESP-NOW 数据,调用 [`esp_now_register_send_cb()`](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32c3/api-reference/network/esp_now.html#_CPPv424esp_now_register_send_cb17esp_now_send_cb_t "esp_now_register_send_cb") 注册发送回调函数。如果 MAC 层成功接收到数据,则该函数将返回 ESP_NOW_SEND_SUCCESS 事件。否则,它将返回 ESP_NOW_SEND_FAIL。 + +如果有大量 ESP-NOW 数据要发送,调用 `esp_now_send()` 时需注意单次发送的数据不能超过 250 字节。请注意,两个 ESP-NOW 数据包的发送间隔太短可能导致回调函数返回混乱。因此,建议在等到上一次回调函数返回 ACK 后再发送下一个 ESP-NOW 数据。 +### 2.4 接收数据 + +调用 [`esp_now_register_recv_cb()`](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32c3/api-reference/network/esp_now.html#_CPPv424esp_now_register_recv_cb17esp_now_recv_cb_t "esp_now_register_recv_cb") 注册接收回调函数。当接收 ESP-NOW 数据时,需要调用接收回调函数。接收回调函数也在 Wi-Fi 任务任务中运行。因此,不要在回调函数中执行冗长的操作。 相反,将必要的数据发布到队列,并交给优先级较低的任务处理。 + +## 三、示例 + +### 3.1 发送 + +> 注意:发送需要接收方的MAC地址 + +```c +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "freertos/timers.h" +#include "nvs_flash.h" +#include "esp_random.h" +#include "esp_event.h" +#include "esp_netif.h" +#include "esp_wifi.h" +#include "esp_log.h" +#include "esp_mac.h" +#include "esp_now.h" +#include "esp_crc.h" + +// 接收方地址 +//static uint8_t s_broadcast_mac[ESP_NOW_ETH_ALEN] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static uint8_t s_broadcast_mac[ESP_NOW_ETH_ALEN] = {0x24, 0xdc, 0xc3, 0x9c, 0xeb, 0x18}; + +static const char *TAG = "espnow"; + +/* WiFi should start before using ESPNOW */ +static void wifi_init(void) +{ + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM)); + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + ESP_ERROR_CHECK(esp_wifi_start()); + +} + +static void example_espnow_send_cb(const uint8_t *mac_addr, esp_now_send_status_t status) +{ + if (status != ESP_NOW_SEND_SUCCESS) + { + ESP_LOGE(TAG, "Send error"); + } + else + { + ESP_LOGI(TAG, "Send success"); + } +} + +// ESPNOW发送任务 +static void espnow_task(void *pvParameter) +{ + char send_msg[] = "ESPNOW test!"; + ESP_LOGI(TAG, "Start sending broadcast data"); + while (1) + { + // 发送数据 + if (esp_now_send(s_broadcast_mac, (uint8_t *)send_msg, sizeof(send_msg)) != ESP_OK) + { + ESP_LOGE(TAG, "Send error"); + } + vTaskDelay(1000 / portTICK_PERIOD_MS); + } +} + +static esp_err_t espnow_init(void) +{ + + /* Initialize ESPNOW and register sending and receiving callback function. */ + ESP_ERROR_CHECK(esp_now_init()); + // 注册发送回调函数 + ESP_ERROR_CHECK(esp_now_register_send_cb(example_espnow_send_cb)); + //ESP_ERROR_CHECK(esp_now_register_recv_cb(example_espnow_recv_cb)); + + // 将广播对等方信息添加到对等方列表 + esp_now_peer_info_t *peer = malloc(sizeof(esp_now_peer_info_t)); + if (peer == NULL) + { + ESP_LOGE(TAG, "Malloc peer information fail"); + esp_now_deinit(); + return ESP_FAIL; + } + // 初始化对等方信息 + memset(peer, 0, sizeof(esp_now_peer_info_t)); + peer->channel = 1; + peer->ifidx = ESP_IF_WIFI_STA; + peer->encrypt = false; + memcpy(peer->peer_addr, s_broadcast_mac, ESP_NOW_ETH_ALEN); + // 添加对等方 + ESP_ERROR_CHECK(esp_now_add_peer(peer)); + free(peer); + + xTaskCreate(espnow_task, "espnow_task", 2048, NULL, 4, NULL); + + return ESP_OK; +} + +void app_main(void) +{ + // Initialize NVS + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) + { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + // Initialize WiFi + wifi_init(); + + // Initialize ESPNOW + espnow_init(); +} + +``` + +### 3.2 接收 + +```c +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/semphr.h" +#include "freertos/timers.h" +#include "nvs_flash.h" +#include "esp_random.h" +#include "esp_event.h" +#include "esp_netif.h" +#include "esp_wifi.h" +#include "esp_log.h" +#include "esp_mac.h" +#include "esp_now.h" +#include "esp_crc.h" + +// 接收方地址 +// static uint8_t s_broadcast_mac[ESP_NOW_ETH_ALEN] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; +static uint8_t s_broadcast_mac[ESP_NOW_ETH_ALEN] = {0x24, 0xdc, 0xc3, 0x9c, 0xeb, 0x18}; + +static const char *TAG = "espnow"; + +/* WiFi should start before using ESPNOW */ +static void wifi_init(void) +{ + ESP_ERROR_CHECK(esp_netif_init()); + ESP_ERROR_CHECK(esp_event_loop_create_default()); + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM)); + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + ESP_ERROR_CHECK(esp_wifi_start()); +} + +// ESPNOW接收回调函数 +static void example_espnow_recv_cb(const esp_now_recv_info_t *recv_info, const uint8_t *data, int len) +{ + ESP_LOGI(TAG, "Receive data from: " MACSTR ", len: %d", MAC2STR(recv_info->src_addr), len); + ESP_LOGI(TAG, "Data: %s", data); +} + +// ESPNOW接受数据任务 +static void espnow_task(void *pvParameter) +{ + ESP_LOGI(TAG, "Start receiving data"); + while (1) + { + vTaskDelay(1000 / portTICK_PERIOD_MS); + } +} + +static esp_err_t espnow_init(void) +{ + + /* Initialize ESPNOW and register sending and receiving callback function. */ + ESP_ERROR_CHECK(esp_now_init()); + // 注册接受回调函数 + //ESP_ERROR_CHECK(esp_now_register_send_cb(example_espnow_send_cb)); + ESP_ERROR_CHECK(esp_now_register_recv_cb(example_espnow_recv_cb)); + + xTaskCreate(espnow_task, "espnow_task", 2048, NULL, 4, NULL); + + return ESP_OK; +} + +void app_main(void) +{ + // Initialize NVS + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) + { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + // Initialize WiFi + wifi_init(); + + // Initialize ESPNOW + espnow_init(); +} + +``` + +效果: + +![](attachments/20240327152252.png) + +# 参考链接 + +1. https://github.com/espressif/esp-idf/blob/v5.1.3/examples/wifi/espnow/main/espnow_example.h +2. https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32c3/api-reference/network/esp_now.html \ No newline at end of file diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.6-ESP-NOW协议/attachments/20240327133912.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.6-ESP-NOW协议/attachments/20240327133912.png new file mode 100644 index 0000000..39ccf0d Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.6-ESP-NOW协议/attachments/20240327133912.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.6-ESP-NOW协议/attachments/20240327141511.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.6-ESP-NOW协议/attachments/20240327141511.png new file mode 100644 index 0000000..edf72bf Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.6-ESP-NOW协议/attachments/20240327141511.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.6-ESP-NOW协议/attachments/20240327152252.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.6-ESP-NOW协议/attachments/20240327152252.png new file mode 100644 index 0000000..3f5cf38 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.6-ESP-NOW协议/attachments/20240327152252.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.7-SNTP校时/SNTP校时.md b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.7-SNTP校时/SNTP校时.md new file mode 100644 index 0000000..af945a7 --- /dev/null +++ b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.7-SNTP校时/SNTP校时.md @@ -0,0 +1,260 @@ + +# ESP32网络入门-SNTP校时 + +## 一、介绍 + +SNTP 指 **简单网络时间协议**(Simple Network Time Protocol),一个合格的物联网设备,少不了一个准确的钟。通过SNTP,可以使ESP32设备通过网络校准本地时间。使用起来也非常简单。 + +## 二、使用 + +#### 2.1 初始化特定的 SNTP 服务器 + +要初始化特定的 SNTP 服务器并启动 SNTP 服务,只需创建有特定服务器名称的默认 SNTP 服务器配置,然后调用 [`esp_netif_sntp_init()`](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32s3/api-reference/network/esp_netif.html#_CPPv419esp_netif_sntp_initPK17esp_sntp_config_t "esp_netif_sntp_init") 注册该服务器并启动 SNTP 服务。 + +```c +esp_sntp_config_t config = ESP_NETIF_SNTP_DEFAULT_CONFIG("pool.ntp.org"); +esp_netif_sntp_init(&config); +``` + +#### 2.1 启动 SNTP 服务 + +```c + // Start SNTP + esp_netif_sntp_start(); +``` + +#### 2.3 同步时间 + +```c + // wait for time to be set + time_t now = 0; + struct tm timeinfo = {0}; + int retry = 0; + const int retry_count = 15; + while (esp_netif_sntp_sync_wait(2000 / portTICK_PERIOD_MS) == ESP_ERR_TIMEOUT && ++retry < retry_count) + { + ESP_LOGI(TAG, "Waiting for system time to be set... (%d/%d)", retry, retry_count); + } + time(&now); + localtime_r(&now, &timeinfo); + +``` + +#### 2.4 其他 + +设置时区: + +1. 调用 `setenv()`,将 `TZ` 环境变量根据设备位置设置为正确的值。时间字符串的格式与 [GNU libc 文档](https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html) 中描述的相同(但实现方式不同)。 +2. 调用 `tzset()`,为新的时区更新 C 库的运行数据。 + +例: +```c +// Set timezone to Beijing time +setenv("TZ", "CST-8", 1); // CST-8 represents China Standard Time (UTC+8) +tzset(); +``` + +## 三、示例 + +```c +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "esp_wifi.h" +#include "esp_log.h" +#include "esp_event.h" +#include "nvs_flash.h" +#include "esp_mac.h" +#include "esp_netif.h" +#include +#include "esp_netif_sntp.h" +#include "lwip/ip_addr.h" +#include "esp_sntp.h" + +// 要连接的WIFI +#define ESP_WIFI_STA_SSID "duruofu" +#define ESP_WIFI_STA_PASSWD "1234567890" + + +static const char *TAG = "main"; + +void time_sync_notification_cb(struct timeval *tv) +{ + ESP_LOGI(TAG, "Notification of a time synchronization event"); +} + +static void print_servers(void) +{ + ESP_LOGI(TAG, "List of configured NTP servers:"); + + for (uint8_t i = 0; i < SNTP_MAX_SERVERS; ++i) + { + if (esp_sntp_getservername(i)) + { + ESP_LOGI(TAG, "server %d: %s", i, esp_sntp_getservername(i)); + } + else + { + // we have either IPv4 or IPv6 address, let's print it + char buff[INET6_ADDRSTRLEN]; + ip_addr_t const *ip = esp_sntp_getserver(i); + if (ipaddr_ntoa_r(ip, buff, INET6_ADDRSTRLEN) != NULL) + ESP_LOGI(TAG, "server %d: %s", i, buff); + } + } +} + +static void obtain_time(void) +{ + // Set timezone to Beijing time + setenv("TZ", "CST-8", 1); // CST-8 represents China Standard Time (UTC+8) + tzset(); + + ESP_LOGI(TAG, "Initializing SNTP"); + esp_sntp_config_t config = ESP_NETIF_SNTP_DEFAULT_CONFIG("cn.pool.ntp.org"); + config.sync_cb = time_sync_notification_cb; + config.renew_servers_after_new_IP = true, + + // Initialize SNTP with the provided configuration + esp_netif_sntp_init(&config); + + // Start SNTP + esp_netif_sntp_start(); + + ESP_LOGI(TAG, "Initializing and starting SNTP"); + + print_servers(); + + // wait for time to be set + time_t now = 0; + struct tm timeinfo = {0}; + int retry = 0; + const int retry_count = 15; + while (esp_netif_sntp_sync_wait(2000 / portTICK_PERIOD_MS) == ESP_ERR_TIMEOUT && ++retry < retry_count) + { + ESP_LOGI(TAG, "Waiting for system time to be set... (%d/%d)", retry, retry_count); + } + time(&now); + localtime_r(&now, &timeinfo); + + + + // Print current time + char strftime_buf[64]; + strftime(strftime_buf, sizeof(strftime_buf), "%c", &timeinfo); + ESP_LOGI(TAG, "Current time: %s", strftime_buf); +} + + +void WIFI_CallBack(void *event_handler_arg, esp_event_base_t event_base, int32_t event_id, void *event_data) +{ + static uint8_t connect_count = 0; + // WIFI 启动成功 + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_START"); + ESP_ERROR_CHECK(esp_wifi_connect()); + } + // WIFI 连接失败 + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_DISCONNECTED"); + connect_count++; + if (connect_count < 6) + { + vTaskDelay(1000 / portTICK_PERIOD_MS); + ESP_ERROR_CHECK(esp_wifi_connect()); + } + else + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_DISCONNECTED 10 times"); + } + } + // WIFI 连接成功(获取到了IP) + if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) + { + ESP_LOGI("WIFI_EVENT", "WIFI_EVENT_STA_GOT_IP"); + ip_event_got_ip_t *info = (ip_event_got_ip_t *)event_data; + ESP_LOGI("WIFI_EVENT", "got ip:" IPSTR "", IP2STR(&info->ip_info.ip)); + // 更新时间 + obtain_time(); + } +} + +// wifi初始化 +static void wifi_sta_init(void) +{ + ESP_ERROR_CHECK(esp_netif_init()); + + // 注册事件(wifi启动成功) + ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_START, WIFI_CallBack, NULL, NULL)); + // 注册事件(wifi连接失败) + ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, WIFI_EVENT_STA_DISCONNECTED, WIFI_CallBack, NULL, NULL)); + // 注册事件(wifi连接失败) + ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, WIFI_CallBack, NULL, NULL)); + + // 初始化STA设备 + esp_netif_create_default_wifi_sta(); + + /*Initialize WiFi */ + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + // WIFI_INIT_CONFIG_DEFAULT 是一个默认配置的宏 + + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + //----------------配置阶段------------------- + // 初始化WIFI设备( 为 WiFi 驱动初始化 WiFi 分配资源,如 WiFi 控制结构、RX/TX 缓冲区、WiFi NVS 结构等,这个 WiFi 也启动 WiFi 任务。必须先调用此API,然后才能调用所有其他WiFi API) + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + + // STA详细配置 + wifi_config_t sta_config = { + .sta = { + .ssid = ESP_WIFI_STA_SSID, + .password = ESP_WIFI_STA_PASSWD, + .bssid_set = false, + }, + }; + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &sta_config)); + + //----------------启动阶段------------------- + ESP_ERROR_CHECK(esp_wifi_start()); + + //----------------配置省电模式------------------- + // 不省电(数据传输会更快) + ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE)); +} + + +void app_main(void) +{ + // Initialize NVS + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) + { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + // 创建默认事件循环 + ESP_ERROR_CHECK(esp_event_loop_create_default()); + + // 配置启动WIFI + wifi_sta_init(); + + // 使用SNTP校准时间 + +} + + +``` + +效果: + +![](attachments/20240412152658.png) + +# 参考链接 + +1. https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32s3/api-reference/system/system_time.html?highlight=sntp +2. https://github.com/espressif/esp-idf/blob/5a40bb8746633477c07ff9a3e90016c37fa0dc0c/examples/protocols/sntp/main/sntp_example_main.c \ No newline at end of file diff --git a/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.7-SNTP校时/attachments/20240412152658.png b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.7-SNTP校时/attachments/20240412152658.png new file mode 100644 index 0000000..2f2ab24 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.2-网络协议应用/6.2.7-SNTP校时/attachments/20240412152658.png differ diff --git a/docs/06.Wi-Fi功能与相关协议/6.3-Wi-Fi配网技术/6.3.1-BluFi配网/ESP32网络入门-BluFi配网.md b/docs/06.Wi-Fi功能与相关协议/6.3-Wi-Fi配网技术/6.3.1-BluFi配网/ESP32网络入门-BluFi配网.md new file mode 100644 index 0000000..4adf790 --- /dev/null +++ b/docs/06.Wi-Fi功能与相关协议/6.3-Wi-Fi配网技术/6.3.1-BluFi配网/ESP32网络入门-BluFi配网.md @@ -0,0 +1,59 @@ + +### 说明: + +1. 本文档由DuRuofu撰写,由DuRuofu负责解释及执行。 +2. 本文档记录如何使用BluFi完成ESP32的WiFi配网程序。 + +### 修订历史: + +| 文档名称 | 版本 | 作者 | 时间 | 备注 | +| ----------------- | ------ | ------- | ---------- | ---- | +| ESP32网络入门-BluFi配网 | v1.0.0 | DuRuofu | 2024-04-03 | 首次建立 | + +
+ +# ESP32网络入门-BluFi配网 + +## 一、介绍 + +### 1.1 BluFi是什么 + +BluFi 是一项基于蓝牙通道的 Wi-Fi 网络配置功能,适用于 ESP32。它通过安全协议将 Wi-Fi 的 SSID、密码等配置信息传输到 ESP32。基于这些信息,ESP32 可进而连接到 AP 或建立 SoftAP。 + +BluFi 流程的关键部分包括数据的分片、加密以及校验和验证。 + +用户可按需自定义用于对称加密、非对称加密以及校验的算法。此处,我们采用 DH 算法进行密钥协商,128-AES 算法用于数据加密,CRC16 算法用于校验和验证。 + + +### 1.2 流程 + +BluFi 配网流程包含配置 SoftAP 和配置 Station 两部分。 + +![](attachments/Pasted%20image%2020240403181235.png) + +1. ESP32 开启 GATT Server 模式,发送带有特定 _advertising data_ 的广播。该广播不属于 BluFi Profile,可以按需对其进行自定义。 +2. 使用手机应用程序搜索到该广播后,手机将作为 GATT Client 连接 ESP32。该步骤对具体使用哪款手机应用程序并无特殊要求。 +3. 成功建立 GATT 连接后,手机会向 ESP32 发送数据帧进行密钥协商(详见 [BluFi 中定义的帧格式](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-guides/blufi.html#frame-formats) )。 +4. ESP32 收到密钥协商的数据帧后,会按照自定义的协商方法进行解析。 +5. 手机与 ESP32 进行密钥协商。协商过程可使用 DH/RSA/ECC 等加密算法。 +6. 协商结束后,手机端向 ESP32 发送控制帧,用于设置安全模式。 +7. ESP32 收到控制帧后,使用共享密钥以及安全配置对通信数据进行加密和解密。 +8. 手机向 ESP32 发送 [BluFi 中定义的帧格式](https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-guides/blufi.html#frame-formats) 中定义的数据帧,包括 SSID、密码等 Wi-Fi 配置信息。 +9. 手机向 ESP32 发送 Wi-Fi 连接请求的控制帧。ESP32 收到控制帧后,即默认手机已完成必要信息的传输,准备连接 Wi-Fi。 +10. 连接到 Wi-Fi 后,ESP32 发送 Wi-Fi 连接状态报告的控制帧到手机。至此,配网结束。 + +>1. ESP32 收到安全模式配置的控制帧后,会根据定义的安全模式进行相关操作。 +>2. 进行对称加密和解密时,加密和解密前后的数据长度必须一致。支持原地加密和解密。 + + + + + +## 二、使用 + + + +## 三、示例 + + +# 参考链接 \ No newline at end of file diff --git a/docs/06.Wi-Fi功能与相关协议/6.3-Wi-Fi配网技术/6.3.1-BluFi配网/attachments/Pasted image 20240403181235.png b/docs/06.Wi-Fi功能与相关协议/6.3-Wi-Fi配网技术/6.3.1-BluFi配网/attachments/Pasted image 20240403181235.png new file mode 100644 index 0000000..13f8330 Binary files /dev/null and b/docs/06.Wi-Fi功能与相关协议/6.3-Wi-Fi配网技术/6.3.1-BluFi配网/attachments/Pasted image 20240403181235.png differ