Files
c5-project/ESP32-C5-Toolkit/main/bt_scanner.c
2026-05-03 23:19:51 -07:00

301 lines
8.6 KiB
C

/**
* @file bt_scanner.c
* @brief NimBLE-based BLE Scanner for ESP32-C5
*
* Uses NimBLE stack (not Bluedroid) which is required for ESP32-C5
*/
#include "bt_scanner.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "host/ble_hs.h"
#include "host/util/util.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include <string.h>
static const char *TAG = "ble_scanner";
#define MAX_BT_DEVICES 30
// Global state
static bool ble_initialized = false;
static bool scan_active = false;
static bt_device_t devices[MAX_BT_DEVICES];
static int device_count = 0;
static SemaphoreHandle_t ble_mutex = NULL;
static uint8_t own_addr_type;
// Forward declarations
static void ble_host_task(void *param);
static int ble_gap_event(struct ble_gap_event *event, void *arg);
// Check if device already exists in list
static bool device_exists(const uint8_t *addr) {
for (int i = 0; i < device_count; i++) {
if (memcmp(devices[i].addr, addr, 6) == 0) {
return true;
}
}
return false;
}
// Parse device name from advertising data
static void parse_adv_name(const uint8_t *data, uint8_t len, char *name_out, size_t name_size) {
name_out[0] = '\0';
uint8_t pos = 0;
while (pos < len) {
uint8_t field_len = data[pos];
if (field_len == 0 || pos + field_len >= len) break;
uint8_t field_type = data[pos + 1];
// Complete Local Name (0x09) or Shortened Local Name (0x08)
if (field_type == 0x09 || field_type == 0x08) {
uint8_t name_len = field_len - 1;
if (name_len >= name_size) name_len = name_size - 1;
memcpy(name_out, &data[pos + 2], name_len);
name_out[name_len] = '\0';
return;
}
pos += field_len + 1;
}
}
// GAP event handler for scan results
static int ble_gap_event(struct ble_gap_event *event, void *arg) {
switch (event->type) {
case BLE_GAP_EVENT_DISC: {
// Received advertising report
if (device_count >= MAX_BT_DEVICES) {
break;
}
// Check if device already in list
if (device_exists(event->disc.addr.val)) {
break;
}
if (xSemaphoreTake(ble_mutex, pdMS_TO_TICKS(10)) == pdTRUE) {
bt_device_t *dev = &devices[device_count];
// Copy address (reverse byte order for display)
for (int i = 0; i < 6; i++) {
dev->addr[i] = event->disc.addr.val[5 - i];
}
dev->rssi = event->disc.rssi;
dev->adv_type = event->disc.event_type;
dev->timestamp = (uint32_t)(esp_timer_get_time() / 1000000ULL);
// Parse name from advertising data
struct ble_hs_adv_fields fields;
if (ble_hs_adv_parse_fields(&fields, event->disc.data, event->disc.length_data) == 0) {
if (fields.name != NULL && fields.name_len > 0) {
size_t copy_len = fields.name_len < 31 ? fields.name_len : 31;
memcpy(dev->name, fields.name, copy_len);
dev->name[copy_len] = '\0';
} else {
strcpy(dev->name, "Unknown");
}
} else {
// Fallback: try manual parsing
parse_adv_name(event->disc.data, event->disc.length_data, dev->name, sizeof(dev->name));
if (dev->name[0] == '\0') {
strcpy(dev->name, "Unknown");
}
}
device_count++;
ESP_LOGI(TAG, "Device %d: %02X:%02X:%02X:%02X:%02X:%02X RSSI:%d %s",
device_count,
dev->addr[0], dev->addr[1], dev->addr[2],
dev->addr[3], dev->addr[4], dev->addr[5],
dev->rssi, dev->name);
xSemaphoreGive(ble_mutex);
}
break;
}
case BLE_GAP_EVENT_DISC_COMPLETE:
ESP_LOGI(TAG, "BLE scan complete, found %d devices", device_count);
scan_active = false;
break;
default:
break;
}
return 0;
}
// NimBLE host sync callback
static void ble_on_sync(void) {
int rc;
// Determine address type
rc = ble_hs_util_ensure_addr(0);
if (rc != 0) {
ESP_LOGE(TAG, "Failed to ensure address: %d", rc);
return;
}
rc = ble_hs_id_infer_auto(0, &own_addr_type);
if (rc != 0) {
ESP_LOGE(TAG, "Failed to infer address type: %d", rc);
return;
}
ESP_LOGI(TAG, "NimBLE host synced, ready to scan");
ble_initialized = true;
}
// NimBLE host reset callback
static void ble_on_reset(int reason) {
ESP_LOGW(TAG, "NimBLE host reset, reason: %d", reason);
ble_initialized = false;
}
// NimBLE host task
static void ble_host_task(void *param) {
ESP_LOGI(TAG, "NimBLE host task started");
nimble_port_run();
nimble_port_freertos_deinit();
}
bool bt_scanner_init(void) {
if (ble_initialized) {
return true;
}
ESP_LOGI(TAG, "Initializing NimBLE BLE scanner");
// Create mutex
if (ble_mutex == NULL) {
ble_mutex = xSemaphoreCreateMutex();
if (ble_mutex == NULL) {
ESP_LOGE(TAG, "Failed to create mutex");
return false;
}
}
// Initialize NimBLE
esp_err_t ret = nimble_port_init();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to init NimBLE port: %s", esp_err_to_name(ret));
return false;
}
// Configure NimBLE host
ble_hs_cfg.sync_cb = ble_on_sync;
ble_hs_cfg.reset_cb = ble_on_reset;
// Start NimBLE host task
nimble_port_freertos_init(ble_host_task);
// Wait for sync (up to 2 seconds)
for (int i = 0; i < 20 && !ble_initialized; i++) {
vTaskDelay(pdMS_TO_TICKS(100));
}
if (!ble_initialized) {
ESP_LOGW(TAG, "NimBLE not synced yet, but continuing");
}
ESP_LOGI(TAG, "NimBLE BLE scanner initialized");
return true;
}
bool bt_scan_start(void) {
if (!ble_initialized) {
if (!bt_scanner_init()) {
ESP_LOGE(TAG, "BLE not initialized");
return false;
}
// Wait a bit more for sync
vTaskDelay(pdMS_TO_TICKS(500));
}
if (scan_active) {
ESP_LOGW(TAG, "Scan already active");
return true;
}
// Clear previous results
if (xSemaphoreTake(ble_mutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
device_count = 0;
memset(devices, 0, sizeof(devices));
xSemaphoreGive(ble_mutex);
}
// Configure scan parameters
struct ble_gap_disc_params scan_params = {
.itvl = BLE_GAP_SCAN_ITVL_MS(100), // 100ms interval
.window = BLE_GAP_SCAN_WIN_MS(100), // 100ms window (continuous)
.filter_policy = BLE_HCI_SCAN_FILT_NO_WL,
.limited = 0,
.passive = 0, // Active scanning (request scan response)
.filter_duplicates = 1,
};
// Start discovery (scan for 30 seconds)
int rc = ble_gap_disc(own_addr_type, 30000, &scan_params, ble_gap_event, NULL);
if (rc != 0) {
ESP_LOGE(TAG, "Failed to start scan: %d", rc);
return false;
}
scan_active = true;
ESP_LOGI(TAG, "BLE scan started");
return true;
}
bool bt_scan_stop(void) {
if (!scan_active) {
return true;
}
int rc = ble_gap_disc_cancel();
if (rc != 0 && rc != BLE_HS_EALREADY) {
ESP_LOGE(TAG, "Failed to stop scan: %d", rc);
return false;
}
scan_active = false;
ESP_LOGI(TAG, "BLE scan stopped, found %d devices", device_count);
return true;
}
int bt_get_devices(bt_device_t *out_devices, int max_devices) {
if (out_devices == NULL) {
return 0;
}
int count = 0;
if (xSemaphoreTake(ble_mutex, pdMS_TO_TICKS(1000)) == pdTRUE) {
count = (device_count < max_devices) ? device_count : max_devices;
memcpy(out_devices, devices, count * sizeof(bt_device_t));
xSemaphoreGive(ble_mutex);
}
return count;
}
// Stub implementations for jamming (not implemented)
bool bt_jam_start(uint8_t *target_addr, uint32_t duration) {
ESP_LOGW(TAG, "BT jamming not implemented");
return false;
}
bool bt_jam_stop(void) {
return true;
}
bool bt_jam_is_active(void) {
return false;
}