下面给你一个基于 ESP-IDF(v5.x) 的完整示例:在 ESP32-C3 上同时扫描附近 Wi-Fi 与蓝牙(BLE)广播,把结果以 JSON 结构统一输出到串口,并且可可选通过 MQTT 上报到服务器(打开一个宏即可)。日志默认中文。示例考虑了功耗与速率限制,避免长时间占用射频。
功能概述
Wi-Fi 扫描:主动/被动可选,返回 SSID、BSSID、信道、RSSI、加密类型。
BLE 扫描(NimBLE):解析设备地址、RSSI、广告类型、部分厂商数据。
统一打包 JSON:每个周期输出一帧
{ts, wifi_list, ble_list}
。MQTT 可选上报:定义
ENABLE_MQTT
后,会将 JSON 发布到scan/uplink
主题。节流与功耗:可配置扫描周期、Wi-Fi dwell time、BLE 扫描窗口/间隔;支持在两次扫描间隙小休眠。
目录结构(示例工程)
scaniot/├─ main/│ ├─ CMakeLists.txt│ └─ app_main.c├─ CMakeLists.txt└─ sdkconfig.defaults (可选:预置扫描/MQTT参数)
main/CMakeLists.txt
idf_component_register(SRCS "app_main.c"INCLUDE_DIRS ".")
顶层 CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(scaniot)
app_main.c
复制即可编译;根据注释修改 Wi-Fi/MQTT 参数。
#include <stdio.h>
#include <string.h>
#include <inttypes.h>
#include <time.h>#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"#include "esp_event.h"
#include "esp_log.h"
#include "esp_system.h"
#include "esp_err.h"
#include "nvs_flash.h"#include "esp_wifi.h"
#include "esp_netif.h"#include "cJSON.h"// ---- 可选使能 MQTT 上报 ----
#define ENABLE_MQTT 1 // 1=启用MQTT上报;0=仅串口输出#if ENABLE_MQTT
#include "mqtt_client.h"
#endif// ---- NimBLE BLE 扫描 ----
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "host/ble_hs.h"
#include "host/ble_gap.h"
#include "host/util/util.h"static const char *TAG = "ScanIOT";// ====== 可配参数 ======
#define WIFI_SCAN_ACTIVE 1 // 1主动扫描 0被动扫描
#define WIFI_SCAN_MAX_AP 24 // 单次最多AP记录
#define WIFI_SCAN_CHANNEL_TIME_MS 110 // 主动扫描每信道停留(ms)// BLE 扫描参数(窗口/间隔单位均为 0.625ms)
#define BLE_SCAN_ITVL 0x0060 // 60 * 0.625ms = 37.5ms
#define BLE_SCAN_WINDOW 0x0030 // 30 * 0.625ms = 18.75ms
#define BLE_SCAN_DURATION_SEC 5 // 每轮BLE扫描时长(s)#define SCAN_PERIOD_SEC 15 // 每轮合并扫描周期(s)// Wi-Fi 连接(若需要MQTT):
static const char *WIFI_SSID = "YourAP";
static const char *WIFI_PASS = "YourPassword";// MQTT 参数:
#if ENABLE_MQTT
static const char *MQTT_BROKER_URI = "mqtt://192.168.1.100:1883";
static const char *MQTT_TOPIC = "scan/uplink";
static esp_mqtt_client_handle_t s_mqtt = NULL;
#endif// ====== 实用:时间戳 ======
static int64_t epoch_millis(void) {struct timespec ts;clock_gettime(CLOCK_REALTIME, &ts);return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}// ====== BLE 扫描收集 ======
typedef struct {char addr[18];int rssi;uint8_t adv_type;char name[32];
} ble_item_t;#define BLE_LIST_MAX 64
static ble_item_t g_ble_list[BLE_LIST_MAX];
static int g_ble_cnt = 0;// 提取设备名(若有)
static void parse_name_from_adv(const uint8_t *data, uint8_t len, char out[32]) {out[0] = 0;uint8_t i = 0;while (i + 1 < len) {uint8_t l = data[i];if (l == 0 || i + l >= len) break;uint8_t type = data[i+1];if (type == 0x09 || type == 0x08) { // Complete/Shortened Local Nameuint8_t copy = l - 1;if (copy > 31) copy = 31;memcpy(out, &data[i+2], copy);out[copy] = 0;return;}i += (l + 1);}
}static int ble_gap_event_cb(struct ble_gap_event *event, void *arg) {switch (event->type) {case BLE_GAP_EVENT_DISC:if (g_ble_cnt < BLE_LIST_MAX) {ble_item_t *it = &g_ble_list[g_ble_cnt++];uint8_t addr[6];ble_addr_t a = event->disc.addr;memcpy(addr, a.val, 6);snprintf(it->addr, sizeof(it->addr),"%02X:%02X:%02X:%02X:%02X:%02X",addr[5], addr[4], addr[3], addr[2], addr[1], addr[0]);it->rssi = event->disc.rssi;it->adv_type = event->disc.event_type;parse_name_from_adv(event->disc.data, event->disc.length_data, it->name);}return 0;default:return 0;}
}static void ble_host_sync(void) {// 设定随机地址(如需要)ble_hs_id_infer_auto(0, NULL);
}static void ble_host_task(void *param) {nimble_port_run(); // 不会返回nimble_port_freertos_deinit();
}// 启动一轮 BLE 扫描
static esp_err_t do_ble_scan_seconds(int duration_sec) {g_ble_cnt = 0;struct ble_gap_disc_params params = {.itvl = BLE_SCAN_ITVL,.window = BLE_SCAN_WINDOW,.filter_policy = 0,.passive = 0,.limited = 0,.filter_duplicates = 1,};int rc = ble_gap_disc(BLE_OWN_ADDR_PUBLIC, duration_sec * 1000, ¶ms, ble_gap_event_cb, NULL);if (rc != 0) {ESP_LOGE(TAG, "BLE scan start failed rc=%d", rc);return ESP_FAIL;}// 简单等待扫描结束vTaskDelay(pdMS_TO_TICKS(duration_sec * 1000));ble_gap_disc_cancel();return ESP_OK;
}// ====== Wi-Fi 初始化/扫描 ======
static EventGroupHandle_t s_wifi_evt;
#define WIFI_EVT_CONNECTED BIT0static void wifi_event_handler(void* arg, esp_event_base_t base, int32_t id, void* data) {if (base == WIFI_EVENT && id == WIFI_EVENT_STA_START) {esp_wifi_connect();} else if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {xEventGroupClearBits(s_wifi_evt, WIFI_EVT_CONNECTED);ESP_LOGW(TAG, "Wi-Fi断开,重连中…");esp_wifi_connect();} else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {xEventGroupSetBits(s_wifi_evt, WIFI_EVT_CONNECTED);ESP_LOGI(TAG, "已获取IP");}
}static void wifi_init_sta(void) {ESP_ERROR_CHECK(esp_netif_init());ESP_ERROR_CHECK(esp_event_loop_create_default());esp_netif_create_default_wifi_sta();wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();ESP_ERROR_CHECK(esp_wifi_init(&cfg));s_wifi_evt = xEventGroupCreate();ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL));ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handler, NULL));wifi_config_t wc = {0};strncpy((char*)wc.sta.ssid, WIFI_SSID, sizeof(wc.sta.ssid));strncpy((char*)wc.sta.password, WIFI_PASS, sizeof(wc.sta.password));wc.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;wc.sta.sae_pwe_h2e = WPA3_SAE_PWE_BOTH;ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wc));ESP_ERROR_CHECK(esp_wifi_start());
}static int wifi_scan_collect(cJSON *wifi_arr) {wifi_scan_config_t sc = {0};
#if WIFI_SCAN_ACTIVEsc.scan_type = WIFI_SCAN_TYPE_ACTIVE;sc.scan_time.active.min = WIFI_SCAN_CHANNEL_TIME_MS;sc.scan_time.active.max = WIFI_SCAN_CHANNEL_TIME_MS + 40;
#elsesc.scan_type = WIFI_SCAN_TYPE_PASSIVE;sc.scan_time.passive = WIFI_SCAN_CHANNEL_TIME_MS + 60;
#endifESP_ERROR_CHECK(esp_wifi_scan_start(&sc, true));uint16_t ap_num = WIFI_SCAN_MAX_AP;wifi_ap_record_t aps[WIFI_SCAN_MAX_AP];ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(&ap_num, aps));for (int i = 0; i < ap_num; ++i) {cJSON *item = cJSON_CreateObject();cJSON_AddStringToObject(item, "ssid", (const char*)aps[i].ssid);char bssid[18];snprintf(bssid, sizeof(bssid), "%02X:%02X:%02X:%02X:%02X:%02X",aps[i].bssid[0], aps[i].bssid[1], aps[i].bssid[2],aps[i].bssid[3], aps[i].bssid[4], aps[i].bssid[5]);cJSON_AddStringToObject(item, "bssid", bssid);cJSON_AddNumberToObject(item, "rssi", aps[i].rssi);cJSON_AddNumberToObject(item, "primary", aps[i].primary);cJSON_AddNumberToObject(item, "auth", aps[i].authmode);cJSON_AddItemToArray(wifi_arr, item);}return ap_num;
}// ====== MQTT(可选) ======
#if ENABLE_MQTT
static void mqtt_start(void) {esp_mqtt_client_config_t cfg = {.broker.address.uri = MQTT_BROKER_URI,.session.protocol_ver = MQTT_PROTOCOL_V_3_1_1,.credentials.client_id = "esp32c3-scanner",.task.priority = 5,};s_mqtt = esp_mqtt_client_init(&cfg);esp_mqtt_client_start(s_mqtt);
}static void mqtt_publish_json(const char *topic, const char *json) {if (!s_mqtt) return;int msg_id = esp_mqtt_client_publish(s_mqtt, topic, json, 0, 0, 0);if (msg_id >= 0) {ESP_LOGI(TAG, "MQTT已发布,msg_id=%d, bytes=%d", msg_id, (int)strlen(json));} else {ESP_LOGW(TAG, "MQTT发布失败");}
}
#endif// ====== 主任务:合并扫描并输出/上报 ======
static void scan_cycle_task(void *arg) {while (1) {int64_t start_ms = epoch_millis();ESP_LOGI(TAG, "====== 新一轮扫描开始 ======");// 1) BLEESP_LOGI(TAG, "开始 BLE 扫描 %ds…", BLE_SCAN_DURATION_SEC);if (do_ble_scan_seconds(BLE_SCAN_DURATION_SEC) != ESP_OK) {ESP_LOGW(TAG, "BLE 扫描失败");}// 2) Wi-FicJSON *root = cJSON_CreateObject();cJSON_AddNumberToObject(root, "ts", (double)(epoch_millis()));cJSON *wifi_arr = cJSON_CreateArray();int wifi_cnt = wifi_scan_collect(wifi_arr);cJSON_AddItemToObject(root, "wifi_list", wifi_arr);cJSON *ble_arr = cJSON_CreateArray();for (int i = 0; i < g_ble_cnt; ++i) {cJSON *o = cJSON_CreateObject();cJSON_AddStringToObject(o, "addr", g_ble_list[i].addr);cJSON_AddNumberToObject(o, "rssi", g_ble_list[i].rssi);cJSON_AddNumberToObject(o, "adv_type", g_ble_list[i].adv_type);if (g_ble_list[i].name[0]) {cJSON_AddStringToObject(o, "name", g_ble_list[i].name);}cJSON_AddItemToArray(ble_arr, o);}cJSON_AddItemToObject(root, "ble_list", ble_arr);char *json = cJSON_PrintUnformatted(root);// 串口输出printf("%s\n", json);// 可选:MQTT上报#if ENABLE_MQTTmqtt_publish_json(MQTT_TOPIC, json);#endifcJSON_Delete(root);free(json);int64_t elapsed = epoch_millis() - start_ms;int64_t sleep_ms = SCAN_PERIOD_SEC * 1000 - elapsed;if (sleep_ms < 0) sleep_ms = 0;ESP_LOGI(TAG, "本轮完成:WiFi=%d, BLE=%d,用时=%" PRId64 " ms,休息=%" PRId64 " ms",wifi_cnt, g_ble_cnt, elapsed, sleep_ms);vTaskDelay(pdMS_TO_TICKS((uint32_t)sleep_ms));}
}// ====== NimBLE 初始化 ======
static void ble_init(void) {ESP_ERROR_CHECK(nimble_port_init());ble_hs_cfg.reset_cb = NULL;ble_hs_cfg.sync_cb = ble_host_sync;// 可设置GAP首选参数等…nimble_port_freertos_init(ble_host_task);
}// ====== app_main ======
void app_main(void) {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());ESP_ERROR_CHECK(nvs_flash_init());}ESP_LOGI(TAG, "系统启动");// 初始化 BLE Hostble_init();// 若启用 MQTT,需要联网
#if ENABLE_MQTTwifi_init_sta();// 等待联网(最多10秒)if (xEventGroupWaitBits(s_wifi_evt, WIFI_EVT_CONNECTED, false, true, pdMS_TO_TICKS(10000)) == 0) {ESP_LOGW(TAG, "10秒内未联网,后续只串口输出。");} else {mqtt_start();}
#else// 即便不联网也可以做 Wi-Fi 扫描:需要初始化 netif + wifi,但无需配置AP信息ESP_ERROR_CHECK(esp_netif_init());ESP_ERROR_CHECK(esp_event_loop_create_default());esp_netif_create_default_wifi_sta();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());
#endifxTaskCreatePinnedToCore(scan_cycle_task, "scan_cycle", 6144, NULL, 5, NULL, 0);
}
sdkconfig 关键点(如用 menuconfig 配)
Component config -> Bluetooth
选择 NimBLE(而非 Bluedroid)
适度调低
NimBLE max connections
,保守内存
Component config -> Wi-Fi
若仅扫描,可不开
Wi-Fi NVS flash
节省写入
Partition Table
默认
factory
足够;如启用 OTA/MQTT 大日志,可适当放宽nvs
/phy
上位机示例输出(一行一帧 JSON)
{"ts": 1731285230123,"wifi_list":[{"ssid":"Home","bssid":"AA:BB:CC:DD:EE:FF","rssi":-45,"primary":1,"auth":3}, ...],"ble_list":[{"addr":"12:34:56:78:9A:BC","rssi":-67,"adv_type":0,"name":"MiBand"}, ...]
}
调优建议
射频复用与时间片:示例采用“先 BLE 后 Wi-Fi”的顺序,避免两者同时占用 RF,减少丢包。
速率限制:
SCAN_PERIOD_SEC
≥ 10s 较稳妥,过于频繁会影响系统与空口。功耗:将
BLE_SCAN_WINDOW
设为BLE_SCAN_ITVL * 0.5~0.6
,并在两轮之间vTaskDelay
。离线场景可考虑esp_light_sleep_start()
做轻睡。数据量:可为
wifi_list/ble_list
设置 RSSI 阈值过滤(如< -90dBm
丢弃),或限制最大数量。MQTT 可靠性:生产环境请启用
session.keepalive
、LWT、QoS1/2 与重连回退。合规性:采集与上报他人设备信息需遵守当地法律法规与隐私政策;生产前务必征得授权。
如果你希望把上报方式改为 HTTP(S) / WebSocket,或者把Wi-Fi 凭据用 BluFi/配网 App 来下发(你常用的 CozyLife / BluFi 流程),我可以直接把上面工程改成对应版本,并补上 sdkconfig.defaults
与分区表。