目录

前言

一、工程实现

1.1 WIFI组件代码展览

scan.c

station.c

scan.h

station.h

mian.c

1.2 组件功能

scan.c — 扫描模块实现

station.c — STA 连接模块实现

scan.h — 扫描模块头文件

station.h — STA 模块头文件

main.c — 应用入口

1.3 代理逻辑说明

1)为什么要在事件循环之后创建默认 STA netif

2)为什么必须在 esp_wifi_start() 之前创建 STA netif

3)返回 netif 指针,方便后续使用

4)station_init 中的主机名和 IP 事件回调

二、结果展示

总结


前言

本文在《ESP-IDF+vscode开发ESP32 联网篇第一讲——WIFI扫描》的基础上,继续完善 ESP32 的联网能力。 本篇重点是实现 Station 终端功能,让 ESP32 作为 Wi-Fi 客户端连接各类无线网络,并通过事件回调和终端命令实现异常处理与自动重连。

在这一讲中,你将看到如何将扫描结果与 STA 连接逻辑结合起来,构建一个可以在控制台中交互式选择 AP 并发起连接的完整示例。


一、工程实现

在 Wi-Fi 网络中,STA(Station)指的是所有连接到无线网络的终端设备,例如手机、电脑、智能家居等。 在本示例中,ESP32 将以 STA 模式工作:先扫描周围 AP,再根据用户在控制台输入的编号和密码完成连接与断开操作。

整个工程围绕一个“WiFi 组件”展开,主要由以下几个源文件组成:

1.1 WIFI组件代码展览

scan.c

负责初始化 WiFi 子系统并完成首次全信道扫描,同时注册 scan 控制台命令。

/* ==========================================================================
 * WiFi 扫描模块 — 实现文件
 *
 * 功能:
 *   1. 初始化 NVS、TCP/IP 协议栈、事件循环、WiFi 子系统
 *   2. 将 WiFi 设为 STA 模式并触发主动扫描
 *   3. 注册 scan 控制台命令供用户手动重新扫描
 *
 * 依赖:
 *   - ESP-IDF: nvs_flash, esp_netif, esp_wifi, esp_event, esp_console, esp_log
 *   - 本组件: scan.h(函数声明)、wifi_event.h(事件处理回调)
 * ========================================================================== */

/* ---------- 头文件 ---------- */
#include "scan.h"                  /* 本模块接口声明 */
#include "wifi_event.h"            /* WiFi 事件处理回调 */

/* ---------- 宏定义 ---------- */

/** 模块日志标签 */
static const char *TAG = "wifi_scan";

/* ======================================================================
 * 控制台命令
 * ====================================================================== */

/**
 * @brief scan 命令回调 — 触发一次全信道扫描
 */
static int scan_cmd_handler(int argc, char **argv)
{
    ESP_LOGI(TAG, "触发重新扫描...");
    esp_err_t ret = esp_wifi_scan_start(NULL, false);
    if (ret != ESP_OK) {
        ESP_LOGE(TAG, "扫描启动失败: %s", esp_err_to_name(ret));
    }
    return 0;
}

/**
 * @brief 注册 scan 控制台命令
 */
static void register_scan_cmd(void)
{
    const esp_console_cmd_t cmd = {
        .command  = "scan",
        .help     = "触发一次 WiFi 全信道扫描,结果通过日志输出",
        .hint     = NULL,
        .func     = &scan_cmd_handler,
    };
    ESP_ERROR_CHECK(esp_console_cmd_register(&cmd));
}

/* ======================================================================
 * 公开 API
 * ====================================================================== */

/**
 * @brief 初始化 WiFi 并触发一次全信道扫描
 *
 * 本函数完成以下步骤:
 *   1. 初始化 NVS(WiFi 校准数据存储依赖)
 *   2. 初始化 TCP/IP 协议栈(esp_netif)
 *   3. 创建默认事件循环
 *   4. 创建默认 WiFi STA 网络接口(必须在 WiFi 初始化之前)
 *   5. 初始化 WiFi 子系统(默认配置)
 *   6. 注册 WiFi 事件回调
 *   7. 设置为 STA 模式并启动 WiFi
 *   8. 发起主动全信道扫描
 *   9. 注册 scan 控制台命令
 */
esp_netif_t *wifi_scan_init(void)
{
    esp_netif_t* sta_netif;
    /* ---- 1. 初始化 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);

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

    /* ---- 3. 创建默认事件循环 ---- */
    ESP_ERROR_CHECK(esp_event_loop_create_default());

    /* ---- 4. 创建默认 WiFi STA 网络接口(DHCP 依赖,必须先于 WiFi 初始化) ---- */
    sta_netif = esp_netif_create_default_wifi_sta();

    /* ---- 5. 初始化 WiFi 子系统(默认配置) ---- */
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    /* ---- 6. 注册 WiFi 事件回调(所有 WiFi 事件) ---- */
    ESP_ERROR_CHECK(
        esp_event_handler_instance_register(WIFI_EVENT,
                                            ESP_EVENT_ANY_ID,
                                            &wifi_event_handler,
                                            NULL,   /* arg: 未使用 */
                                            NULL)); /* instance: 未保存句柄 */

    /* ---- 7. 设置为 STA 模式并启动 WiFi ---- */
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_start());

    /* ---- 8. 发起主动全信道扫描 ---- */
    ESP_ERROR_CHECK(esp_wifi_scan_start(NULL, false));
    ESP_LOGI(TAG, "WiFi 初始化完成,开始扫描 WiFi 网络...");

    /* ---- 9. 注册 scan 控制台命令 ---- */
    register_scan_cmd();
    return sta_netif;
}

station.c

负责 STA 连接控制、自动重连逻辑以及 connect / disconnect 控制台命令。

// WiFi STA 连接模块 — 提供 connect/disconnect 控制台命令
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "station.h"
#include "wifi_event.h"

static const char *TAG = "wifi_station";
void wifi_station_connect(char *ssid, char *password);

// --- 控制台命令回调 ---

// connect <编号> [密码] — 从 scan 列表中选编号连接 WiFi
static int connect_cmd_handler(int argc, char **argv)
{
    // 1. 检查扫描结果
    if (wifi_event.ap_count == 0) {
        printf("错误: 没有可用的扫描结果,请先执行 scan 命令\r\n");
        return 1;
    }

    // 2. 解析编号
    int index = atoi(argv[1]);
    if (index < 1 || index > wifi_event.ap_count) {
        printf("错误: 编号 %d 超出有效范围 (1 ~ %d)\r\n", index, wifi_event.ap_count);
        return 1;
    }

    wifi_ap_record_t *ap = &wifi_event.ap_records[index - 1];
    char *password = (argc >= 3) ? argv[2] : "";

    if (ap->authmode == WIFI_AUTH_OPEN && strlen(password) > 0){
        printf("提示: 所选网络 \"%s\" 为开放网络,密码将被忽略\r\n", (char *)ap->ssid);
    }

    // 3. 重复连接判断
    if (wifi_event.is_connected) {
        if (strcmp(wifi_event.last_ssid, (char *)ap->ssid) == 0) {
            printf("提示: 已连接到 \"%s\",无需重复连接\r\n", wifi_event.last_ssid);
            return 0;
        }
        printf("当前已连接 \"%s\",断开后连接到新网络...\r\n", wifi_event.last_ssid);
        wifi_event.retry_enabled = false;
        esp_wifi_disconnect();
        vTaskDelay(pdMS_TO_TICKS(500));
    }

    // 4. 发起连接
    wifi_station_connect((char *)ap->ssid, password);
    printf("连接中,请稍候... (该过程中不要重复连接)\r\n");
    return 0;
}

// disconnect — 断开当前 WiFi 并禁用自动重连
static int disconnect_cmd_handler(int argc, char **argv)
{
    if (!wifi_event.is_connected) { 
        printf("提示: 当前未连接任何网络\r\n"); return 0; 
    }

    wifi_event.retry_enabled = false;
    esp_err_t ret = esp_wifi_disconnect();
    if (ret != ESP_OK) { 
        ESP_LOGE(TAG, "断开连接失败: %s", esp_err_to_name(ret)); return 1; 
    }
    printf("已断开连接\r\n");
    return 0;
}

// --- 公开 API ---

// 注册 IP 事件回调及 connect/disconnect 控制台命令(须在 wifi_scan_init 之后调用)
void wifi_station_init(esp_netif_t *esp_netif)
{
    esp_netif_set_hostname(esp_netif, "ESP32C6_HK");

    ESP_ERROR_CHECK(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP,
                                                        &wifi_event_handler, NULL, NULL));

    const esp_console_cmd_t connect_cmd = {
        .command = "connect", 
        .help = "连接指定 WiFi 网络(需先执行 scan)",
        .hint = "<编号> [密码]", 
        .func = &connect_cmd_handler,
    };
    ESP_ERROR_CHECK(esp_console_cmd_register(&connect_cmd));

    const esp_console_cmd_t disconnect_cmd = {
        .command = "disconnect", 
        .help = "断开当前 WiFi 连接",
        .hint = NULL, 
        .func = &disconnect_cmd_handler,
    };
    ESP_ERROR_CHECK(esp_console_cmd_register(&disconnect_cmd));
}

// 保存凭据、配置 WiFi 并发起连接,启用自动重连(认证失败不重试)
void wifi_station_connect(char *ssid, char *password)
{
    strncpy(wifi_event.last_ssid, ssid, sizeof(wifi_event.last_ssid) - 1);
    wifi_event.last_ssid[sizeof(wifi_event.last_ssid) - 1] = '\0';

    strncpy(wifi_event.last_password, password, sizeof(wifi_event.last_password) - 1);
    wifi_event.last_password[sizeof(wifi_event.last_password) - 1] = '\0';

    wifi_event.retry_count = 0;

    wifi_config_t wifi_config = {
        .sta = { 
            .scan_method = WIFI_ALL_CHANNEL_SCAN, 
            .failure_retry_cnt = 5,
            .threshold.authmode = WIFI_AUTH_OPEN 
        },
    };

    strncpy((char *)wifi_config.sta.ssid, ssid, sizeof(wifi_config.sta.ssid) - 1);
    strncpy((char *)wifi_config.sta.password, password, sizeof(wifi_config.sta.password) - 1);

    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
    esp_err_t ret = esp_wifi_connect();
    if (ret != ESP_OK) { 
        ESP_LOGE(TAG, "连接启动失败: %s", esp_err_to_name(ret)); 
        wifi_event.retry_enabled = false; 
        return; 
    }
    ESP_LOGI(TAG, "连接请求已发送,目标 SSID: %s", ssid);
}

scan.h

对应的头文件,声明对外暴露的初始化和连接接口。

#ifndef SCAN_H
#define SCAN_H

#include <stdint.h>                /* uint16_t */
#include "esp_wifi.h"              /* wifi_ap_record_t */

/**
 * @brief 初始化 WiFi 子系统并触发首次全信道扫描
 *
 * 完成 NVS、TCP/IP 协议栈、事件循环、WiFi 驱动的初始化,
 * 设置 STA 模式并发起主动扫描。同时注册 scan 控制台命令。
 *
 * @note 必须在 wifi_station_init() 之前调用
 */
esp_netif_t *wifi_scan_init(void);

#endif /* SCAN_H */

station.h

对应的头文件,声明对外暴露的初始化和连接接口。

#ifndef STATION_H
#define STATION_H

/**
 * @brief 配置并发起 WiFi STA 连接
 *
 * 保存连接凭据并调用 esp_wifi_connect()。
 * 内部启用自动重连(最多 3 次),认证失败时不重试。
 *
 * @param[in] ssid     目标 SSID(以 '\0' 结尾)
 * @param[in] password 密码(开放网络可传空字符串)
 */
void wifi_station_connect(char *ssid, char *password);

/**
 * @brief 注册 IP 事件回调及控制台命令
 *
 * 注册 IP_EVENT_STA_GOT_IP 回调以及 connect / disconnect 命令。
 *
 * @note 必须在 wifi_scan_init() 之后调用
 */
void wifi_station_init(void);

#endif /* STATION_H */

mian.c

应用入口,串联 REPL 环境初始化、WiFi 扫描初始化和 STA 功能初始化。

#include <stdio.h>
#include "basic.h"
#include "scan.h"
#include "station.h"

void app_main(void)
{
    esp_log_level_set("wifi", ESP_LOG_WARN); // 设置 WiFi 模块日志级别为 ERROR,减少日志输出
    CONSOLE_REPL_INIT();      // 初始化控制台REPL环境
    esp_netif_t *sta_netif = wifi_scan_init();         // 初始化WiFi子系统(NVS/TCP/IP/事件循环/WiFi驱动),设为STA模式并触发首次全信道扫描,注册scan命令
    wifi_station_init(sta_netif);     // 注册IP事件回调(GOT_IP)及connect/disconnect控制台命令
    while(1)
    {
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

1.2 组件功能

scan.c — 扫描模块实现

scan.c 模块负责完成 NVS、TCP/IP 协议栈、事件循环以及 WiFi 驱动的初始化,并在 STA 模式下主动扫描周围的 AP。 同时,它会为控制台注册一个 scan 命令,方便用户在运行过程中随时重新触发扫描。

代码的主要功能包括:

  • 初始化 NVS,用于存储 WiFi 校准数据和配置。

  • 初始化 esp_netif 网络栈,并创建默认事件循环。

  • 创建默认 WiFi STA 网络接口,为 DHCP 和 IP 分配提供基础。

  • 初始化 WiFi 驱动,注册 WiFi 事件回调,并设置为 STA 模式后启动 WiFi。

  • 发起一次主动全信道扫描,并在控制台注册 scan 命令。通过该命令可以在后续随时重新扫描并查看日志输出结果。

你可以在模块注释中清晰看到每一步初始化流程以及依赖关系,对理解 ESP-IDF 下的 WiFi 启动过程非常有帮助。

station.c — STA 连接模块实现

station.c 模块负责具体的连接与断开控制,它通过注册 connect 和 disconnect 控制台命令,让用户可以基于扫描结果选择要连接的 AP。

模块的核心逻辑包括:

  • connect 命令:

    • 检查是否已有有效的扫描结果,没有则提示先执行 scan。

    • 根据用户输入的编号在扫描列表中找到对应 AP,并处理开放网络与密码网络的差异。

    • 判断是否为重复连接:如果当前已连接同一 SSID,则直接提示无需重复连接;如果要切换到不同网络,则先断开当前连接。随后调用 wifi_station_connect() 发起连接请求。

  • disconnect 命令:

    • 当用户主动断开连接时,关闭自动重连机制,并调用 esp_wifi_disconnect() 完成断开操作。

wifi_station_connect() 函数内部会:

  • 保存当前连接使用的 SSID 和密码,方便自动重连使用。

  • 启用自动重连机制,初始化重连计数。认证失败时不会进行重试,以免陷入错误密码循环。

  • 配置 wifi_config_t 结构体,包括扫描方式、重试次数和认证阈值等,再通过 esp_wifi_set_config() 应用配置并调用 esp_wifi_connect() 发起连接请求。

通过丰富的日志和状态结构体 wifi_event,程序可以在事件回调中对不同异常情况(连接失败、认证错误、意外断开等)做出相应处理,从而保证运行的稳定性。

scan.h — 扫描模块头文件

scan.h 声明了 wifi_scan_init() 接口,用于初始化 WiFi 子系统并触发首次全信道扫描。 该函数会完成 NVS、TCP/IP 协议栈、事件循环以及 WiFi 驱动的初始化,并将 WiFi 设置为 STA 模式后发起主动扫描,同时注册 scan 控制台命令。

需要注意的是:文档中明确说明,必须在 wifi_station_init() 之前调用 wifi_scan_init(),否则后续 IP 事件回调和 STA 连接逻辑无法正常工作。

station.h — STA 模块头文件

station.h 头文件中声明了两个对外函数:

  • wifi_station_connect(ssid, password):配置并发起 WiFi STA 连接,保存连接凭据并启用自动重连机制。

  • wifi_station_init():注册 IP 事件回调(例如 IP_EVENT_STA_GOT_IP)以及 connect / disconnect 控制台命令。

同样需要注意调用时序:必须在 wifi_scan_init() 之后调用 wifi_station_init(),否则 IP 事件回调无法正确绑定到已存在的网络接口。

main.c — 应用入口

main.c 中的 app_main() 函数串联了整个示例的启动流程。 主要步骤如下:csdn

  1. 设置 WiFi 模块的日志级别为 ESP_LOG_ERROR,减少普通信息日志,方便聚焦异常和错误信息。

  2. 初始化控制台 REPL 环境,为后续的 scan、connect、disconnect 等命令提供交互界面。

  3. 调用 wifi_scan_init() 初始化 WiFi 扫描功能,完成基础网络栈和事件循环的搭建,并发起首次扫描。

  4. 调用 wifi_station_init() 注册 STA 相关的 IP 事件回调和控制台命令,使得后续连接逻辑得以生效。

  5. 进入一个简单的 while(1) 循环,通过 vTaskDelay() 保持任务存在,让整个 REPL 环境和事件系统持续运行。

通过这几个步骤,一个完整的“基于控制台命令操作 WiFi 扫描与连接”的示例就搭建完成了。

1.3 代理逻辑说明

本节主要解释 wifi_scan_init() 和 wifi_station_init() 中关键调用顺序背后的原因,尤其是默认 STA 网络接口(netif)的创建时机。

1)为什么要在事件循环之后创建默认 STA netif

在 wifi_scan_init() 中,需要在创建事件循环之后、调用 esp_wifi_start() 之前执行 esp_netif_create_default_wifi_sta() 创建默认 WiFi STA 网络接口。

原因是:

  • esp_netif_create_default_wifi_sta() 内部会自动注册与 WiFi/IP 相关的事件处理器,用于 DHCP、IP 分配等流程。

  • 事件处理器依赖已经存在的默认事件循环,如果在事件循环创建之前调用,会导致注册失败,并触发 ESP_ERROR_CHECK 的断言报错。

因此正确的顺序应该是:

  1. 初始化 esp_netif。

  2. 创建默认事件循环。

  3. 调用 esp_netif_create_default_wifi_sta() 创建 STA netif。

  4. 再初始化和启动 WiFi 驱动。

2)为什么必须在 esp_wifi_start() 之前创建 STA netif

esp_wifi_start() 启动 WiFi 硬件后,固件会立即在事件循环中投递 WIFI_EVENT_STA_START 等事件。 此时,必须已经存在 STA netif 的事件处理器来接收并处理这些事件,否则后续 IP 分配流程会异常。

如果在 esp_wifi_start() 之后才创建 netif,现象表现为:

  • 从 WiFi 的角度看,连接过程似乎是成功的。

  • 但由于 IP 事件处理器没有及时就位,DHCP 和 IP 分配无法顺利完成,wifi_event_handler 永远收不到 IP_EVENT_STA_GOT_IP 事件。

  • 最终体现为“能连上 AP,但一直拿不到 IP 地址”。

所以,在设计本示例时,esp_netif_create_default_wifi_sta() 被明确地放在事件循环创建之后、esp_wifi_start() 之前,这一点对后续的站点连接和 IP 获取至关重要。

3)返回 netif 指针,方便后续使用

为了在后续模块中能够方便地使用已经创建好的 STA netif,本示例将 wifi_scan_init() 的返回值设计为 esp_netif_t *,即返回默认 STA 网络接口的指针。

虽然当前代码中主要通过 esp_netif_get_handle_from_ifkey("WIFI_STA_DEF") 等方式获取句柄,但保留返回值可以让你在扩展工程时更灵活地传递和管理网络接口实例。

4)station_init 中的主机名和 IP 事件回调

在 wifi_station_init() 函数中,首先通过 esp_netif_set_hostname() 设置 STA 接口的主机名,例如示例中使用的 "ESP32C6_HK",这样在路由器的设备列表中更容易识别这块开发板。

随后注册 IP 事件回调:当 STA 成功获取 IP 地址时,会触发 IP_EVENT_STA_GOT_IP,由统一的 wifi_event_handler 进行处理,包括日志输出、状态更新以及后续自动重连逻辑等。

最后注册 connect 和 disconnect 控制台命令,为用户提供一个友好的交互入口,让整个连接流程更易于调试和验证。

5)回调参数 event_data 的调用

在 wifi_event 的回调函数 wifi_event_handler 中 ,有这一句 ip_event_got_ip_t *event = (ip_event_got_ip_t *)event_data; 语句,其中用户并没有传入 event_data 数据,是由 ESP-IDF 的事件系统自动传进来的。

当 Wi-Fi 或 IP 协议栈触发某个事件时,ESP-IDF 会把对应的“事件数据”打包成一个指针;然后把这个指针通过 event_data 传给回调函数;在代码里再把它强制转成正确的结构体类型。

要注意的是 event_data 具体的结构体类型由当前触发的事件ID绝对,例如,当事件ID为WIFI_EVENT_STA_CONNECTED,则event_data的结构体类型为wifi_event_sta_connected_t;当事件ID为IP_EVENT_STA_GOT_IP,则event_data的结构体类型为ip_event_got_ip_t;实际使用中要正确强制转换。

另外这种机制不是只在 Wi-Fi/IP 协议里出现,而是 ESP-IDF 的事件系统里通用的一种机制。

二、结果展示


总结

本篇在上一讲 WiFi 扫描的基础上,完成了 ESP32 作为 STA 终端的连接控制逻辑,实现了从“发现网络”到“选择并连接”的完整流程。 通过合理安排事件循环、netif 创建和 WiFi 启动的时序,示例保证了 DHCP 和 IP 分配过程的正确性。

同时,借助丰富的事件回调和控制台命令,程序具备了基本的异常处理和自动重连能力,非常适合作为后续扩展(如 HTTP/MQTT 客户端、云端接入等)的基础工程模板。

你可以在此基础上继续扩展:例如为每个 AP 记录信号强度排序、为不同网络配置不同的安全策略,或在获得 IP 后自动启动上层协议任务,使 ESP32 真正成为一个“可交互、可联网”的物联网节点。

更多推荐