下面給你一個基于 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
與分區表。