ESP32入门开发·SmartConfig实现配网功能

目录

1.  概述

2.  步骤解析

2.1  设备进入配网模式

2.2  手机APP准备配置信息

2.3  编码与发送

2.4  监听与解码

2.5  连接与验证

2.6  拓展

3.  代码编写

3.1  准备工作

3.2  头文件包含

3.3  初始化配置

3.4  事件处理函数

3.5  完整工程


1.  概述

        SmartConfig 是由 TI 开发的配网技术,用于将新的 Wi-Fi 设备连接到 Wi-Fi 网络。它使用移动应用程序将无线网凭据从智能手机或平板电脑端广播给未配网的 Wi-Fi 设备。

        在没有使用 SmartConfig 技术之前,通过我们之前对通过 STA 模式连接 WIFI 可知,我们连接 WIFI 是靠在代码中加固固定的 SSID 和密码区连接热点,但实际应用中不可能这么弄,我们得有办法把家里的 WiFi SSID 和密码输入到设备里面去,对于带屏带输入设备还好,因为可以人为手动输入,但很多 IOT 设备都不具备这种能力,因此我们需要其他方法。把 SSID 和密码告诉给设备,让设备能正确连接 WiFi 热点接入到物联网的过程,称为配网。

        而使用 SmartConfig 技术,其优势在于,设备无需直接获知 AP 的 SSID 或密码,而是通过智能手机获取。这对于没有用户界面的无头设备和系统而言十分重要。

        其工作原理,首先要让 WiFi 芯片处于混杂模式下,监听网络中的所有报文;手机 APP 将 SSID 和密码编码到 UDP 报文中,通过广播包或组播报发送,智能硬件接收到 UDP 报文后解码,得到正确的SSID 和密码,然后主动连接指定 SSID 的路由完成连接。

2.  步骤解析

2.1  设备进入配网模式

        智能硬件首次启动或用户按下“配网键”后,会进入一个特殊的等待配置状态。在这个状态下,设备断开与任何已知Wi-Fi网络的连接,并让其Wi-Fi芯片工作在混杂模式。这使得它可以监听范围内所有的无线数据包,而不仅仅是发给自己的包。

2.2  手机APP准备配置信息

        用户在手机APP上选择家庭Wi-Fi的SSID(网络名称)并输入密码。APP还会生成一些额外的配置信息,例如:

  • 目标设备的MAC地址(如果APP通过蓝牙或手机本地网络发现了设备,则可预先获取,用于定向发送,提高成功率)。
  • 随机Token(用于后续设备连接成功后上报服务器时的身份验证,防止伪冒)。
  • 加密密钥(可选,为了安全,可以对SSID和密码进行加密)。

        APP将这些信息按照预定义好的协议格式组装成一个数据包。

2.3  编码与发送

        手机APP将这个配置数据包封装在UDP协议中。关键的一步是选择发送的目标地址。为了确保设备无论在哪个IP段都能收到,通常采用两种方式:

  • 广播:发送到当前手机所连接的Wi-Fi网络的广播地址(例如 192.168.1.255)。该局域网内的所有设备都会收到这个包。
  • 组播:发送到一个预先约定好的组播地址(例如 224.0.0.1 或自定义地址)。设备只需要加入这个组播组就能收到信息,比广播稍微“优雅”一些。

        手机APP会重复发送这个UDP包多次,以增加设备接收到的概率。

2.4  监听与解码

        处于混杂模式的智能硬件一直在扫描空中的802.11无线报文。当发现有UDP数据包发往广播或特定组播地址时,它会根据目标端口号(设备固件预先约定好的一个端口,例如 10000)来判断这是否是一个配网包,设备抓取到这个包后,按照预定义的协议格式进行解码,提取出其中的SSID、密码、Token等信息。

2.5  连接与验证

        处于混杂模式的智能硬件一直在扫描空中的802.11无线报文。当发现有UDP数据包发往广播或特定组播地址时,它会根据目标端口号(设备固件预先约定好的一个端口,例如 10000)来判断这是否是一个配网包。设备抓取到这个包后,按照预定义的协议格式进行解码,提取出其中的SSID、密码、Token等信息。

2.6  拓展

        这里大概解释一下是如何接收报文的,对于 802.11 协议,MAC 帧数据域是加密的,设备没有连上 WiFi 是无法读取这部分内容的。对于 UDP 广播包,MAC 数据帧中数据部分(MSDU),我们无法知道内容,但是通过对帧的解析我们知道这部分数据的长度,这部分数据是由 20 字节 IPv4 头部+8 字节 UDP 报文头部+UDP 内容组成的 IP 报文,假如 IP 报文长度为 500 字节,则 UDP 内容长度为 500-20-8=472字节,这里我们定义,500 字节称为明文长度。

明文长度:指的是手机APP发送的、未经加密的整个配置数据包的长度(以字节为单位)。

密文长度=明文长度+算法常量,算法常量往往是一个固定值,由 APP 和 WIFI 设备默认。

        假如算法常量是 10。现在手机 APP 要传输 1234 这个数据,只需要在 UDP 报文内容中填充(1234-20(Ipv4 头)-8(UDP 报头)-10(算法常量))个字节即可(内容任意)。

IP 报文总长 1224
Ipv4 头:20 字节
UDP 头:8 字节
UDP 内容:1196 字节
也就是说我们通过 UDP 广播发送 1196 个字节就行,内容任意。

        当设备收到这个 UDP 报文后需要解码,先得到 IP 报文长度 1224,然后我们要加上算法常量 10,得到 1234,因此设备最终获得了 1234 这个数据。

        那么对于设备来说,如何知道这个 UDP 广播包就是 SmartConfig 发出的呢?这里涉及到一个前导码的概念,当设备 WIFI 开启混杂模式时,会在所处环境中快速切换各条信道来抓取每个信道中的数据包,当遇到正在发送前导码数据包的信道时,锁定该信道并继续接收广播数据,直到收到足够的数据来解码出其中的 WiFi 密码然后连接 WiFi,因此前导码一般由几个特殊的字节组成,方便和其他 UDP 包区分。

        假设手机 APP 要发送”test”四个字符,算法常量为 16,流程如下:

  • APP 连续发送 3 个 UDP 广播包,数据为均为前导码。
  • APP 发送 1 个 UDP 广播包,IP 报文数据长度为’t’-16。
  • APP 发送 1 个 UDP 广播包,IP 报文数据长度为’e’-16。
  • APP 发送 1 个 UDP 广播包,IP 报文数据长度为’s’-16。
  • APP 发送 1 个 UDP 广播包,IP 报文数据长度为’t’-16。
  • APP 切换 WIFI 信道重复上述步骤

        上述是数据传输的基本原理,但由于每一家厂商的算法常量、传输内容格式、前导码等都不一样,因此不同厂家之间的 SmartConfig 一般无法通用。

3.  代码编写

3.1  准备工作

        首先找到我们之前移植好的连接WiFi的工程,当然也可以使用自己的,这里只是在其中加入SmartConfig相关的内容,详细可以跳转到:

ESP32入门开发·使用STA模式连接WIFI(过程详解,源码可直接移植)-优快云博客

        将其中有关配网的先删掉,因为此时我们不需要在代码中输入SSID和密码,当前代码:

#include <stdio.h>
#include "nvs_flash.h"          // 非易失性存储(NVS)库,用于保存配置信息
#include "esp_wifi.h"           // ESP32 WiFi功能库
#include "esp_event.h"          // ESP32事件循环处理库
#include "esp_log.h"            // ESP32日志功能库
#include "esp_err.h"            // ESP32错误码定义
#include <string.h>             // 标准字符串操作库
 
#define TAG "sta"  // 日志标签
 
// WiFi和IP事件处理函数
void wifi_event_handle(void* event_handler_arg, esp_event_base_t event_base,
                      int32_t event_id, void* event_data)
{
    if(event_base == WIFI_EVENT)  // 处理WiFi相关事件
    {
        switch (event_id)
        {
        case WIFI_EVENT_STA_START:  // 站模式(STA)启动事件
            esp_wifi_connect();     // 开始连接AP
            break;
        case WIFI_EVENT_STA_CONNECTED:  // 成功连接到AP
            ESP_LOGI(TAG,"ESP32已连接到AP!");
            break;
        case WIFI_EVENT_STA_DISCONNECTED:  // 从AP断开连接
            esp_wifi_connect();     // 尝试重新连接
            ESP_LOGI(TAG,"ESP32连接AP失败! 正在重试...");
            break;
        default:
            break;
        }
    }
    else if(event_base == IP_EVENT)  // 处理IP相关事件
    {
        switch (event_id)
        {
        case IP_EVENT_STA_GOT_IP:  // 成功获取IP地址
            ESP_LOGI(TAG,"ESP32已获取IP地址");
            break;
        
        default:
            break;
        }
    }
}
 
// 主应用程序入口
void app_main(void)
{
    // 初始化NVS(非易失性存储)
    ESP_ERROR_CHECK(nvs_flash_init());
    // 初始化TCP/IP网络接口
    ESP_ERROR_CHECK(esp_netif_init());
    // 创建默认事件循环
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    // 创建默认的WiFi站接口
    esp_netif_create_default_wifi_sta();
    
    // 初始化WiFi配置为默认值
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));
 
    // 注册WiFi事件处理函数
    esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event_handle, NULL);
    // 注册IP事件处理函数
    esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, wifi_event_handle, NULL);
 

 
    // 设置WiFi为站模式(STA)
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    // 启动WiFi
    ESP_ERROR_CHECK(esp_wifi_start());
}

        然后由于我们需要手机端APP进行连接,因此去官网下载APP:

https://www.espressif.com.cn/zh-hans/support/download/apps?keys=&field_technology_tid%5B%5D=20

        这个安卓的点击下载会跳转到github,我们正常使用网络的情况下,有时候能进去有时候进不去,这里我把其保存到百度网盘当中了,自行下载使用:

通过网盘分享的文件:esptouch-v2.0.0.apk
链接: https://pan.baidu.com/s/13iVZ0vcr1YNpB2-sleDqDw?pwd=5866

提取码: 5866 

3.2  头文件包含

        因为我们要使用 SmartConfig 相关的 API 函数因此需要将其头文件包含进来:

#include "esp_smartconfig.h"    // ESP-Touch SmartConfig配网协议库

        此时的完整头文件:

#include <stdio.h>
#include "nvs_flash.h"          // 非易失性存储(NVS)库,用于保存Wi-Fi配置等信息,断电不丢失
#include "esp_wifi.h"           // ESP32 Wi-Fi驱动和配置API
#include "esp_event.h"          // ESP32事件循环库,用于处理Wi-Fi、IP等系统事件
#include "esp_log.h"            // ESP32日志库,用于输出调试和信息日志
#include "esp_err.h"            // 包含ESP32专用的错误码(如ESP_OK, ESP_FAIL)
#include "esp_smartconfig.h"    // ESP-Touch SmartConfig配网协议库
#include "esp_netif.h"          // ESP网络接口库,用于管理TCP/IP栈和网络接口
#include <string.h>             // 标准字符串操作函数,如memset, memcpy, snprintf

3.3  初始化配置

        对于初始化函数,我们将有关NVS和WIFI相关的大概过一遍,详细了解,找到准备工作小节的链接,跳转到WIFI章节查看。

        首先初始化NVS,调用函数 nvs_flash_init() 不过这里我们为了使结果更加严谨稍微增加了一点判断,判断NVS是否被损坏或者版本不兼容,如果是则重新擦除写入:

    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        // 如果NVS分区被损坏或版本不兼容,则擦除并重新初始化
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

        对WIFI相关的初始化操作:

    // 初始化TCP/IP网络接口
    ESP_ERROR_CHECK(esp_netif_init());
    // 创建默认事件循环
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    // 创建默认的WiFi站接口
    esp_netif_create_default_wifi_sta();
    
    // 初始化WiFi配置为默认值
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));
 
    // 注册WiFi事件处理函数
    esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event_handle, NULL);
    // 注册IP事件处理函数
    esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, wifi_event_handle, NULL);
 

        然后增加一个 SmartConfig 事件处理函数,其函数事件为 SC_EVENT ,其监听值有:

/** Smartconfig event declarations */
typedef enum {
    SC_EVENT_SCAN_DONE,                /*!< Station smartconfig has finished to scan for APs */
    SC_EVENT_FOUND_CHANNEL,            /*!< Station smartconfig has found the channel of the target AP */
    SC_EVENT_GOT_SSID_PSWD,            /*!< Station smartconfig got the SSID and password */
    SC_EVENT_SEND_ACK_DONE,            /*!< Station smartconfig has sent ACK to cellphone */
} smartconfig_event_t;

/** @brief smartconfig event base declaration */
ESP_EVENT_DECLARE_BASE(SC_EVENT);

        翻译一下:

/** 智能配置事件声明 */
typedef enum {
SC_EVENT_SCAN_DONE,                /*!< 站点智能配置已完成对 AP 的扫描 */
SC_EVENT_FOUND_CHANNEL,            /*!< 站点智能配置已找到目标 AP 的信道 */
SC_EVENT_GOT_SSID_PSWD,            /*!< 站点智能配置已获取 SSID 和密码 */
SC_EVENT_SEND_ACK_DONE,            /*!< 站点智能配置已向手机发送确认 */
} smartconfig_event_t;
/** @brief 智能配置事件基础声明 */
ESP_EVENT_DECLARE_BASE(SC_EVENT);

        这里我们还是监听所有ID,并且在 wifi_event_handle 中进行处理,因此此时的函数为:

esp_event_handler_register(SC_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handle, NULL);

        然后配备 WIFI 工作模式,启动 WIFI :

    // 设置WiFi为站模式(STA)
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    // 启动WiFi
    ESP_ERROR_CHECK(esp_wifi_start());

        然后我们需要配置 SmartConfig 的协议类型,需要调用 esp_smartconfig_set_type() 函数:

/**
  * @brief     Set protocol type of SmartConfig.
  *
  * @attention If users need to set the SmartConfig type, please set it before calling
  *            esp_smartconfig_start.
  *
  * @param     type  Choose from the smartconfig_type_t.
  *
  * @return
  *     - ESP_OK: succeed
  *     - others: fail
  */
esp_err_t esp_smartconfig_set_type(smartconfig_type_t type);

        翻译一下:

/**
* @brief 设置 SmartConfig 的协议类型。*
* @注意 如果用户需要设置 SmartConfig 类型,请在调用 esp_smartconfig_start 之前进行设置。*
* @param     type  从 smartconfig_type_t 中选择。*
* 返回值
*     - ESP_OK:成功
*     - 其他:失败*/
esp_err_t esp_smartconfig_set_type(smartconfig_type_t type);

        这里我们使用是 ESP-Touch 乐鑫官方提供的协议:

typedef enum {
    SC_TYPE_ESPTOUCH = 0,       /**< protocol: ESPTouch */
    SC_TYPE_AIRKISS,            /**< protocol: AirKiss */
    SC_TYPE_ESPTOUCH_AIRKISS,   /**< protocol: ESPTouch and AirKiss */
    SC_TYPE_ESPTOUCH_V2,        /**< protocol: ESPTouch v2*/
} smartconfig_type_t;

        所以函数:

esp_smartconfig_set_type(SC_TYPE_ESPTOUCH);

        然后启动 SmartConfig 任务:

    // 使用默认配置启动SmartConfig任务
    // 这个任务会在后台运行,监听手机App发送的配网信息包
    smartconfig_start_config_t sc_cfg = SMARTCONFIG_START_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_smartconfig_start(&sc_cfg));

        此时完整的主函数代码,加点注释方便理解,并且将一些函数增加了错误检测:

void app_main(void)
{
    // 1. 初始化非易失性存储(NVS)
    // NVS用于存储Wi-Fi配置、设备参数等,确保断电后配置不丢失
    // ESP_ERROR_CHECK宏会检查返回值,如果不是ESP_OK则打印错误并重启
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        // 如果NVS分区被损坏或版本不兼容,则擦除并重新初始化
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    // 2. 初始化底层TCP/IP协议栈
    ESP_ERROR_CHECK(esp_netif_init());

    // 3. 创建系统默认的事件循环
    // 事件循环是ESP-IDF的核心,用于分发和处理各种系统事件(如Wi-Fi、IP事件)
    ESP_ERROR_CHECK(esp_event_loop_create_default());

    // 4. 创建默认的Wi-Fi Station(工作站)网络接口
    // 这个调用会初始化TCP/IP栈的STA部分,并为Wi-Fi驱动配置好网络接口
    esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
    assert(sta_netif); // 简单检查是否创建成功

    // 5. 初始化Wi-Fi驱动
    // WIFI_INIT_CONFIG_DEFAULT()提供了默认的驱动配置参数(如缓存大小、队列长度等)
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    // 6. 注册事件处理函数
    // 将我们上面定义的wifi_event_handle函数注册到相应的事件上
    ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handle, NULL));
    ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handle, NULL));
    // 注册SmartConfig事件处理函数
    ESP_ERROR_CHECK(esp_event_handler_register(SC_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handle, NULL));

    // 7. 设置Wi-Fi工作模式为站模式(STA),即作为客户端连接路由器
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));

    // 8. 启动Wi-Fi驱动
    // 这将触发WIFI_EVENT_STA_START事件,我们在事件处理函数中会调用esp_wifi_connect()
    ESP_ERROR_CHECK(esp_wifi_start());

    // 9. 配置并启动SmartConfig
    // 设置SmartConfig协议类型为ESP-Touch(乐鑫官方协议)
    // 也可以选择SC_TYPE_ESPTOUCH_AIRKISS(同时支持ESP-Touch和AirKiss,用于兼容微信配网)
    ESP_ERROR_CHECK(esp_smartconfig_set_type(SC_TYPE_ESPTOUCH));

    // 使用默认配置启动SmartConfig任务
    // 这个任务会在后台运行,监听手机App发送的配网信息包
    smartconfig_start_config_t sc_cfg = SMARTCONFIG_START_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_smartconfig_start(&sc_cfg));

    // 打印提示日志,指导用户操作
    ESP_LOGI(TAG, "SmartConfig配网已启动,请使用EspTouch App进行配网...");

    // 主函数返回,但系统会继续在后台运行,处理事件循环中的各种任务
    // 程序的控制权交给了事件处理函数(wifi_event_handle)和SmartConfig任务
}

3.4  事件处理函数

        对于 wifi_event_handle() 函数,其中对于WIFI以及IP相关的事件不用动:

// WiFi和IP事件处理函数
void wifi_event_handle(void* event_handler_arg, esp_event_base_t event_base,
                      int32_t event_id, void* event_data)
{
    if(event_base == WIFI_EVENT)  // 处理WiFi相关事件
    {
        switch (event_id)
        {
        case WIFI_EVENT_STA_START:  // 站模式(STA)启动事件
            esp_wifi_connect();     // 开始连接AP
            break;
        case WIFI_EVENT_STA_CONNECTED:  // 成功连接到AP
            ESP_LOGI(TAG,"ESP32已连接到AP!");
            break;
        case WIFI_EVENT_STA_DISCONNECTED:  // 从AP断开连接
            esp_wifi_connect();     // 尝试重新连接
            ESP_LOGI(TAG,"ESP32连接AP失败! 正在重试...");
            break;
        default:
            break;
        }
    }
    else if(event_base == IP_EVENT)  // 处理IP相关事件
    {
        switch (event_id)
        {
        case IP_EVENT_STA_GOT_IP:  // 成功获取IP地址
            ESP_LOGI(TAG,"ESP32已获取IP地址");
            break;
        
        default:
            break;
        }
    }
}

        在此基础上增加处理 SmartConfig 配网相关事件,我们在声明 SmartConfig 事件的时候知道,我们要监控所有ID,其事件分别是:

/** 智能配置事件声明 */
typedef enum {
SC_EVENT_SCAN_DONE,                /*!< 站点智能配置已完成对 AP 的扫描 */
SC_EVENT_FOUND_CHANNEL,            /*!< 站点智能配置已找到目标 AP 的信道 */
SC_EVENT_GOT_SSID_PSWD,            /*!< 站点智能配置已获取 SSID 和密码 */
SC_EVENT_SEND_ACK_DONE,            /*!< 站点智能配置已向手机发送确认 */
} smartconfig_event_t;
/** @brief 智能配置事件基础声明 */
ESP_EVENT_DECLARE_BASE(SC_EVENT);

        我们对其进行判断,首先创建判断条件,通过 switch 语句对满足条件进行判断:

    // 处理SmartConfig配网相关事件
    else if (event_base == SC_EVENT) {
        switch (event_id) {
            case SC_EVENT_SCAN_DONE:
                break;

            case SC_EVENT_FOUND_CHANNEL:
                break;

            case SC_EVENT_GOT_SSID_PSWD:
                break;

            case SC_EVENT_SEND_ACK_DONE:
                break;

            default:
                break;
        }

        首先对于 SC_EVENT_SCAN_DONE 这里是完成后返回的值,我们这里不做任何处理,就打印一句以完成即可:

            case SC_EVENT_SCAN_DONE:
                // 事件:SmartConfig过程完成
                ESP_LOGI(TAG, "SmartConfig过程完成");
                break;

        同样的 SC_EVENT_FOUND_CHANNEL 找到信道IP,也打印一句,已找到:

            case SC_EVENT_FOUND_CHANNEL:
                // 事件:SmartConfig找到了正确的通信信道
                ESP_LOGI(TAG, "找到SmartConfig信道");
                break;

        其中最重要的事件 SC_EVENT_GOT_SSID_PSWD 成功从手机App接收并解码出SSID和密码,我们需要调用 smartconfig_event_got_ssid_pswd_t() 其是用于 SC_EVENT_GOT_SSID_PSWD 事件的参数结构体,参数如下:

/** Argument structure for SC_EVENT_GOT_SSID_PSWD event */
typedef struct {
    uint8_t ssid[32];           /**< SSID of the AP. Null terminated string. */
    uint8_t password[64];       /**< Password of the AP. Null terminated string. */
    bool bssid_set;             /**< whether set MAC address of target AP or not. */
    uint8_t bssid[6];           /**< MAC address of target AP. */
    smartconfig_type_t type;    /**< Type of smartconfig(ESPTouch or AirKiss). */
    uint8_t token;              /**< Token from cellphone which is used to send ACK to cellphone. */
    uint8_t cellphone_ip[4];    /**< IP address of cellphone. */
} smartconfig_event_got_ssid_pswd_t;

        翻译:

/** 用于 SC_EVENT_GOT_SSID_PSWD 事件的参数结构体 */
typedef struct {
uint8_t ssid[32]; /< AP的SSID。以空字符结尾的字符串。 */
uint8_t password[64]; /< AP的密码。以空字符结尾的字符串。 */
bool bssid_set; /< 是否设置了目标AP的MAC地址。 */
uint8_t bssid[6]; /< 目标AP的MAC地址。 */
smartconfig_type_t type; /< SmartConfig的类型(ESPTouch 或 AirKiss)。 */
uint8_t token; /< 来自手机的Token(令牌),用于向手机发送ACK确认包。 */
uint8_t cellphone_ip[4]; /**< 手机的IP地址。 */
} smartconfig_event_got_ssid_pswd_t;

        我们先调用 wifi_config_t 创建一个空的wifi配置,方便将读取到的 wifi 信息写入,清空结构体:

                wifi_config_t wifi_config = {0}; // 初始化一个空的Wi-Fi配置

                // 安全做法:清空结构体,防止残留数据
                memset(&wifi_config, 0, sizeof(wifi_config));

        对于拷贝SSID和密码的操作,这里我们使用 snprintf 函数,其作用是将一个格式化的字符串安全地写入一个字符数组(缓冲区)中,并确保永远不会超出缓冲区的边界,从而防止缓冲区溢出:

snprintf((char*)wifi_config.sta.ssid, sizeof(wifi_config.sta.ssid), "%s", (char*)evt->ssid);
snprintf((char*)wifi_config.sta.password, sizeof(wifi_config.sta.password), "%s", (char*)evt->password);

拓展:

        snprintf函数原型:

int snprintf(char *str, size_t size, const char *format, ...);
  • str:目标字符数组的指针(你要写入的缓冲区)。
  • size:目标缓冲区的大小(以字节为单位)。这是这个函数安全的关键。
  • format:格式化字符串(指定如何组织输出,和 printf 一样)。
  • ...:可变参数列表(要插入到格式化字符串中的变量)。

        处理BSSID,其作用是如果手机App发送了特定AP的BSSID,则使用它,可以用于在多个相同SSID的网络中指定连接某一个,不过这里我们现在写的历程用不到,加上也可以:

wifi_config.sta.bssid_set = evt->bssid_set; // 标志位,指示BSSID是否有效
if (wifi_config.sta.bssid_set == true) {
  memcpy(wifi_config.sta.bssid, evt->bssid, sizeof(evt->bssid));
}

        然后为了方便正常连接,我们下载断开所有可能得连接:

esp_wifi_disconnect();

        将新的配置保存到NVS当中:

esp_wifi_set_config(WIFI_IF_STA, &wifi_config);

        重新发起连接:

esp_wifi_connect();

        然后就是配网完成后的操作 SC_EVENT_SEND_ACK_DONE ,配网完成后我们就可以释放 SmartConfig 任务,省的一直占用资源:

            case SC_EVENT_SEND_ACK_DONE:
                // 事件:设备已成功向手机App发送了确认(ACK)信号
                // 说明:手机App收到ACK后就知道配网成功,可以提示用户
                ESP_LOGI(TAG, "已向手机发送配网成功确认(ACK)");
                esp_smartconfig_stop(); // 停止SmartConfig任务,释放资源
                break;

        此时完整的时间处理函数代码:

// WiFi事件、IP事件和SmartConfig事件处理函数
// event_handler_arg: 注册时传入的用户自定义参数
// event_base: 事件基(属于哪个大类,如WIFI_EVENT, IP_EVENT)
// event_id: 具体的事件ID(如连接成功、获取IP等)
// event_data: 事件附带的数据,其类型根据event_base和event_id而变化
void wifi_event_handle(void* event_handler_arg, esp_event_base_t event_base,
                      int32_t event_id, void* event_data)
{
    // 处理Wi-Fi相关事件
    if (event_base == WIFI_EVENT) {
        switch (event_id) {
            case WIFI_EVENT_STA_START:
                // 事件:Wi-Fi站模式(STA)启动完成
                // 说明:当调用esp_wifi_start()成功后,底层驱动就绪,可以开始连接了
                ESP_LOGI(TAG, "Wi-Fi STA模式启动成功,开始尝试连接...");
                esp_wifi_connect(); // 发起连接到之前配置的AP(如果已配置)
                break;

            case WIFI_EVENT_STA_CONNECTED:
                // 事件:已成功与路由器(AP)建立链路层连接
                // 注意:此时还没有获取到IP地址
                ESP_LOGI(TAG, "已成功连接到AP!");
                break;

            case WIFI_EVENT_STA_DISCONNECTED:
                // 事件:与AP断开连接(可能是密码错误、信号差、AP关闭等)
                // event_data: 指向wifi_event_sta_disconnected_t结构体,包含断开原因
                ESP_LOGI(TAG, "与AP断开连接,尝试重新连接...");
                esp_wifi_connect(); // 尝试重新连接
                break;

            // 可以在此处添加更多Wi-Fi事件的处理,如扫描完成等
            default:
                break;
        }
    }
    // 处理IP相关事件
    else if (event_base == IP_EVENT) {
        switch (event_id) {
            case IP_EVENT_STA_GOT_IP:
                // 事件:成功从路由器DHCP服务器获取到IP地址
                // event_data: 指向ip_event_got_ip_t结构体,包含IP地址、网关、掩码等信息
                // 说明:只有获取到IP后,设备才能进行网络通信(如连接MQTT服务器)
                ESP_LOGI(TAG, "成功获取到IP地址!网络配置完成。");
                break;

            default:
                break;
        }
    }
    // 处理SmartConfig配网相关事件
    else if (event_base == SC_EVENT) {
        switch (event_id) {
            case SC_EVENT_SCAN_DONE:
                // 事件:SmartConfig过程完成
                ESP_LOGI(TAG, "SmartConfig过程完成");
                break;

            case SC_EVENT_FOUND_CHANNEL:
                // 事件:SmartConfig找到了正确的通信信道
                ESP_LOGI(TAG, "找到SmartConfig信道");
                break;

            case SC_EVENT_GOT_SSID_PSWD:
                // 事件:最重要的的事件!成功从手机App接收并解码出SSID和密码
                // event_data: 指向smartconfig_event_got_ssid_pswd_t结构体
                ESP_LOGI(TAG, "成功接收SSID和密码信息");

                // 将接收到的信息转换为Wi-Fi配置结构体
                smartconfig_event_got_ssid_pswd_t *evt = (smartconfig_event_got_ssid_pswd_t*)event_data;
                wifi_config_t wifi_config = {0}; // 初始化一个空的Wi-Fi配置

                // 安全做法:清空结构体,防止残留数据
                memset(&wifi_config, 0, sizeof(wifi_config));

                // 将接收到的SSID和密码拷贝到配置中
                // 使用snprintf是为了防止字符串溢出,保证安全
                snprintf((char*)wifi_config.sta.ssid, sizeof(wifi_config.sta.ssid), "%s", (char*)evt->ssid);
                snprintf((char*)wifi_config.sta.password, sizeof(wifi_config.sta.password), "%s", (char*)evt->password);

                // 处理BSSID(AP的MAC地址),如果手机App发送了特定AP的BSSID,则使用它
                // 这可以用于在多个相同SSID的网络中指定连接某一个
                wifi_config.sta.bssid_set = evt->bssid_set; // 标志位,指示BSSID是否有效
                if (wifi_config.sta.bssid_set == true) {
                    memcpy(wifi_config.sta.bssid, evt->bssid, sizeof(evt->bssid));
                }

                // 先断开当前任何可能的连接
                esp_wifi_disconnect();
                // 将新的Wi-Fi配置(SSID/密码)设置到Wi-Fi驱动中
                // 这个配置也会被保存到NVS中,下次上电会自动连接
                esp_wifi_set_config(WIFI_IF_STA, &wifi_config);
                // 使用新的配置发起连接
                esp_wifi_connect();
                break;

            case SC_EVENT_SEND_ACK_DONE:
                // 事件:设备已成功向手机App发送了确认(ACK)信号
                // 说明:手机App收到ACK后就知道配网成功,可以提示用户
                ESP_LOGI(TAG, "已向手机发送配网成功确认(ACK)");
                esp_smartconfig_stop(); // 停止SmartConfig任务,释放资源
                break;

            default:
                break;
        }
    }
}

        这里把宏定义标签改一下,方便查看日志信息:

// 定义一个标签,用于在日志输出中标识该模块的日志信息
#define TAG "smartconfig_demo"

3.5  完整工程

#include <stdio.h>
#include "nvs_flash.h"          // 非易失性存储(NVS)库,用于保存Wi-Fi配置等信息,断电不丢失
#include "esp_wifi.h"           // ESP32 Wi-Fi驱动和配置API
#include "esp_event.h"          // ESP32事件循环库,用于处理Wi-Fi、IP等系统事件
#include "esp_log.h"            // ESP32日志库,用于输出调试和信息日志
#include "esp_err.h"            // 包含ESP32专用的错误码(如ESP_OK, ESP_FAIL)
#include "esp_smartconfig.h"    // ESP-Touch SmartConfig配网协议库
#include "esp_netif.h"          // ESP网络接口库,用于管理TCP/IP栈和网络接口
#include <string.h>             // 标准字符串操作函数,如memset, memcpy, snprintf

// 定义一个标签,用于在日志输出中标识该模块的日志信息
#define TAG "smartconfig_demo"

// WiFi事件、IP事件和SmartConfig事件处理函数
// event_handler_arg: 注册时传入的用户自定义参数
// event_base: 事件基(属于哪个大类,如WIFI_EVENT, IP_EVENT)
// event_id: 具体的事件ID(如连接成功、获取IP等)
// event_data: 事件附带的数据,其类型根据event_base和event_id而变化
void wifi_event_handle(void* event_handler_arg, esp_event_base_t event_base,
                      int32_t event_id, void* event_data)
{
    // 处理Wi-Fi相关事件
    if (event_base == WIFI_EVENT) {
        switch (event_id) {
            case WIFI_EVENT_STA_START:
                // 事件:Wi-Fi站模式(STA)启动完成
                // 说明:当调用esp_wifi_start()成功后,底层驱动就绪,可以开始连接了
                ESP_LOGI(TAG, "Wi-Fi STA模式启动成功,开始尝试连接...");
                esp_wifi_connect(); // 发起连接到之前配置的AP(如果已配置)
                break;

            case WIFI_EVENT_STA_CONNECTED:
                // 事件:已成功与路由器(AP)建立链路层连接
                // 注意:此时还没有获取到IP地址
                ESP_LOGI(TAG, "已成功连接到AP!");
                break;

            case WIFI_EVENT_STA_DISCONNECTED:
                // 事件:与AP断开连接(可能是密码错误、信号差、AP关闭等)
                // event_data: 指向wifi_event_sta_disconnected_t结构体,包含断开原因
                ESP_LOGI(TAG, "与AP断开连接,尝试重新连接...");
                esp_wifi_connect(); // 尝试重新连接
                break;

            // 可以在此处添加更多Wi-Fi事件的处理,如扫描完成等
            default:
                break;
        }
    }
    // 处理IP相关事件
    else if (event_base == IP_EVENT) {
        switch (event_id) {
            case IP_EVENT_STA_GOT_IP:
                // 事件:成功从路由器DHCP服务器获取到IP地址
                // event_data: 指向ip_event_got_ip_t结构体,包含IP地址、网关、掩码等信息
                // 说明:只有获取到IP后,设备才能进行网络通信(如连接MQTT服务器)
                ESP_LOGI(TAG, "成功获取到IP地址!网络配置完成。");
                break;

            default:
                break;
        }
    }
    // 处理SmartConfig配网相关事件
    else if (event_base == SC_EVENT) {
        switch (event_id) {
            case SC_EVENT_SCAN_DONE:
                // 事件:SmartConfig过程完成
                ESP_LOGI(TAG, "SmartConfig过程完成");
                break;

            case SC_EVENT_FOUND_CHANNEL:
                // 事件:SmartConfig找到了正确的通信信道
                ESP_LOGI(TAG, "找到SmartConfig信道");
                break;

            case SC_EVENT_GOT_SSID_PSWD:
                // 事件:最重要的的事件!成功从手机App接收并解码出SSID和密码
                // event_data: 指向smartconfig_event_got_ssid_pswd_t结构体
                ESP_LOGI(TAG, "成功接收SSID和密码信息");

                // 将接收到的信息转换为Wi-Fi配置结构体
                smartconfig_event_got_ssid_pswd_t *evt = (smartconfig_event_got_ssid_pswd_t*)event_data;
                wifi_config_t wifi_config = {0}; // 初始化一个空的Wi-Fi配置

                // 安全做法:清空结构体,防止残留数据
                memset(&wifi_config, 0, sizeof(wifi_config));

                // 将接收到的SSID和密码拷贝到配置中
                // 使用snprintf是为了防止字符串溢出,保证安全
                snprintf((char*)wifi_config.sta.ssid, sizeof(wifi_config.sta.ssid), "%s", (char*)evt->ssid);
                snprintf((char*)wifi_config.sta.password, sizeof(wifi_config.sta.password), "%s", (char*)evt->password);

                // 处理BSSID(AP的MAC地址),如果手机App发送了特定AP的BSSID,则使用它
                // 这可以用于在多个相同SSID的网络中指定连接某一个
                wifi_config.sta.bssid_set = evt->bssid_set; // 标志位,指示BSSID是否有效
                if (wifi_config.sta.bssid_set == true) {
                    memcpy(wifi_config.sta.bssid, evt->bssid, sizeof(evt->bssid));
                }

                // 先断开当前任何可能的连接
                esp_wifi_disconnect();
                // 将新的Wi-Fi配置(SSID/密码)设置到Wi-Fi驱动中
                // 这个配置也会被保存到NVS中,下次上电会自动连接
                esp_wifi_set_config(WIFI_IF_STA, &wifi_config);
                // 使用新的配置发起连接
                esp_wifi_connect();
                break;

            case SC_EVENT_SEND_ACK_DONE:
                // 事件:设备已成功向手机App发送了确认(ACK)信号
                // 说明:手机App收到ACK后就知道配网成功,可以提示用户
                ESP_LOGI(TAG, "已向手机发送配网成功确认(ACK)");
                esp_smartconfig_stop(); // 停止SmartConfig任务,释放资源
                break;

            default:
                break;
        }
    }
}

// 主应用程序入口,系统启动后会自动调用此函数
void app_main(void)
{
    // 1. 初始化非易失性存储(NVS)
    // NVS用于存储Wi-Fi配置、设备参数等,确保断电后配置不丢失
    // ESP_ERROR_CHECK宏会检查返回值,如果不是ESP_OK则打印错误并重启
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        // 如果NVS分区被损坏或版本不兼容,则擦除并重新初始化
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    // 2. 初始化底层TCP/IP协议栈
    ESP_ERROR_CHECK(esp_netif_init());

    // 3. 创建系统默认的事件循环
    // 事件循环是ESP-IDF的核心,用于分发和处理各种系统事件(如Wi-Fi、IP事件)
    ESP_ERROR_CHECK(esp_event_loop_create_default());

    // 4. 创建默认的Wi-Fi Station(工作站)网络接口
    // 这个调用会初始化TCP/IP栈的STA部分,并为Wi-Fi驱动配置好网络接口
    esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
    assert(sta_netif); // 简单检查是否创建成功

    // 5. 初始化Wi-Fi驱动
    // WIFI_INIT_CONFIG_DEFAULT()提供了默认的驱动配置参数(如缓存大小、队列长度等)
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    // 6. 注册事件处理函数
    // 将我们上面定义的wifi_event_handle函数注册到相应的事件上
    ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handle, NULL));
    ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handle, NULL));
    // 注册SmartConfig事件处理函数
    ESP_ERROR_CHECK(esp_event_handler_register(SC_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handle, NULL));

    // 7. 设置Wi-Fi工作模式为站模式(STA),即作为客户端连接路由器
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));

    // 8. 启动Wi-Fi驱动
    // 这将触发WIFI_EVENT_STA_START事件,我们在事件处理函数中会调用esp_wifi_connect()
    ESP_ERROR_CHECK(esp_wifi_start());

    // 9. 配置并启动SmartConfig
    // 设置SmartConfig协议类型为ESP-Touch(乐鑫官方协议)
    // 也可以选择SC_TYPE_ESPTOUCH_AIRKISS(同时支持ESP-Touch和AirKiss,用于兼容微信配网)
    ESP_ERROR_CHECK(esp_smartconfig_set_type(SC_TYPE_ESPTOUCH));

    // 使用默认配置启动SmartConfig任务
    // 这个任务会在后台运行,监听手机App发送的配网信息包
    smartconfig_start_config_t sc_cfg = SMARTCONFIG_START_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_smartconfig_start(&sc_cfg));

    // 打印提示日志,指导用户操作
    ESP_LOGI(TAG, "SmartConfig配网已启动,请使用EspTouch App进行配网...");

    // 主函数返回,但系统会继续在后台运行,处理事件循环中的各种任务
    // 程序的控制权交给了事件处理函数(wifi_event_handle)和SmartConfig任务
}

        下载调试看一下:

        可以看到我们的SmartConfig已经启动,先连接一个2.4GHz的网络:

        自行查看自己WIFI的属性,频段要在2.4GHz,不要5GHz的,否则不能用:

        然后打开刚刚下载好的APP,找到EspTouch,点击:

        可以看到自己当前连接的网络,输入密码,记得打开自己的手机热点,然后点击下方确认:

        可以看到此时已经连接上了,并且上方板子还反馈给手机告诉已经连接上了:

        我们来看一下VScode上打印的数据:

        当我们点击完确定后,发送MAC板子找到 SmartConfig 信道,获取到我们所连接的 WiFi 的账号密码等,然后打印成功连接:

        并且此时显示的IP和我们上方显示的IP是一样的:

ESP32学习笔记_时光の尘的博客-优快云博客

物联网应用_时光の尘的博客-优快云博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

时光の尘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值