目录
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组件架构中处于配网接口实现层,下图展示了其在整体架构中的位置:
从图中可以看出,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的主要工作原理是:
- ESP设备进入混杂模式(Promiscuous Mode),监听所有WiFi数据包
- 用户在微信小程序或APP上输入WiFi的SSID和密码
- 微信APP将这些信息编码成特殊的数据包发送到空气中
- ESP设备接收并解码这些数据包,获取到WiFi的SSID和密码
- ESP设备使用获取到的信息连接WiFi网络
- 配网成功后,设备通过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和设备IDairkiss_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;
}
创建阶段的流程如下:
创建阶段的关键点:
- 使用
esp_wifi_setting_create
创建通用接口 - 分配和初始化配置结构体
- 为AES密钥分配内存并复制(如果提供了密钥)
- 通过
esp_wifi_setting_set_data
存储配置 - 注册
airkiss_start
、airkiss_stop
和airkiss_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上下文
- 设置AES密钥(如果启用加密)
- 扫描可用WiFi通道并选择初始通道
- 创建通道切换定时器,定期切换WiFi通道以寻找配网数据包
- 设置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);
}
}
数据包处理流程如下:
数据包处理阶段的关键点:
- 损获每个WiFi数据包并交给AirKiss库处理
- 当AirKiss返回
AIRKISS_STATUS_CHANNEL_LOCKED
时,表示已找到配网信道,停止通道切换 - 当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(¶->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的交互流程:
时序图说明:
-
WiFi连接成功:设备成功连接到WiFi网络后通知配网APP
-
发送ACK应答:调用
airkiss_answer()
发送确认包给配网APP,确认配网成功 -
启动SSDP通知任务:调用
airkiss_ssdp_notify()
创建独立的通知任务 -
创建UDP套接字:通知任务创建发送和监听的UDP套接字
-
广播发送SSDP通知包:向局域网内广播发送SSDP通知包,通知设备的存在
-
收到SSDP请求:局域网内的设备(如手机APP)发送SSDP请求,询问设备信息
-
发送SSDP响应包:设备响应SSDP请求,提供详细的设备信息
-
重复发送通知:总共发送10次通知,确保局域网内的设备能够发现
-
通过局域网发现设备:手机APP或其他设备可以发现该设备并获得其信息
-
建立控制连接:手机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;
}
停止配网阶段的流程如下:
停止阶段的关键点:
- 设置嗅探停止标志,使数据包接收回调函数停止工作
- 停止并清理通道切换定时器
- 关闭WiFi混杂模式,恢复正常的WiFi工作模式
- 释放相关缓冲区和资源
完整流程分析
下面的时序图展示了AirKiss从创建到使用的完整流程:
使用示例
下面是一个使用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接口定义的模式:
- 创建和初始化:
airkiss_config_create
分配资源并注册功能函数 - 启动配网:
airkiss_start
初始化并启动AirKiss配网过程,包括扫描AP、设置混杂模式和周期性切换信道 - 数据包接收处理:通过混杂模式回调函数捕获并处理WiFi数据包,当获取到完整WiFi信息后通知WiFi服务
- 配网完成处理:
airkiss_teardown
发送ACK确认并根据设置进行SSDP通知 - 停止配网:
airkiss_stop
清理资源并恢复正常WiFi模式
AirKiss具有以下特点:
- 微信生态集成:专为微信IoT平台开发的配网协议,可直接与微信小程序交互
- 无界面配网:不需要设备具备显示界面或输入能力
- 嗅探模式工作:通过混杂模式捕获特殊数据包实现配网
- AES加密支持:支持使用AES密钥增强安全性
- SSDP设备发现:支持配网后通过SSDP协议在局域网中通知设备存在
通过合理使用AirKiss配网方式,开发者可以为ESP设备提供与微信平台无缝集成的WiFi配置体验,特别适合面向微信用户的IoT产品。