用 ESP8266 RTOS SDK写一个详细的示例,要求只输出beacon包,其它包不处理
下面给出一个基于 ESP8266 RTOS SDK 的完整示例,演示如何在 Promiscuous 模式下只抓取并打印 Beacon 管理帧(802.11 Type=0, Subtype=8),忽略其他所有帧。代码中包含详细注释,帮助你理解每一步的作用。
一、工程准备
-
环境要求
- ESP8266 RTOS SDK(本文示例基于 v3.x/4.x 系列均可)。
- 已正确配置并能够编译、烧录 ESP8266 的开发环境(如使用 ESP-IDF 工具链或 VSCode + PlatformIO 中的 ESP8266 RTOS SDK 配置)。
- 板子与电脑已通过串口线连接。
-
工程文件结构(假设使用 ESP-IDF 工具链)
your_project/ ├── CMakeLists.txt ├── sdkconfig # 根据需要打开的功能自行配置 └── main/├── CMakeLists.txt└── main.c
CMakeLists.txt
、sdkconfig
等文件按常规 ESP-IDF 工程生成即可,不在此赘述。- 下面重点展示
main/main.c
的完整内容。
二、main.c
详细代码
#include <string.h>
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "tcpip_adapter.h" // ESP8266 RTOS SDK 中使用 tcpip_adapter 而非 esp_netif// 日志标签,用于 ESP_LOG 输出时区分模块
static const char *TAG = "wifi_sniffer";/*** @brief 抓包回调函数:只处理 Management 帧中的 Beacon 子类型** @param buf 指向一个 wifi_promiscuous_pkt_t 结构体的指针,包含 rx_ctrl(元信息) + payload(帧内容)* @param type 表示这帧的类型:WIFI_PKT_MGMT / WIFI_PKT_CTRL / WIFI_PKT_DATA / WIFI_PKT_MISC*/
static void wifi_sniffer_packet_handler(void *buf, wifi_promiscuous_pkt_type_t type)
{// 1. 只关注管理帧 (Management Frames)if (type != WIFI_PKT_MGMT) {return;}// 2. 将 buf 强制转换为 wifi_promiscuous_pkt_t 结构体const wifi_promiscuous_pkt_t *ppkt = (wifi_promiscuous_pkt_t *)buf;const wifi_pkt_rx_ctrl_t *rx_ctrl = &ppkt->rx_ctrl; // 收到帧的元信息 (RSSI、信道等)const uint8_t *payload = ppkt->payload; // 真正的 802.11 帧首字节// 3. 解析 802.11 帧头 (Frame Control 字段) 判断帧子类型// Frame Control (2 bytes):低字节在 payload[0],高字节在 payload[1]uint16_t frame_ctrl = ((uint16_t)payload[1] << 8) | payload[0];uint8_t frame_type = (frame_ctrl >> 2) & 0x3; // 2-bit:00=Management, 01=Control, 10=Datauint8_t frame_subtype= (frame_ctrl >> 4) & 0xF; // 4-bit 子类型(Beacon=8, Probe Req=4, Probe Resp=5, ...)// 冗余判断:type==WIFI_PKT_MGMT 一般对应 frame_type==0,但额外检查以防意外情况if (frame_type != 0 || frame_subtype != 8) {// 不是 Beacon 管理帧,直接返回return;}// 4. Beacon 帧解析:// - RSSI:rx_ctrl->rssi(dBm,带符号)// - 抓到的频道:rx_ctrl->channel// - 源 MAC:前 24 字节 MAC 头部中的 Addr2(Source Address),在 payload[10]~payload[15]// - SSID:Beacon 的 Tag 信息中第一项通常是 SSID Tag (Tag Number = 0),// Tag 的起始偏移 = 24 (MAC header) + 12 (beacon 固定字段) = 36int8_t rssi = rx_ctrl->rssi;uint8_t channel = rx_ctrl->channel;// 源 MAC 地址char src_mac_str[18];const uint8_t *mac_src = payload + 10; // Management Frame Header 中的 Address2snprintf(src_mac_str, sizeof(src_mac_str),"%02X:%02X:%02X:%02X:%02X:%02X",mac_src[0], mac_src[1], mac_src[2],mac_src[3], mac_src[4], mac_src[5]);// SSID 字段解析:从 payload[36] 开始const uint8_t *ie_ptr = payload + 36;uint8_t tag_number = ie_ptr[0];uint8_t tag_len = ie_ptr[1];char ssid[33]; // 802.11 中 SSID 最长 32 字节if (tag_number == 0 && tag_len > 0) {if (tag_len > 32) tag_len = 32; // 防止越界memcpy(ssid, ie_ptr + 2, tag_len);ssid[tag_len] = '\0';} else {// 如果第一个 Tag 不是 SSID,可视为隐藏网络或不规范,此处直接置空ssid[0] = '\0';}// 5. 最终将信息打印到串口ESP_LOGI(TAG,"Beacon >>> SSID: \"%s\", MAC: %s, RSSI: %d dBm, Channel: %d",ssid, src_mac_str, rssi, channel);
}/*** @brief 应用程序入口函数*/
void app_main(void)
{esp_err_t ret;// -------------------------------// 1. 初始化 NVS -- ESP8266 WiFi 需要底层使用 NVS// -------------------------------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 适配层 + 事件循环// -------------------------------tcpip_adapter_init();ESP_ERROR_CHECK(esp_event_loop_create_default());// -------------------------------// 3. 初始化 WiFi Driver(使用默认配置)// -------------------------------wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();ESP_ERROR_CHECK(esp_wifi_init(&cfg));// -------------------------------// 4. 设置 WiFi Work Mode 为 NULL 模式(既不 Station 也不 SoftAP)// 这样可以仅将 WiFi 射频打开用于 Promiscuous 抓包// 如想先以 STA 模式关联 AP 再抓包,也可以改为 WIFI_MODE_STA// -------------------------------ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_NULL));// WiFi 参数存储只使用 RAMESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));// -------------------------------// 5. 启动 WiFi Driver// -------------------------------ESP_ERROR_CHECK(esp_wifi_start());// -------------------------------// 6. 配置 Promiscuous Filter:只让 Management 帧通过回调//// WIFI_PROMIS_FILTER_MASK_MGMT 表示只接收管理帧(所有子类型),// 具体在回调里再额外判断是否 Subtype == 8(Beacon)// -------------------------------wifi_promiscuous_filter_t filter = {.filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT};ESP_ERROR_CHECK(esp_wifi_set_promiscuous_filter(&filter));// -------------------------------// 7. 注册回调函数// -------------------------------ESP_ERROR_CHECK(esp_wifi_set_promiscuous_rx_cb(wifi_sniffer_packet_handler));// -------------------------------// 8. 启用 Promiscuous 模式// 如果第二个参数设为 true,则会进入被动抓包,并接收所有 Management/Data/Control/Misc,// 结合上面 filter,可以减少回调次数。(filter 已经只让 MGMT 帧触发)// -------------------------------ESP_ERROR_CHECK(esp_wifi_set_promiscuous(true));// -------------------------------// 9. 设置要抓取的信道 (1~13)// 这里直接锁定在信道 1,后续如果需要轮询信道可以使用定时器动态切换。// -------------------------------uint8_t channel = 1;ESP_ERROR_CHECK(esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE));ESP_LOGI(TAG, "已切换到信道:%d,开始抓取 Beacon 帧...", channel);// -------------------------------// 10. 如果需要轮询信道(可选,可按需打开)// 下面示例演示如何每隔 200ms 切换到下一个信道,从 1~13 循环:// -------------------------------/*static uint8_t current_channel = 1;void channel_hop_timer_cb(void *arg){current_channel = (current_channel % 13) + 1;esp_wifi_set_channel(current_channel, WIFI_SECOND_CHAN_NONE);ESP_LOGI(TAG, "跳转到信道:%d", current_channel);}// 创建并启动 FreeRTOS 定时器TimerHandle_t channel_timer = xTimerCreate("chan_hop",pdMS_TO_TICKS(200),pdTRUE,NULL,channel_hop_timer_cb);xTimerStart(channel_timer, 0);*/// -------------------------------// 11. 到这里 app_main 不再阻塞,Promiscuous 回调会不断输出 Beacon 信息。// -------------------------------
}
三、关键点说明
-
NVS 初始化
ESP8266 RTOS SDK 的 WiFi 驱动会调用 NVS 存储来保存一些参数,必须先调用nvs_flash_init()
。如果出现ESP_ERR_NVS_NO_FREE_PAGES
类似错误,需要先擦除再初始化。 -
tcpip_adapter_init + esp_event_loop_create_default
用来初始化底层的 TCP/IP 协议栈及事件循环系统,为后续esp_wifi_init
等操作做好准备。 -
esp_wifi_set_mode(WIFI_MODE_NULL)
WIFI_MODE_NULL
:既不作为 Station,也不作为 SoftAP,仅打开射频,让芯片能工作在 Promiscuous 模式抓包。- 如果你想先连上某个 AP 再抓包,也可以改为
WIFI_MODE_STA
,但此时可能会抓到自己发出的 Probe/ACK 等帧。使用NULL
模式能最纯粹地只有“监听”功能。
-
Promiscuous Filter
wifi_promiscuous_filter_t
中的filter_mask = WIFI_PROMIS_FILTER_MASK_MGMT
,表示仅让“管理帧(Management Frames)”触发回调。管理帧包括 Beacon、Probe Request/Response、Association、Authentication 等。- 如果把
filter_mask
设为WIFI_PROMIS_FILTER_MASK_MGMT | WIFI_PROMIS_FILTER_MASK_DATA
,则会触发管理帧和数据帧回调。本例里为了降低回调频率,直接只让管理帧进来。
-
回调函数
wifi_sniffer_packet_handler
-
原型是:
void (* wifi_promiscuous_cb_t)(void *buf, wifi_promiscuous_pkt_type_t type);
- 第一个参数
buf
是一个指向wifi_promiscuous_pkt_t
的指针,它封装了 RX 控制信息(rx_ctrl
)与帧载荷(payload)。 - 第二个参数
type
告诉你这是管理帧、控制帧、数据帧还是 misc 帧。
- 第一个参数
-
本例一开始就判断
type != WIFI_PKT_MGMT
,若不是管理帧就直接返回。 -
再进一步从
payload[0..1]
(Frame Control)解析出frame_type
(2bit)和frame_subtype
(4bit),只有当frame_type==0 && frame_subtype==8
(即 Beacon)时才继续处理。否则再返回。 -
对于 Beacon 帧,示例中提取了:
- RSSI:
ppkt->rx_ctrl.rssi
- Channel:
ppkt->rx_ctrl.channel
- Source MAC:
payload + 10
(Management Frame Header 中的 Address2) - SSID:从
payload + 36
开始,第一项 Tag(Tag Number = 0)即为 SSID 参数集。若 Tag Number ≠ 0 或长度为 0,则认为是隐藏 SSID,此处示例直接输出空字符串。
- RSSI:
-
最后通过
ESP_LOGI
将抓到的 Beacon 信息(SSID、MAC、RSSI、Channel)输出到串口。
-
-
信道设置
esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE)
:将收包信道固定在 1。你可以根据周围 AP 分布情况自行修改信道;- 如果想覆盖整个 1~13 信道,可参考注释部分的定时器示例,每 200ms 切换一次信道。
-
打印日志
-
本示例使用
ESP_LOGI(TAG, ...)
输出日志,你也可改成printf
/ESP_LOGE
等,根据实际需求调整。 -
编译后,在串口工具中将波特率设置为 115200(或 sdkconfig 中配置的默认波特率),即可看到类似如下输出:
I (210) wifi_sniffer: 已切换到信道:1,开始抓取 Beacon 帧... I (510) wifi_sniffer: Beacon >>> SSID: "MyWiFi", MAC: 12:34:56:78:9A:BC, RSSI: -45 dBm, Channel: 1 I (1210) wifi_sniffer: Beacon >>> SSID: "GuestNet", MAC: DE:AD:BE:EF:00:11, RSSI: -60 dBm, Channel: 1 ...
-
四、注意事项
-
并发性能
- Promiscuous 模式下回调非常频繁,尤其在信道人多、AP 较多环境下。如果回调函数中做过多耗时操作会导致丢包。
- 本示例中仅完成最简单的字符串拼接与
ESP_LOGI
,已经尽可能减少阻塞;若后续要把抓到的原始帧丢给其他任务进一步处理,建议使用 FreeRTOS 队列或环形缓冲,将payload
数据先复制到独立缓冲,然后由专门任务做深度解析。
-
信道跳频 (Channel Hopping)
- 如果只固定在一个信道(如 1),只能抓到该信道上发射的 Beacon(以及 Probe Response、某些广播)。
- 若要覆盖所有常见信道,可使用定时器 (
xTimerCreate
) 每隔几百毫秒调用esp_wifi_set_channel()
。 - 注意:在跳频期间,如果切换过快,Promiscuous 回调几乎“看不到”任何帧;如果切换过慢,虽然能多抓到 Beacon,但响应速度变慢。一般建议每 200~500ms 跳一个信道。
-
隐藏 SSID 与私有扩展
- 如果某 AP 隐藏了 SSID(SSID 长度为 0),本例将不会输出 SSID 内容。你可以自行改造:当
tag_number!=0
时,继续遍历后续 Tag,直到找到 Tag Number = 0 或结束。 - Beacon 中可能包含多种 Information Element (IE),如 HT Capabilities、Vendor Specific 等,应用层可根据需要自行解析。
- 如果某 AP 隐藏了 SSID(SSID 长度为 0),本例将不会输出 SSID 内容。你可以自行改造:当
-
功耗与天线
- Promiscuous 模式下 WiFi 射频始终处于监听状态,功耗高于普通 Station/SoftAP 模式。
- 板子天线设计、外部环境(墙壁、干扰信号)也会影响抓包效果。
- 在实际项目中,可根据使用场景决定是否在特定时段才开启抓包,其他时间切回普通模式。
五、小结
以上示例演示了如何使用 ESP8266 RTOS SDK 进入 WiFi Promiscuous 模式,只捕获并打印 Beacon 帧,忽略其他所有 Management/Data/Control 帧。你可以根据业务需要对回调函数和信道策略进行二次开发,例如:
- 进一步解析 Beacon 里携带的更多 IE 信息;
- 将抓到的 Beacon 信息通过 UDP/HTTP 上报到云端;
- 或在捕获到特定 SSID/特定 MAC 时触发其他动作。
只需根据示例中的步骤(NVS → TCPIP → WiFi 初始化 → 配置 Filter → 注册回调 → 启用 Promiscuous → 切信道),即可快速搭建一个只输出 Beacon 的 WiFi Sniffer 模块。祝你开发顺利!