ESP-ADF wifi_service子模块esp_wifi_setting配网之airkiss_config详解

SP-ADF wifi_service子模块esp_wifi_setting配网之airkiss_config详解

版本信息: v2.7-65-gcf908721

本章节分析的源码位于 /components/wifi_service/airkiss_config/airkiss_config.c 文件和 /components/wifi_service/include/airkiss_config.h 文件

模块概览

AirKiss Config在ESP-ADF的wifi_service组件架构中处于配网接口实现层,下图展示了其在整体架构中的位置:

wifi_service组件架构
esp_wifi_setting
(配网接口抽象层)
wifi_service
(WiFi服务核心)
wifi_ssid_manager
(SSID管理器)
smart_config
(ESP Touch配网)
blufi_config
(蓝牙配网)
airkiss_config
(微信配网)
softap_config
(软AP配网)
NVS存储
(永久保存WiFi信息)
智能选择
(最佳网络选择算法)

从图中可以看出,airkiss_config是esp_wifi_setting抽象接口的一种具体实现,与其他配网方式(smart_config、blufi_config和softap_config)并列。配网完成后,获取的WiFi凭证会被保存到wifi_ssid_manager模块中进行管理。

ESP-ADF的airkiss_config模块是基于微信硬件平台开发的AirKiss协议实现的一种WiFi配网方式,它遵循esp_wifi_setting接口规范,实现了通过微信小程序或APP向ESP设备传输WiFi配置信息的功能。AirKiss是一种无需显示配置界面的WiFi配网技术,专门为微信智能硬件生态系统设计。

AirKiss的主要工作原理是:

  1. ESP设备进入混杂模式(Promiscuous Mode),监听所有WiFi数据包
  2. 用户在微信小程序或APP上输入WiFi的SSID和密码
  3. 微信APP将这些信息编码成特殊的数据包发送到空气中
  4. ESP设备接收并解码这些数据包,获取到WiFi的SSID和密码
  5. ESP设备使用获取到的信息连接WiFi网络
  6. 配网成功后,设备通过UDP广播通知微信APP配网已完成

数据结构

airkiss_config模块定义了以下关键数据结构:

/**
 * @brief AirKiss局域网数据包
 */
typedef struct {
    void *appid;        /*!< 应用标识符数据 */
    void *deviceid;     /*!< 设备标识符数据 */
} airkiss_lan_pack_param_t;

/**
 * @brief AirKiss配置信息
 */
typedef struct {
    airkiss_lan_pack_param_t    lan_pack;                /*!< 用户局域网包参数 */
    bool                        ssdp_notify_enable;      /*!< 通知使能标志 */
    char                        *aes_key;                /*!< AirKiss AES密钥数据 */
} airkiss_config_info_t;

// 默认配置宏
#define AIRKISS_CONFIG_INFO_DEFAULT() { \
    .lan_pack = { \
        .appid = NULL, \
        .deviceid = NULL, \
    }, \
    .ssdp_notify_enable = true, \
    .aes_key = NULL, \
}

/**
 * @brief AirKiss内部通知参数结构体
 */
typedef struct {
    airkiss_lan_pack_param_t    lan_pack;         /*!< 局域网包参数,包含应用ID和设备ID */
    bool                        ssdp_notify_enable; /*!< SSDP通知开关,控制是否在配网后发送局域网通知 */
    char                        *aes_key;         /*!< AES加密密钥,用于增强配网安全性 */
} airkiss_notify_para_t;

这些结构体用于:

  • airkiss_lan_pack_param_t: 定义AirKiss局域网协议包的参数,包括应用ID和设备ID
  • airkiss_config_info_t: 定义AirKiss配置信息,包括局域网包参数、是否启用SSDP通知以及AES密钥
  • airkiss_notify_para_t: 内部使用的结构体,与airkiss_config_info_t结构相同,用于存储配置信息

常量和全局变量

模块中定义了一系列重要的常量和全局变量:

// 调试开关
#define AIRKISS_DEBUG_ON                0

// 任务优先级和栈大小
#define AIRKISS_NOTIFY_TASK_PRIORITY    3
#define AIRKISS_NOTIFY_TASK_STACK_SIZE  4096
#define AIRKISS_DEFAULT_LAN_PORT        12476

#define AIRKISS_ACK_TASK_PRIORITY       2
#define AIRKISS_ACK_TASK_STACK_SIZE     4096
#define AIRKISS_ACK_PORT                10000

// 通道切换相关参数
#define AIRKISS_CHANNEL_CHANGE_PERIOD   130  // 通道切换周期(毫秒)
#define AIRKISS_MAX_CHANNEL_NUM         17   // 最大通道数
#define AIRKISS_MIN_RSSI                -90  // 最小接收信号强度指示

// 全局变量
static esp_wifi_setting_handle_t air_setting_handle;  // 配网接口句柄
static uint8_t s_sniffer_stop_flag = 1;               // 嗅探停止标志
static int s_cur_chan_idx = AIRKISS_MAX_CHANNEL_NUM - 1;  // 当前通道索引
static airkiss_context_t *ak_ctx;                     // AirKiss上下文
static uint8_t ak_random_num = 0;                     // AirKiss随机数
static esp_timer_handle_t channel_change_timer;       // 通道切换定时器
static TaskHandle_t air_answer_task_handle;           // ACK任务句柄

这些常量控制着AirKiss配网的行为,如通道切换速度、任务优先级、UDP端口等。

通道表结构

模块定义了WiFi通道配置表:

// 通道结构体
typedef struct {
    bool ap_exist;                // 该通道是否存在AP
    uint8_t primary_chan;         // 主信道号
    wifi_second_chan_t second_chan; // 次信道类型
} airkiss_chan_t;

// 通道表
static airkiss_chan_t s_airkiss_chan_tab[AIRKISS_MAX_CHANNEL_NUM] = {
    {false, 1, WIFI_SECOND_CHAN_ABOVE},
    {false, 2, WIFI_SECOND_CHAN_ABOVE},
    // ... 更多通道配置
};

这个通道表定义了ESP设备在嗅探模式下需要扫描的所有WiFi通道及其配置,AirKiss会在这些通道之间切换以监听配网数据包。

AirKiss配置

模块设置了AirKiss库需要的配置:

// AirKiss配置
const airkiss_config_t ak_conf = {
    (airkiss_memset_fn) &memset,
    (airkiss_memcpy_fn) &memcpy,
    (airkiss_memcmp_fn) &memcmp,
    (airkiss_printf_fn) &printf,
};

这里将标准C库函数封装为AirKiss库需要的函数指针,以供AirKiss库内部使用。

esp_wifi_setting生命周期实现

airkiss_config模块作为esp_wifi_setting接口的实现,遵循了接口定义的生命周期,下面我们按照生命周期的各个阶段详细分析其实现。

1. 创建和初始化阶段

AirKiss配网方式通过airkiss_config_create函数创建,此函数是遵循esp_wifi_setting接口规范的实现。

/**
 * @brief 创建AirKiss配网实例
 * 
 * 该函数完成以下主要操作:
 * 1. 创建esp_wifi_setting接口实例
 * 2. 分配并初始化AirKiss配置参数
 * 3. 处理AES密钥的内存分配
 * 4. 注册配网实现函数
 * 
 * @param info AirKiss配置信息,包含应用ID、设备ID、SSDP通知设置和AES密钥
 * @return esp_wifi_setting_handle_t 配网接口句柄,失败时返回NULL
 */
esp_wifi_setting_handle_t airkiss_config_create(airkiss_config_info_t *info)
{
    // 1. 创建esp_wifi_setting接口实例
    air_setting_handle = esp_wifi_setting_create("airkiss_config");
    AUDIO_MEM_CHECK(TAG, air_setting_handle, return NULL);
    
    // 2. 分配内部配置结构体内存
    airkiss_notify_para_t *cfg = audio_calloc(1, sizeof(airkiss_notify_para_t));
    AUDIO_MEM_CHECK(TAG, cfg, {
        audio_free(air_setting_handle);
        return NULL;
    });
    
    // 3. 把用户配置复制到内部结构体
    cfg->lan_pack.appid = info->lan_pack.appid;
    cfg->lan_pack.deviceid = info->lan_pack.deviceid;
    if (info->aes_key) {
        // 如果指定AES密钥,为其分配内存并复制
        cfg->aes_key = audio_strdup(info->aes_key);
    }
    cfg->ssdp_notify_enable = info->ssdp_notify_enable;
    
    // 4. 将配置结构体设置为接口的用户数据
    esp_wifi_setting_set_data(air_setting_handle, cfg);
    
    // 5. 注册配网实现函数:启动、停止和清理
    esp_wifi_setting_register_function(air_setting_handle, airkiss_start, airkiss_stop, airkiss_teardown);
    
    // 6. 返回配网接口句柄
    return air_setting_handle;
}

创建阶段的流程如下:

调用airkiss_config_create(info)
创建esp_wifi_setting接口: esp_wifi_setting_create
接口创建成功?
返回NULL
分配配置结构体内存
内存分配成功?
释放接口内存并返回NULL
初始化配置结构体
为AES密钥分配内存(如果有)
设置用户数据: esp_wifi_setting_set_data
注册配网函数: esp_wifi_setting_register_function
返回配网接口句柄

创建阶段的关键点:

  1. 使用esp_wifi_setting_create创建通用接口
  2. 分配和初始化配置结构体
  3. 为AES密钥分配内存并复制(如果提供了密钥)
  4. 通过esp_wifi_setting_set_data存储配置
  5. 注册airkiss_startairkiss_stopairkiss_teardown函数实现配网控制

2. 启动配网阶段

AirKiss的启动由airkiss_start函数实现,该函数在WiFi服务调用esp_wifi_setting_start时被触发。

/**
 * @brief 启动AirKiss配网
 * 
 * 该函数完成以下主要操作:
 * 1. 创建并初始化AirKiss上下文
 * 2. 扫描可用WiFi通道
 * 3. 设置信道切换定时器
 * 4. 启用WiFi混杂模式接收数据包
 * 
 * @param handle 配网接口句柄
 * @return esp_err_t 成功返回ESP_OK,失败返回ESP_FAIL
 */
static esp_err_t airkiss_start(esp_wifi_setting_handle_t handle)
{
    int chan_idx = 0;
    esp_err_t res = ESP_OK;
    
    // 输出日志,显示AirKiss版本信息
    ESP_LOGI(TAG, "Start airkiss, Version:%s", airkiss_version());
    
    // 1. 创建AirKiss上下文并分配内存
    ak_ctx = audio_calloc(1, sizeof(airkiss_context_t));
    if (ak_ctx == NULL) {
        ESP_LOGE(TAG, "Airkiss context allocate fail");
        return ESP_FAIL;
    }
    
    // 2. 初始化AirKiss上下文,设置基本配置
    res = airkiss_init(ak_ctx, &ak_conf);
    if (res < 0) {
        audio_free(ak_ctx);
        ESP_LOGE(TAG, "Airkiss init failed!");
        return ESP_FAIL;
    }
    
    // 3. 获取用户配置数据
    airkiss_notify_para_t *para = esp_wifi_setting_get_data(handle);
    
    // 4. 如果启用了SSDP通知并提供了AES密钥,设置加密密钥
    if (para->ssdp_notify_enable && para->aes_key) {
        airkiss_set_key(ak_ctx, (uint8_t *)para->aes_key, strlen(para->aes_key));
    }
    
    // 5. 断开当前的WiFi连接,准备进入配网模式
    esp_wifi_disconnect();
    
    // 6. 扫描并记录周围环境中的WiFi信道
    airkiss_wifi_scan_ap();
    
    // 7. 选择一个初始信道进行监听
    chan_idx = airkiss_get_next_channel_idx();
    esp_wifi_set_channel(s_airkiss_chan_tab[chan_idx].primary_chan,
                          s_airkiss_chan_tab[chan_idx].second_chan);
    
    // 8. 创建并启动信道切换定时器,定时切换信道寻找配网数据包
    esp_timer_create_args_t create_args = {
        .callback = &channel_change_callback,  // 设置回调函数
        .arg = NULL,                        // 不传递参数
        .name = "channel_change",           // 定时器名称
    };
    esp_timer_create(&create_args, &channel_change_timer);
    
    // 启动周期性定时器,间隔为AIRKISS_CHANNEL_CHANGE_PERIOD毫秒
    esp_timer_start_periodic(channel_change_timer, AIRKISS_CHANNEL_CHANGE_PERIOD * 1000);

    // 9. 设置WiFi混杂模式接收数据包
    esp_wifi_set_promiscuous(false);  // 先关闭混杂模式,防止多次设置
    esp_wifi_set_promiscuous_rx_cb(wifi_promiscuous_rx);  // 设置混杂模式回调函数
    esp_wifi_set_promiscuous(true);   // 开启混杂模式
    
    // 10. 清除嗅探停止标志,允许数据包接收
    s_sniffer_stop_flag = 0;

    return res;
}

启动配网阶段的流程如下:

调用airkiss_start(handle)
创建AirKiss上下文
分配成功?
返回ESP_FAIL
初始化AirKiss上下文: airkiss_init
初始化成功?
释放上下文并返回ESP_FAIL
获取配置: esp_wifi_setting_get_data
如果启用AES加密,设置密钥
断开当前WiFi连接
扫描可用WiFi通道: airkiss_wifi_scan_ap
选择初始通道并设置
创建通道切换定时器
启动周期性通道切换
设置WiFi混杂模式回调函数
启用WiFi混杂模式
清除嗅探停止标志
返回ESP_OK

启动阶段的关键点:

  1. 创建和初始化AirKiss上下文
  2. 设置AES密钥(如果启用加密)
  3. 扫描可用WiFi通道并选择初始通道
  4. 创建通道切换定时器,定期切换WiFi通道以寻找配网数据包
  5. 设置WiFi混杂模式并注册回调函数,用于捕获和处理所有WiFi数据包
WiFi通道扫描

AirKiss配网过程中,需要先扫描环境中存在的AP信号,以确定哪些通道有活跃的AP:

/**
 * @brief 扫描WiFi通道上的AP
 * 
 * 该函数完成以下主要操作:
 * 1. 执行WiFi扫描获取周围AP信息
 * 2. 根据AP的信号强度标记通道表中的通道状态
 */
static void airkiss_wifi_scan_ap(void)
{
    // 初始化扫描相关变量
    wifi_scan_config_t *scan_config = NULL;  // WiFi扫描配置
    uint16_t ap_num = 0;                     // 所发现的AP数量
    wifi_ap_record_t *ap_record = NULL;      // AP记录结构数组
    
    // 为扫描配置分配内存
    scan_config = audio_calloc(1, sizeof(wifi_scan_config_t));
    if (scan_config == NULL) {
        ESP_LOGE(TAG, "scan config allocate fail");
        return;
    }
    
    // 进行两次扫描,增加扫描成功率
    for (int scan_cnt = 0; scan_cnt < 2; scan_cnt++) {
        // 清空扫描配置结构体
        bzero(scan_config, sizeof(wifi_scan_config_t));
        
        // 设置扫描选项,包括隐藏SSID的AP
        scan_config->show_hidden = true;
        
        // 启动WiFi扫描,使用阻塞模式(第二个参数为true)
        esp_wifi_scan_start(scan_config, true);
        
        // 获取扫描到的AP数量
        esp_wifi_scan_get_ap_num(&ap_num);
        
        // 如果发现了AP,处理扫描结果
        if (ap_num) {
            // 为AP记录分配内存
            ap_record = audio_calloc(1, ap_num * sizeof(wifi_ap_record_t));
            if (ap_record == NULL) {
                ESP_LOGE(TAG, "ap record allocate fail");
                continue;  // 内存分配失败时跳过当前扫描周期
            }
            
            // 获取详细的AP记录
            esp_wifi_scan_get_ap_records(&ap_num, ap_record);
            
            // 根据AP信息标记通道表中存在AP的通道
            for (int i = 0; i < AIRKISS_MAX_CHANNEL_NUM; i++) {
                // 如果该通道已经被标记为存在AP,跳过
                if (s_airkiss_chan_tab[i].ap_exist == true) {
                    continue;
                }
                
                // 遍历所有发现的AP
                for (int j = 0; j < ap_num; j++) {
                    // 如果AP信号强度低于最小阈值,跳过
                    // 这是为了过滤信号太弱的AP,避免影响配网成功率
                    if (ap_record[j].rssi < AIRKISS_MIN_RSSI) {
                        continue;
                    }
                    
                    // 如果AP的主信道与通道表中的信道匹配,标记为存在
                    if (ap_record[j].primary == s_airkiss_chan_tab[i].primary_chan) {
                        s_airkiss_chan_tab[i].ap_exist = true;
                    }
                }
            }
            
            // 释放AP记录内存
            audio_free(ap_record);
        }
    }
    
    // 释放扫描配置内存
    audio_free(scan_config);
}
通道切换机制

AirKiss在配网过程中需要在不同WiFi通道间切换,以寻找配网数据包:

/**
 * @brief 获取下一个要切换的通道索引
 */
/**
 * @brief 获取下一个要切换的WiFi通道索引
 * 
 * 该函数在当前信道索引基础上寻找下一个存在AP的通道,
 * 确保通道切换只在有实际AP活动的信道上进行,提高配网效率
 */
static int airkiss_get_next_channel_idx(void)
{
    // 循环查找下一个存在AP的通道
    do {
        // 通道循环处理,当到达最后一个通道时跳回第一个通道
        if (s_cur_chan_idx >= AIRKISS_MAX_CHANNEL_NUM - 1) {
            s_cur_chan_idx = 0;  // 重置到第一个通道
        } else {
            s_cur_chan_idx++;    // 移到下一个通道
        }
        
        // 当通道不存在AP时继续循环,直到找到存在AP的通道
        // 这确保了只在有活跃AP的通道上进行监听,避免浪费时间在没有信号的信道上
    } while (s_airkiss_chan_tab[s_cur_chan_idx].ap_exist == false);
    
    // 返回找到的存在AP的通道索引
    return s_cur_chan_idx;
}

/**
 * @brief 通道切换定时器回调函数
 * 
 * 该函数作为定时器回调,定时切换WiFi通道,以寻找配网数据包
 */
static void channel_change_callback(void *timer_arg)
{
    int chan_idx = 0;
    
    // 如果嗅探已停止,不执行通道切换
    if (s_sniffer_stop_flag == 1) {
        return;
    }
    
    // 获取下一个要切换的通道索引
    chan_idx = airkiss_get_next_channel_idx();
    
    // 记录日志,显示当前主通道和辅助通道
    ESP_LOGD(TAG, "ch%d-%d", s_airkiss_chan_tab[chan_idx].primary_chan,
             s_airkiss_chan_tab[chan_idx].second_chan);
    
    // 设置WiFi模块切换到新的通道
    esp_wifi_set_channel(s_airkiss_chan_tab[chan_idx].primary_chan,
                         s_airkiss_chan_tab[chan_idx].second_chan);
    
    // 通知AirKiss库已经切换了通道,重置内部状态
    airkiss_change_channel(ak_ctx);
}

通道切换回调函数会在AirKiss配网过程中周期性地被调用,实现跨通道扫描寻找配网数据包的功能。每次切换通道时,需要同时通知ESP-IDF的WiFi驱动和AirKiss库,确保两者保持状态同步。

3. 数据包接收处理阶段

AirKiss在混杂模式下会损抓并分析所有经过的WiFi数据包,这是通过wifi_promiscuous_rx回调函数实现的:

/**
 * @brief WiFi混杂模式数据包接收回调函数
 * 
 * 该函数处理在混杂模式下捕获的所有WiFi数据包
 * 并将其传递给AirKiss库进行解析
 * 
 * @param buf 捕获的数据包缓冲区
 * @param type 数据包类型
 */
static void wifi_promiscuous_rx(void *buf, wifi_promiscuous_pkt_type_t type)
{
    // 将输入的缓冲区转换为WiFi数据包类型
    wifi_promiscuous_pkt_t *pkt = (wifi_promiscuous_pkt_t *) buf;
    uint8_t *payload;  // 数据包负载指针
    uint16_t len;      // 负载长度
    int ret;           // AirKiss处理返回值
    
    // 如果嗅探已停止或缓冲区为空,直接跳出
    if (s_sniffer_stop_flag == 1 || buf == NULL) {
        return;
    }
    
    // 获取数据包的负载部分和长度
    payload = pkt->payload;  // 数据包内容(WiFi帧数据)
    len = pkt->rx_ctrl.sig_len;  // 信号长度
    
    // 将数据包传递给AirKiss库进行解析
    ret = airkiss_recv(ak_ctx, payload, len);
    
    // 根据不同的返回状态进行处理
    if (ret == AIRKISS_STATUS_CHANNEL_LOCKED) {
        // 状态:成功锁定到正确的信道(发现了配网数据包的信道)
        
        // 停止和删除通道切换定时器,因为已找到正确信道,不需要继续切换
        esp_timer_stop(channel_change_timer);
        esp_timer_delete(channel_change_timer);
        channel_change_timer = NULL;
        
        ESP_LOGI(TAG, "AIRKISS_STATUS_CHANNEL_LOCKED");  // 记录日志
        
    } else if (ret == AIRKISS_STATUS_COMPLETE) {
        // 状态:配网完成(已成功获取到完整的WiFi配置信息)
        
        // 关闭WiFi混杂模式,不再需要损抓数据包
        esp_wifi_set_promiscuous(false);
        
        // 设置嗅探停止标志,避免其他数据包继续被处理
        s_sniffer_stop_flag = 1;
        
        // 调用完成函数处理获取到的WiFi配置信息
        airkiss_finish();
        
        ESP_LOGI(TAG, "AIRKISS_STATUS_COMPLETE");  // 记录日志
        
    } else {
        // 其他状态(如正在解析、信道切换、等待更多数据等)
        // 此处用于调试时可以展示当前状态,通常保持注释
        //ESP_LOGI(TAG, "AIRKISS_STATUS: %d", ret);
    }
}

数据包处理流程如下:

AIRKISS_STATUS_CHANNEL_LOCKED
AIRKISS_STATUS_COMPLETE
其他状态
调用wifi_promiscuous_rx(buf, type)
是否停止嗅探或buf为NULL?
返回不处理
提取数据包负载和长度
调用airkiss_recv分析数据包
返回值是什么?
停止和删除通道切换定时器
记录通道锁定日志
关闭WiFi混杂模式
设置嗅探停止标志
调用airkiss_finish完成配网
记录配网完成日志
继续WiFi数据包监听

数据包处理阶段的关键点:

  1. 损获每个WiFi数据包并交给AirKiss库处理
  2. 当AirKiss返回AIRKISS_STATUS_CHANNEL_LOCKED时,表示已找到配网信道,停止通道切换
  3. 当AirKiss返回AIRKISS_STATUS_COMPLETE时,表示已完成配网信息收集,进入完成阶段

4. 配网完成阶段

当AirKiss成功解析出配网信息后,会调用airkiss_finish函数完成配网过程:

/**
 * @brief AirKiss配网完成处理
 * 
 * 该函数从 AirKiss上下文中获取解析出的WiFi连接信息,
 * 并通知WiFi服务进行连接
 */
static void airkiss_finish(void)
{
    // 声明变量
    airkiss_result_t result;    // AirKiss解析结果结构体
    wifi_config_t wifi_config;  // WiFi配置结构体
    int err;                    // 错误返回值
    
    // 从 AirKiss上下文中获取解析结果
    err = airkiss_get_result(ak_ctx, &result);
    
    // 如果成功获取到结果
    if (err == 0) {
        // 输出获取到的WiFi信息日志(SSID、密码、长度以及随机数)
        ESP_LOGI(TAG,
                 "ssid = \"%s\", pwd = \"%s\", ssid_length = %d, pwd_length = %d, random = %x",
                 result.ssid, result.pwd, result.ssid_length, result.pwd_length,
                 result.random);
        
        // 保存随机数,用于后续ACK应答发送(向配网者确认配网成功)
        ak_random_num = result.random;
        
        // 初始化WiFi配置结构体并填充获取到的SSID和密码
        bzero(&wifi_config.sta, sizeof(wifi_sta_config_t));  // 清零WiFi站点配置结构
        memcpy(wifi_config.sta.ssid, result.ssid, result.ssid_length);  // 复制SSID
        memcpy(wifi_config.sta.password, result.pwd, result.pwd_length);  // 复制密码

        // 如果有有效的配网接口句柄,通知WiFi服务
        if (air_setting_handle) {
            // 将解析出的WiFi配置信息通知给WiFi服务,触发连接过程
            esp_wifi_setting_info_notify(air_setting_handle, &wifi_config);
        }
    } else {
        // 获取结果失败时记录错误日志
        ESP_LOGI(TAG, "airkiss_get_result() failed !");
    }
    
    // 归还AirKiss上下文内存
    audio_free(ak_ctx);
    ak_ctx = NULL;  // 避免悬空指针
}

当WiFi服务建立连接后,会通过airkiss_teardown函数进行清理工作:

/**
 * @brief AirKiss配网完成后的清理工作
 * 
 * 该函数在WiFi成功连接后被调用,负责:
 * 1. 发送ACK消息给配网者
 * 2. 如果启用了SSDP通知,进行局域网广播
 * 
 * @param handle 配网接口句柄
 * @param arg WiFi配置信息
 * @return esp_err_t 始终返回ESP_OK
 */
static esp_err_t airkiss_teardown(esp_wifi_setting_handle_t handle, wifi_config_t *arg)
{
    ESP_LOGI(TAG, "AirKiss teardown process starting: WiFi connected successfully");
    
    // 获取配网参数,包含通知相关配置
    airkiss_notify_para_t *para = esp_wifi_setting_get_data(handle);
    ESP_LOGD(TAG, "Retrieved notification parameters from setting handle");
    
    // 发送ACK应答消息给配网设备,确认配网成功
    // 这使配网设备(通常是手机)知道设备已成功连接到指定WiFi
    ESP_LOGI(TAG, "Sending ACK response to the configuring device");
    airkiss_answer();
    
    // 如果启用了SSDP通知功能,发送局域网广播
    // 使局域网内的其他设备(特别是配网APP)能够发现该设备
    if (para->ssdp_notify_enable) {
        ESP_LOGI(TAG, "SSDP notification enabled, broadcasting device presence");
        airkiss_ssdp_notify(&para->lan_pack);
    } else {
        ESP_LOGD(TAG, "SSDP notification disabled, skipping broadcast");
    }
    
    ESP_LOGI(TAG, "AirKiss teardown completed successfully");
    return ESP_OK;
}
ACK发送机制

当配网成功后,需要向配网者发送ACK确认消息:

/**
 * @brief 向配网设备发送ACK确认任务
 * 
 * 该函数在新线程中运行,通过UDP广播方式向配网设备(手机等)发送确认包,
 * 告知配网设备配网成功。会在约10秒内发送多次确认包以提高可靠性。
 */
static void airkiss_send_ack_task(void *pvParameters)
{
    // 初始化网络地址相关变量
    int remote_addr_len = sizeof(struct sockaddr_in);  // 远程地址长度
    struct sockaddr_in remote_addr;                    // 远程地址结构(目标广播地址)
    struct sockaddr_in send_addr;                      // 本地发送地址结构
    int send_sock = -1, ret;                           // 发送套接字和返回值
    int i = 0;                                          // 循环计数器

    // 初始化远程地址结构,设置为广播地址
    bzero(&remote_addr, sizeof(struct sockaddr_in));  // 清零内存
    remote_addr.sin_family = AF_INET;                // 使用IPv4协议
    remote_addr.sin_addr.s_addr = INADDR_BROADCAST;   // 设置为广播地址255.255.255.255
    remote_addr.sin_port = htons(AIRKISS_ACK_PORT);    // 设置目标端口(通常为10000)
    remote_addr.sin_len = remote_addr_len;            // 设置地址长度

    // 创建UDP套接字,如果失败则每秒重试一次
    do {
        send_sock = socket(AF_INET, SOCK_DGRAM, 0);  // 创建UDP套接字
        if (send_sock == -1) {
            ESP_LOGE(TAG, "Failed to create socket");  // 记录错误日志
            vTaskDelay((portTickType)(1000 / portTICK_RATE_MS));  // 延时1秒
        }
    } while (send_sock == -1);  // 直到成功创建套接字

    // 初始化本地发送地址结构
    bzero(&send_addr, sizeof(struct sockaddr_in));  // 清零内存
    send_addr.sin_family = AF_INET;                // 使用IPv4协议
    send_addr.sin_addr.s_addr = INADDR_ANY;         // 使用任意本地地址
    // send_addr.sin_addr.s_addr = 0;                // 另一种表达方式(已注释)
    send_addr.sin_port = 0;                         // 使用系统自动分配的端口

    // 将套接字绑定到本地地址
    ret = bind(send_sock, (struct sockaddr *)&send_addr, sizeof(send_addr));
    if (ret) {
        // 绑定失败,记录错误日志并跳转到清理代码
        ESP_LOGE(TAG, "Failed to bind socket, errno: %d", airkiss_get_errno(send_sock));
        goto _exit;
    }

    // 设置套接字为广播模式
    int flag = 1;
    ret = setsockopt(send_sock, SOL_SOCKET, SO_BROADCAST, &flag, sizeof(flag));
    if (ret) {
        // 设置失败,记录错误日志并跳转到清理代码
        ESP_LOGE(TAG, "Failed to set socket, errno: %d", airkiss_get_errno(send_sock));
        goto _exit;
    }

    // 发锅50次ACK包,每次间隔100ms,总计10秒
    for (i = 0; i < 50; ++i) {
        // 准备ACK包数据,包含与配网者协商的随机数
        char tx_buf[2];  // 2字节的确认包,包含配网时获取的随机数
        tx_buf[0] = (uint8_t)ak_random_num;           // 随机数低字节
        tx_buf[1] = (uint8_t)(ak_random_num >> 8);    // 随机数高字节
        
        // 发送UDP广播包
        ret = sendto(send_sock, tx_buf, 2, 0, (struct sockaddr *)&remote_addr, remote_addr_len);
        if (ret < 0) {
            // 发送失败,记录错误日志并跳转到清理代码
            ESP_LOGE(TAG, "failed to send ack, errno: %d", airkiss_get_errno(send_sock));
            goto _exit;
        } else {
            // 发送成功,可在调试时记录日志
            // ESP_LOGI(TAG, "sent ack OK i: %d", i);
            vTaskDelay((portTickType) (100 / portTICK_RATE_MS));  // 延时100ms
        }
    }

_exit:
    // 清理资源并退出任务
    close(send_sock);  // 关闭套接字
    ESP_LOGI(TAG, "airkiss_send_ack_task exit");  // 记录任务退出日志
    air_answer_task_handle = NULL;  // 清除任务句柄
    vTaskDelete(NULL);  // 删除当前任务
}

/**
 * @brief 创建并启动发送ACK的任务
 * 
 * 该函数在WiFi连接成功后被调用,创建一个独立的任务来发送ACK确认包。
 * 如果确认任务已经在运行,则不会重复创建。
 */
void airkiss_answer(void)
{
    // 检查是否已经有任务在运行,避免重复创建
    if (air_answer_task_handle) {
        return;  // 如果任务已存在,直接返回
    }
    
    // 创建ACK发送任务,指定任务名称、堆栈大小和优先级
    xTaskCreate(airkiss_send_ack_task,            // 任务函数
                "KISS_Send_task",                // 任务名称
                AIRKISS_ACK_TASK_STACK_SIZE,       // 堆栈大小(由宏定义)
                NULL,                              // 任务参数(无)
                AIRKISS_ACK_TASK_PRIORITY,         // 任务优先级(由宏定义)
                &air_answer_task_handle);          // 任务句柄指针
}
网络辅助函数

AirKiss模块在处理网络操作时使用了一些辅助函数,如获取套接字错误码的函数:

/**
 * @brief 获取套接字错误码
 * 
 * 该函数用于获取指定套接字的错误码,便于网络操作失败时进行准确的错误处理
 * 主要在ACK发送和SSDP通知过程中使用
 * 
 * @param fd 要获取错误码的套接字描述符
 * @return int 返回套接字错误码
 */
static int airkiss_get_errno(int fd)
{
    int sock_errno = 0;
    u32_t optlen = sizeof(sock_errno);
    getsockopt(fd, SOL_SOCKET, SO_ERROR, &sock_errno, &optlen);
    return sock_errno;
}

该函数使用getsockopt系统调用获取套接字的SO_ERROR选项,返回上一次套接字操作的错误码。这使得我们可以在日志中记录具体的错误信息,便于诊断和解决网络问题。在发送ACK确认和SSDP通知时,如果发送失败,会通过此函数获取并记录具体的错误原因。

SSDP通知机制

如果启用了SSDP通知,在配网成功后还会在局域网内广播设备信息:

/**
 * @brief SSDP通知任务
 * 
 * 该函数在新线程中运行,在局域网内广播设备信息,使配网APP能够发现该设备
 * 并对收到的SSDP请求做出响应,实现设备的局域网可发现性
 */
static void airkiss_notify_task(void *pvParameters)
{
    // 获取传入的SSDP通知参数
    airkiss_lan_pack_param_t *lan_param = (airkiss_lan_pack_param_t *)pvParameters;
    
    // 初始化各种网络相关变量
    struct sockaddr_in local_addr;    // 本地地址结构
    struct sockaddr_in remote_addr;   // 远程地址结构(接收请求的地址)
    struct sockaddr_in broad_addr;    // 广播地址结构
    struct timeval tv;                // 超时结构
    fd_set rfds, exfds;               // 文件描述符集,用于多路复用
    socklen_t addr_len = sizeof(remote_addr);  // 地址结构大小
    int fd = -1;                      // 监听套接字
    int send_socket = -1;             // 发送套接字
    uint16_t buf_len = 200;           // 缓冲区大小
    uint16_t resp_len;                // 响应数据长度
    uint16_t recv_len;                // 接收数据长度
    uint16_t req_len;                 // 请求数据长度
    uint8_t *buf = NULL;              // 接收/发送缓冲区
    uint8_t *req_buf = NULL;          // 请求数据缓冲区
    int ret, err;                     // 返回值和错误码
    airkiss_lan_ret_t lan_ret;        // AirKiss LAN协议返回结果

    // 分配接收/发送缓冲区内存
    buf = audio_malloc(buf_len);
    if (buf == NULL) {
        ESP_LOGE(TAG, "buf allocate fail");
        goto _fail;  // 内存分配失败,跳转到清理代码
    }

    // 分配请求数据缓冲区内存
    req_buf = audio_malloc(buf_len);
    if (req_buf == NULL) {
        ESP_LOGE(TAG, "lan buf allocate fail");
        goto _fail;  // 内存分配失败,跳转到清理代码
    }
    memset(req_buf, 0, buf_len);  // 清零缓冲区
    req_len = buf_len;
    
    // 使用AirKiss LAN协议打包SSDP通知数据
    ret = airkiss_lan_pack(AIRKISS_LAN_SSDP_NOTIFY_CMD,  // 命令类型:通知
                           lan_param->appid,             // 应用ID
                           lan_param->deviceid,          // 设备ID
                           0, 0,                         // 序列号和保留字段
                           req_buf, &req_len, &ak_conf); // 输出缓冲区和长度
    if (ret != AIRKISS_LAN_PAKE_READY) {
        ESP_LOGE(TAG, "Pack lan packet error!");  // 打包失败
        goto _fail;  // 跳转到清理代码
    }
    
    // 创建UDP发送套接字,如果失败则重试
    do {
        send_socket = socket(AF_INET, SOCK_DGRAM, 0);  // 创建UDP套接字
        if (send_socket == -1) {
            ESP_LOGE(TAG, "failed to create sock!");
            vTaskDelay(1000 / portTICK_RATE_MS);  // 延时1秒后重试
        }
    } while (send_socket == -1);

    // 初始化广播地址结构
    memset(&broad_addr, 0, sizeof(broad_addr));
    broad_addr.sin_family = AF_INET;  // IPv4协议
    broad_addr.sin_addr.s_addr = INADDR_BROADCAST;  // 广播地址255.255.255.255
    broad_addr.sin_port = htons(AIRKISS_DEFAULT_LAN_PORT);  // AirKiss默认端口
    broad_addr.sin_len = sizeof(broad_addr);  // 地址结构长度

    // 创建UDP监听套接字,如果失败则重试
    do {
        fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);  // 创建UDP套接字
        if (fd == -1) {
            ESP_LOGE(TAG, "failed to create sock!");
            vTaskDelay(1000 / portTICK_RATE_MS);  // 延时1秒后重试
        }
    } while (fd == -1);

    // 初始化本地监听地址结构
    memset(&local_addr, 0, sizeof(local_addr));
    local_addr.sin_family = AF_INET;  // IPv4协议
    local_addr.sin_addr.s_addr = INADDR_ANY;  // 任意本地地址
    local_addr.sin_port = htons(AIRKISS_DEFAULT_LAN_PORT);  // AirKiss默认端口
    local_addr.sin_len = sizeof(local_addr);  // 地址结构长度

    // 将监听套接字绑定到本地地址
    ret = bind(fd, (const struct sockaddr *)&local_addr, sizeof(local_addr));
    if (ret) {
        err = airkiss_get_errno(fd);
        ESP_LOGE(TAG, "airkiss bind local port ERROR! errno %d", err);
        goto _out;  // 绑定失败,跳转到清理代码
    }

    // 设置超时为1秒
    tv.tv_sec = 1;
    tv.tv_usec = 0;
    uint8_t re_sent_num = 10;  // 重发次数

    // 开始广播与响应循环,重发完成10次通知后退出
    while (re_sent_num) {
        // 初始化文件描述符集,用于多路复用
        FD_ZERO(&rfds);  // 清空读集合
        FD_SET(fd, &rfds);  // 将监听套接字加入读集合
        FD_ZERO(&exfds);  // 清空异常集合
        FD_SET(fd, &exfds);  // 将监听套接字加入异常集合

        // 使用select等待数据到达或超时
        ret = select(fd + 1, &rfds, NULL, &exfds, &tv);
        if (ret > 0) {  // 有数据可读或有异常
            if (FD_ISSET(fd, &exfds) || !FD_ISSET(fd, &rfds)) {
                ESP_LOGE(TAG, "Receive AIRKISS_LAN_SSDP_REQ select error!");
                goto _out;  // 出现异常,跳转到清理代码
            }

            // 读取收到的数据
            memset(buf, 0, buf_len);  // 清空缓冲区
            recv_len = recvfrom(fd, buf, buf_len, 0,
                                 (struct sockaddr *)&remote_addr, (socklen_t *)&addr_len);

            // 使用AirKiss LAN协议解析收到的数据
            lan_ret = airkiss_lan_recv(buf, recv_len, &ak_conf);
            if (lan_ret == AIRKISS_LAN_SSDP_REQ) {  // 如果是SSDP请求
                ESP_LOGD(TAG, "AIRKISS_LAN_SSDP_REQ");  // 记录日志
                
                // 准备SSDP响应数据
                memset(buf, 0, buf_len);  // 清空缓冲区
                resp_len = buf_len;
                lan_ret = airkiss_lan_pack(AIRKISS_LAN_SSDP_RESP_CMD,  // 命令类型:响应
                                           lan_param->appid,              // 应用ID
                                           lan_param->deviceid,           // 设备ID
                                           0, 0,                          // 序列号和保留字段
                                           buf, &resp_len, &ak_conf);     // 输出缓冲区和长度
                if (lan_ret != AIRKISS_LAN_PAKE_READY) {  // 打包失败
                    ESP_LOGE(TAG, "Pack lan packet error! errno %d", lan_ret);
                    goto _out;  // 跳转到清理代码
                }
                
                // 发送SSDP响应数据
                ret = sendto(fd, buf, resp_len, 0,
                              (struct sockaddr *)&remote_addr, sizeof(remote_addr));
                if (ret < 0) {
                    err = airkiss_get_errno(fd);
                    if (err != ENOMEM && err != EAGAIN) {  // 非内存不足或资源暂时不可用的错误
                        ESP_LOGE(TAG, "send notify msg ERROR! errno %d", err);
                        goto _out;  // 发送失败,跳转到清理代码
                    }
                } else {  // 发送成功
                    ESP_LOGD(TAG, "send notify msg OK!");
                    re_sent_num--;  // 减少重发计数
                }
            }
        } else {  // 超时或出错,发送通知消息
            // 广播发送SSDP通知消息
            ret = sendto(send_socket, req_buf, req_len, 0,
                          (const struct sockaddr *)&broad_addr, sizeof(broad_addr));
            if (ret < 0) {
                err = airkiss_get_errno(fd);
                if (err != ENOMEM && err != EAGAIN) {  // 非内存不足或资源暂时不可用的错误
                    ESP_LOGE(TAG, "send notify msg ERROR! errno %d", err);
                    goto _out;  // 发送失败,跳转到清理代码
                }
            } else {  // 发送成功
                ESP_LOGI(TAG, "send notify msg %d OK!", re_sent_num);
                re_sent_num--;  // 减少重发计数
            }
        }
    }

_out:  // 跳转标签,关闭套接字
    close(fd);  // 关闭监听套接字
    close(send_socket);  // 关闭发送套接字
_fail:  // 跳转标签,释放分配的内存
    if (buf) {
        audio_free(buf);
        buf = NULL;
    }
    if (req_buf) {
        audio_free(req_buf);
        req_buf = NULL;
    }
    audio_free(lan_param->appid);    // 释放应用ID内存
    audio_free(lan_param->deviceid); // 释放设备ID内存
    audio_free(lan_param);           // 释放参数结构内存
    vTaskDelete(NULL);               // 删除当前任务
}

/**
 * @brief 启动SSDP通知任务
 * 
 * 该函数创建一个单独的任务来处理局域网内的SSDP通知,
 * 使配网者(手机APP等)能够在配网成功后发现设备并与其建立连接
 * 
 * @param param SSDP通知参数,包含应用ID和设备ID等信息
 */
static void airkiss_ssdp_notify(const airkiss_lan_pack_param_t *param)
{
    // 创建通知任务,该任务用于在局域网内广播设备存在并响应发现请求
    // 参数说明:
    // 1. airkiss_notify_task - 任务函数指针
    // 2. "Airkiss_notify_task" - 任务名称,用于调试识别
    // 3. AIRKISS_NOTIFY_TASK_STACK_SIZE - 任务堆栈大小(通常为4096字节)
    // 4. (void *)param - 传递给任务的参数,包含通知所需的应用和设备信息
    // 5. AIRKISS_NOTIFY_TASK_PRIORITY - 任务优先级(通常为3)
    // 6. NULL - 不需要保存任务句柄
    xTaskCreate(airkiss_notify_task, "Airkiss_notify_task", AIRKISS_NOTIFY_TASK_STACK_SIZE,
                (void *)param, AIRKISS_NOTIFY_TASK_PRIORITY, NULL);
    
    // 注意:该函数不会等待通知任务完成,而是立即返回,通知任务在后台运行
    // 通知任务会自行释放传入的param参数结构内存,调用者不需要手动释放
}
SSDP通知机制时序图

下面是SSDP通知机制的时序图,展示了配网完成后设备与手机APP的交互流程:

配网设备(ESP32/ESP8266) 手机配网APP(微信或其他应用) 其他局域网设备 1. WiFi连接成功 2. 发送ACK应答(airkiss_answer) 3. 启动SSDP通知任务 4. 创建UDP套接字 5. 广播发送SSDP通知包 6. SSDP请求(收到) 7. 发送SSDP响应包 8. 重复发送通知(10次总计) 9. 通过局域网发现设备 10. 建立控制连接 配网设备(ESP32/ESP8266) 手机配网APP(微信或其他应用) 其他局域网设备

时序图说明:

  1. WiFi连接成功:设备成功连接到WiFi网络后通知配网APP

  2. 发送ACK应答:调用airkiss_answer()发送确认包给配网APP,确认配网成功

  3. 启动SSDP通知任务:调用airkiss_ssdp_notify()创建独立的通知任务

  4. 创建UDP套接字:通知任务创建发送和监听的UDP套接字

  5. 广播发送SSDP通知包:向局域网内广播发送SSDP通知包,通知设备的存在

  6. 收到SSDP请求:局域网内的设备(如手机APP)发送SSDP请求,询问设备信息

  7. 发送SSDP响应包:设备响应SSDP请求,提供详细的设备信息

  8. 重复发送通知:总共发送10次通知,确保局域网内的设备能够发现

  9. 通过局域网发现设备:手机APP或其他设备可以发现该设备并获得其信息

  10. 建立控制连接:手机APP与设备建立控制连接,开始进行正常的设备控制

通过这种方式,配网完成后的设备可以被局域网内的其他设备自动发现,实现无缝的用户体验。

5. 停止配网阶段

AirKiss的停止由airkiss_stop函数实现,该函数在WiFi服务调用esp_wifi_setting_stop时被触发。

/**
 * @brief 停止AirKiss配网
 * 
 * 该函数完成以下主要操作:
 * 1. 设置嗅探停止标志
 * 2. 关闭并删除通道切换定时器
 * 3. 关闭WiFi混杂模式
 * 4. 释放相关资源
 * 
 * @param handle 配网接口句柄
 * @return esp_err_t 始终返回ESP_OK
 */
static esp_err_t airkiss_stop(esp_wifi_setting_handle_t handle)
{
    // 设置嗅探停止标志,通知所有监听到数据包的回调函数不再处理数据
    s_sniffer_stop_flag = 1;
    
    // 检查并停止通道切换定时器
    if (channel_change_timer) {
        esp_timer_stop(channel_change_timer);  // 停止定时器
        esp_timer_delete(channel_change_timer);  // 删除定时器
        channel_change_timer = NULL;  // 清除定时器指针
    }
    
    // 关闭WiFi混杂模式,不再损抓数据包
    esp_wifi_set_promiscuous(false);
    
    // 释放AirKiss上下文内存
    audio_free(ak_ctx);
    ak_ctx = NULL;  // 避免悬空指针
    
    // 成功返回
    return ESP_OK;
}

停止配网阶段的流程如下:

调用airkiss_stop(handle)
设置嗅探停止标志
定时器是否存在?
停止定时器
删除定时器
清空定时器句柄
关闭WiFi混杂模式
释放AirKiss上下文
清空上下文指针
返回ESP_OK

停止阶段的关键点:

  1. 设置嗅探停止标志,使数据包接收回调函数停止工作
  2. 停止并清理通道切换定时器
  3. 关闭WiFi混杂模式,恢复正常的WiFi工作模式
  4. 释放相关缓冲区和资源

完整流程分析

下面的时序图展示了AirKiss从创建到使用的完整流程:

应用程序 WiFi服务 airkiss_config ESP-IDF WiFi 微信APP airkiss_config_create(info) 返回配网接口句柄 wifi_service_register_setting_handle(handle) 返回index wifi_service_setting_start(index) esp_wifi_setting_start(handle) airkiss_start(handle) esp_wifi_disconnect() airkiss_wifi_scan_ap() - 扫描可用AP esp_wifi_set_channel() - 设置初始信道 创建信道切换定时器 esp_wifi_set_promiscuous_rx_cb() esp_wifi_set_promiscuous(true) 混杂模式接收数据包 信道切换 loop [周期性信道切换] 通过特殊数据包发送WiFi信息 wifi_promiscuous_rx回调 处理数据包 找到锁定信道 停止信道切换 配网完成 esp_wifi_set_promiscuous(false) airkiss_finish() esp_wifi_setting_info_notify() 连接到获取的WiFi airkiss_teardown() airkiss_answer() - 发送确认 airkiss_ssdp_notify() - 如果启用 esp_wifi_setting_stop(handle) airkiss_stop(handle) 清理资源 wifi_service_destroy() 清理资源(自动释放配网接口) 应用程序 WiFi服务 airkiss_config ESP-IDF WiFi 微信APP

使用示例

下面是一个使用AirKiss配网方式的完整示例:

#include "esp_log.h"
#include "esp_wifi.h"
#include "audio_element.h"
#include "periph_service.h"
#include "wifi_service.h"
#include "airkiss_config.h"

static const char *TAG = "AIRKISS_EXAMPLE";

// WiFi事件回调函数
static esp_err_t wifi_service_cb(periph_service_handle_t handle, periph_service_event_t *evt, void *ctx)
{
    if (evt->type == WIFI_SERV_EVENT_SETTING_FINISHED) {
        ESP_LOGI(TAG, "WIFI_SERV_EVENT_SETTING_FINISHED");
        ESP_LOGI(TAG, "获取到WiFi配置,准备连接");
    } else if (evt->type == WIFI_SERV_EVENT_CONNECTED) {
        ESP_LOGI(TAG, "WIFI_SERV_EVENT_CONNECTED");
        ESP_LOGI(TAG, "已成功连接到WiFi网络");
    } else if (evt->type == WIFI_SERV_EVENT_DISCONNECTED) {
        ESP_LOGI(TAG, "WIFI_SERV_EVENT_DISCONNECTED");
        ESP_LOGI(TAG, "与WiFi网络断开连接");
    } else if (evt->type == WIFI_SERV_EVENT_SETTING_TIMEOUT) {
        ESP_LOGW(TAG, "WIFI_SERV_EVENT_SETTING_TIMEOUT");
        ESP_LOGW(TAG, "配网超时,请重试");
    }
    return ESP_OK;
}

void app_main(void)
{
    // 初始化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);
    
    // 初始化TCP/IP组件
    tcpip_adapter_init();
    
    // 初始化WiFi
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_start());
    
    // 创建WiFi服务
    wifi_service_config_t wifi_cfg = WIFI_SERVICE_DEFAULT_CONFIG();
    wifi_cfg.evt_cb = wifi_service_cb;
    wifi_cfg.setting_timeout_s = 60; // 配网超时时间为60秒
    
    periph_service_handle_t wifi_handle = wifi_service_create(&wifi_cfg);
    if (wifi_handle == NULL) {
        ESP_LOGE(TAG, "Failed to create WiFi service");
        return;
    }
    
    // 创建AirKiss配网方式
    airkiss_config_info_t ak_cfg = AIRKISS_CONFIG_INFO_DEFAULT();
    ak_cfg.lan_pack.appid = "your_app_id";  // 设置应用ID
    ak_cfg.lan_pack.deviceid = "your_device_id";  // 设置设备ID
    ak_cfg.ssdp_notify_enable = true;  // 启用SSDP通知
    ak_cfg.aes_key = "1234567812345678";  // 可选AES密钥,必须是16字节
    
    esp_wifi_setting_handle_t ak_handle = airkiss_config_create(&ak_cfg);
    if (ak_handle == NULL) {
        ESP_LOGE(TAG, "Failed to create AirKiss config");
        return;
    }
    
    // 注册配网方式到WiFi服务
    int setting_index = 0;
    ESP_ERROR_CHECK(wifi_service_register_setting_handle(wifi_handle, ak_handle, &setting_index));
    
    // 启动配网
    ESP_LOGI(TAG, "Starting AirKiss");
    ESP_LOGI(TAG, "请在微信小程序中发送WiFi配置");
    ESP_ERROR_CHECK(wifi_service_setting_start(wifi_handle, setting_index));
    
    // 应用程序主循环...
    
    // 在不再需要时销毁WiFi服务(会自动清理相关的配网资源)
    // wifi_service_destroy(wifi_handle);
}

总结

ESP-ADF的airkiss_config模块实现了基于微信硬件平台AirKiss协议的WiFi配网方式,通过遵循esp_wifi_setting接口规范,它成为了WiFi服务的可插拔组件之一。使用AirKiss,用户可以通过微信小程序或APP向ESP设备传递WiFi配置信息,无需设备具备显示界面或输入能力。

AirKiss的生命周期遵循esp_wifi_setting接口定义的模式:

  1. 创建和初始化airkiss_config_create分配资源并注册功能函数
  2. 启动配网airkiss_start初始化并启动AirKiss配网过程,包括扫描AP、设置混杂模式和周期性切换信道
  3. 数据包接收处理:通过混杂模式回调函数捕获并处理WiFi数据包,当获取到完整WiFi信息后通知WiFi服务
  4. 配网完成处理airkiss_teardown发送ACK确认并根据设置进行SSDP通知
  5. 停止配网airkiss_stop清理资源并恢复正常WiFi模式

AirKiss具有以下特点:

  1. 微信生态集成:专为微信IoT平台开发的配网协议,可直接与微信小程序交互
  2. 无界面配网:不需要设备具备显示界面或输入能力
  3. 嗅探模式工作:通过混杂模式捕获特殊数据包实现配网
  4. AES加密支持:支持使用AES密钥增强安全性
  5. SSDP设备发现:支持配网后通过SSDP协议在局域网中通知设备存在

通过合理使用AirKiss配网方式,开发者可以为ESP设备提供与微信平台无缝集成的WiFi配置体验,特别适合面向微信用户的IoT产品。

Pyraformer是一种用于长程时间序列建模和预测的低复杂度、金字塔式注意力机制。它通过多分辨率的方式捕捉不同范围的时间依赖关系,并且在理论上证明了在适当选择参数的情况下,Pyraformer能够同时实现O(1)的最大信息传播路径和O(L)的时空复杂度。该方法在多个真实数据集上的实验表明,无论是单步预测任务还是长程预测任务,Pyraformer都取得了比Transformer及其变体更好的效果,并且所需的时间和显存消耗更低。如果您需要更多关于Pyraformer的详细信息,您可以参考引用中的论文标题和引用中的论文链接和源码链接。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [[2022 ICLR] Pyraformer: Low-Complexity Pyramidal Attention for Long-Range 时空序列建模和预测](https://blog.csdn.net/qq_33866063/article/details/124332826)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [论文笔记-时序预测-Pyraformer](https://blog.csdn.net/weixin_44611266/article/details/128265508)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

omnibots

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

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

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

打赏作者

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

抵扣说明:

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

余额充值