301 lines
8.6 KiB
C
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;
|
|
}
|