/** * @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 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; }