diff --git a/src/core/scrollableTextArea.cpp b/src/core/scrollableTextArea.cpp index 55b888575..14085aac1 100644 --- a/src/core/scrollableTextArea.cpp +++ b/src/core/scrollableTextArea.cpp @@ -3,7 +3,7 @@ ScrollableTextArea::ScrollableTextArea(const String &title) : firstVisibleLine{0}, _redraw{true}, _title(title), _fontSize(FP), _startX(BORDER_PAD_X), _startY(BORDER_PAD_Y), _width(tftWidth - 2 * BORDER_PAD_X), - _height(tftHeight - BORDER_PAD_X - BORDER_PAD_Y) { + _height(tftHeight - 4 - BORDER_PAD_X - BORDER_PAD_Y) { drawMainBorder(); if (!_title.isEmpty()) { @@ -76,11 +76,11 @@ void ScrollableTextArea::show(bool force) { while (check(SelPress)) { update(force); - vTaskDelay(pdMS_TO_TICKS(1)); + yield(); } while (!check(SelPress)) { update(force); - vTaskDelay(pdMS_TO_TICKS(1)); + yield(); } } @@ -121,7 +121,7 @@ void ScrollableTextArea::fromString(const String &text) { endIdx++; } - // Add the last line if there’s remaining text (text does not ends with \n) + // Add the last line if there's remaining text (text does not ends with \n) if (startIdx < text.length()) { addLine(text.substring(startIdx, endIdx)); } } diff --git a/src/modules/ble/BLE_Suite.cpp b/src/modules/ble/BLE_Suite.cpp index 5320dae78..7afa2bed6 100644 --- a/src/modules/ble/BLE_Suite.cpp +++ b/src/modules/ble/BLE_Suite.cpp @@ -2,7 +2,7 @@ * BLE Suite v3.1 - Complete BLE attack and analysis toolkit * Author: Ninja-jr * Version: 3.1 - * Last Updated: 2026-01-24 + * Last Updated: 19/07/2026 * * Contains: Vulnerability scanning, HID attacks, FastPair exploits, * HFP attacks, Audio attacks, DuckyScript injection, @@ -16,6 +16,7 @@ #include "ble_common.h" #include "core/display.h" #include "core/mykeyboard.h" +#include "core/radio_mem.h" #include "core/utils.h" #include "core/wifi/wifi_common.h" #include "fastpair_crypto.h" @@ -26,6 +27,30 @@ #include #include +//============================================================================= +// NimBLE Version Detection - Must match ble_common.h +//============================================================================= + +// Detect NimBLE 2.x by checking for features only available in v2+ +#if defined(NIMBLE_VERSION) + #if NIMBLE_VERSION >= 20000 + #define NIMBLE_V2_PLUS 1 + #endif +#elif defined(NIMBLE_CPP_VERSION) && NIMBLE_CPP_VERSION >= 2 + #define NIMBLE_V2_PLUS 1 +#elif defined(NIMBLE_VERSION_MAJOR) && NIMBLE_VERSION_MAJOR >= 2 + #define NIMBLE_V2_PLUS 1 +#elif defined(NIMBLE_VERSION_MAJOR) && NIMBLE_VERSION_MAJOR == 1 && NIMBLE_VERSION_MINOR >= 5 + #define NIMBLE_V2_PLUS 1 +#elif __has_include() + #define NIMBLE_V2_PLUS 1 +#endif + +// If none of the above matched, default to v1 behavior (safe fallback) +#ifndef NIMBLE_V2_PLUS + #define NIMBLE_V2_PLUS 0 +#endif + int showSubMenu(const char *title, const char *options[], int optionCount); extern tft_logger tft; @@ -40,6 +65,28 @@ bool BLEStateManager::bleInitialized = false; std::vector BLEStateManager::activeClients; String BLEStateManager::currentDeviceName = ""; +// Scan state management +static NimBLEScan* g_pBLEScan = nullptr; +static bool g_bleScanActive = false; + +// Device selection cache +static SelectedDevice g_selectedDevice; + +//============================================================================= +// Cleanup Function - Only stops scan, doesn't clear data +//============================================================================= + +void cleanupBLESuiteState() { + if (g_pBLEScan) { + g_pBLEScan->stop(); + g_pBLEScan->clearResults(); + g_bleScanActive = false; + } + // DO NOT clear scannerData or g_selectedDevice here + // They persist between operations + delay(50); +} + //============================================================================= // v3.1: Samsung MAC OUI Detection //============================================================================= @@ -67,27 +114,31 @@ bool isSamsungDevice(const String &mac) { FastPairVersion detectFastPairVersion(NimBLEAddress target) { return FP_VERSION_2; } //============================================================================= -// ScannerData Implementation +// ScannerData Implementation with Snapshot Support //============================================================================= ScannerData::ScannerData() { mutex = xSemaphoreCreateMutex(); foundCount = 0; + dataVersion = 0; + snapshotCache = nullptr; + cacheTimestamp = 0; } ScannerData::~ScannerData() { if (mutex) vSemaphoreDelete(mutex); + if (snapshotCache) delete snapshotCache; } void ScannerData::addDevice( const String &name, const String &address, int rssi, bool fastPair, bool hasHFP, uint8_t type ) { - if (xSemaphoreTake(mutex, portMAX_DELAY)) { + if (xSemaphoreTake(mutex, 10 / portTICK_PERIOD_MS)) { bool isDuplicate = false; for (size_t i = 0; i < deviceAddresses.size(); i++) { if (deviceAddresses[i] == address) { isDuplicate = true; - deviceRssi[i] = rssi; + if (rssi > deviceRssi[i]) deviceRssi[i] = rssi; break; } } @@ -99,13 +150,65 @@ void ScannerData::addDevice( deviceHasHFP.push_back(hasHFP); deviceTypes.push_back(type); foundCount++; + dataVersion++; + + if (snapshotCache) { + delete snapshotCache; + snapshotCache = nullptr; + } } xSemaphoreGive(mutex); } } +DeviceSnapshot* ScannerData::getSnapshot() { + if (snapshotCache && (millis() - cacheTimestamp) < 1000) { + return snapshotCache; + } + + if (xSemaphoreTake(mutex, 50 / portTICK_PERIOD_MS)) { + if (snapshotCache) { + delete snapshotCache; + snapshotCache = nullptr; + } + + snapshotCache = new DeviceSnapshot(); + snapshotCache->version = dataVersion; + snapshotCache->count = deviceAddresses.size(); + snapshotCache->timestamp = millis(); + snapshotCache->names = deviceNames; + snapshotCache->addresses = deviceAddresses; + snapshotCache->rssi = deviceRssi; + snapshotCache->fastPair = deviceFastPair; + snapshotCache->hfp = deviceHasHFP; + snapshotCache->types = deviceTypes; + + cacheTimestamp = millis(); + xSemaphoreGive(mutex); + return snapshotCache; + } + return nullptr; +} + +bool ScannerData::getDeviceInfo(int index, DeviceInfo &info) { + bool success = false; + if (xSemaphoreTake(mutex, 10 / portTICK_PERIOD_MS)) { + if (index >= 0 && index < (int)deviceAddresses.size()) { + info.address = deviceAddresses[index]; + info.name = deviceNames[index]; + info.rssi = deviceRssi[index]; + info.hasFastPair = deviceFastPair[index]; + info.hasHFP = deviceHasHFP[index]; + info.deviceType = deviceTypes[index]; + success = true; + } + xSemaphoreGive(mutex); + } + return success; +} + void ScannerData::clear() { - if (xSemaphoreTake(mutex, portMAX_DELAY)) { + if (xSemaphoreTake(mutex, 50 / portTICK_PERIOD_MS)) { deviceNames.clear(); deviceAddresses.clear(); deviceRssi.clear(); @@ -113,13 +216,19 @@ void ScannerData::clear() { deviceHasHFP.clear(); deviceTypes.clear(); foundCount = 0; + dataVersion++; + + if (snapshotCache) { + delete snapshotCache; + snapshotCache = nullptr; + } xSemaphoreGive(mutex); } } size_t ScannerData::size() { size_t result = 0; - if (xSemaphoreTake(mutex, portMAX_DELAY)) { + if (xSemaphoreTake(mutex, 10 / portTICK_PERIOD_MS)) { result = deviceAddresses.size(); xSemaphoreGive(mutex); } @@ -218,6 +327,11 @@ bool BLEStateManager::initBLE(const String &name, int powerLevel) { if (bleInitialized) deinitBLE(true); + if (!radioHasMemForBle()) { + displayError("Low RAM: free WiFi/SD first", true); + return false; + } + std::string nameStr = name.c_str(); NimBLEDevice::init(nameStr); NimBLEDevice::setPower((esp_power_level_t)powerLevel); @@ -233,6 +347,8 @@ void BLEStateManager::deinitBLE(bool immediate) { NimBLEDevice::deinit(true); bleInitialized = false; currentDeviceName = ""; + g_pBLEScan = nullptr; + g_bleScanActive = false; } void BLEStateManager::registerClient(NimBLEClient *client) { @@ -424,14 +540,14 @@ NimBLEClient *attemptConnectionWithStrategies(NimBLEAddress target, String &conn } bool hasHFP = false; - if (xSemaphoreTake(scannerData.mutex, portMAX_DELAY)) { - for (size_t i = 0; i < scannerData.deviceAddresses.size(); i++) { - if (scannerData.deviceAddresses[i] == target.toString().c_str()) { - hasHFP = scannerData.deviceHasHFP[i]; + DeviceInfo info; + for (size_t i = 0; i < scannerData.size(); i++) { + if (scannerData.getDeviceInfo(i, info)) { + if (info.address == target.toString().c_str()) { + hasHFP = info.hasHFP; break; } } - xSemaphoreGive(scannerData.mutex); } if (hasHFP) { @@ -1880,16 +1996,16 @@ bool HIDDuckyService::injectDuckyScript(NimBLEAddress target, const String &scri bool hasHFP = false; String deviceName = ""; + DeviceInfo info; - if (xSemaphoreTake(scannerData.mutex, portMAX_DELAY)) { - for (size_t i = 0; i < scannerData.deviceAddresses.size(); i++) { - if (scannerData.deviceAddresses[i] == target.toString().c_str()) { - deviceName = scannerData.deviceNames[i]; - hasHFP = scannerData.deviceHasHFP[i]; + for (size_t i = 0; i < scannerData.size(); i++) { + if (scannerData.getDeviceInfo(i, info)) { + if (info.address == target.toString().c_str()) { + deviceName = info.name; + hasHFP = info.hasHFP; break; } } - xSemaphoreGive(scannerData.mutex); } if (hasHFP && !deviceName.isEmpty()) { @@ -2564,16 +2680,16 @@ bool HIDAttackServiceClass::injectKeystrokes(NimBLEAddress target) { bool hasHFP = false; String deviceName = ""; + DeviceInfo info; - if (xSemaphoreTake(scannerData.mutex, portMAX_DELAY)) { - for (size_t i = 0; i < scannerData.deviceAddresses.size(); i++) { - if (scannerData.deviceAddresses[i] == target.toString().c_str()) { - deviceName = scannerData.deviceNames[i]; - hasHFP = scannerData.deviceHasHFP[i]; + for (size_t i = 0; i < scannerData.size(); i++) { + if (scannerData.getDeviceInfo(i, info)) { + if (info.address == target.toString().c_str()) { + deviceName = info.name; + hasHFP = info.hasHFP; break; } } - xSemaphoreGive(scannerData.mutex); } if (hasHFP && !deviceName.isEmpty()) { @@ -2971,8 +3087,10 @@ String selectFileFromSD() { } tft.setTextColor(TFT_GREEN, bruceConfig.bgColor); - tft.setCursor(20, tftHeight - 35); - tft.print("SEL: Select PREV/NEXT: Navigate ESC: Back"); + tft.setCursor(20, tftHeight - 30); + tft.print("SEL: Select PREV/NEXT: Navigate"); + tft.setCursor(20, tftHeight - 20); + tft.print("ESC: Back"); lastSelected = selected; lastScrollOffset = scrollOffset; @@ -3095,8 +3213,10 @@ String getScriptFromUser() { } tft.setTextColor(TFT_GREEN, bruceConfig.bgColor); - tft.setCursor(20, tftHeight - 35); - tft.print("SEL: Select PREV/NEXT: Navigate ESC: Back"); + tft.setCursor(20, tftHeight - 30); + tft.print("SEL: Select PREV/NEXT: Navigate"); + tft.setCursor(20, tftHeight - 20); + tft.print("ESC: Back"); lastSelected = selected; lastScrollOffset = scrollOffset; @@ -3194,11 +3314,21 @@ std::vector FastPairExploitEngine::scanForFastPairDevices(in pScan->setActiveScan(true); pScan->setInterval(97); pScan->setWindow(67); - pScan->start(duration, false); - NimBLEScanResults results = pScan->getResults(); + // Use the same version detection as ble_common.h +#if NIMBLE_V2_PLUS + NimBLEScanResults results = pScan->getResults(duration * 1000, false); +#else + // NimBLE 1.x: start returns NimBLEScanResults directly + NimBLEScanResults results = pScan->start(duration, false); +#endif + for (int i = 0; i < results.getCount(); i++) { +#if NIMBLE_V2_PLUS const NimBLEAdvertisedDevice *device = results.getDevice(i); +#else + NimBLEAdvertisedDevice *device = results.getDevice(i); +#endif String address = String(device->getAddress().toString().c_str()); String name = device->getName().c_str(); @@ -3834,7 +3964,11 @@ void BLE_Sniffer() { NimBLEScanResults results = pScan->getResults(10 * 1000, true); for (int i = 0; i < results.getCount(); i++) { +#if NIMBLE_V2_PLUS const NimBLEAdvertisedDevice *device = results.getDevice(i); +#else + NimBLEAdvertisedDevice *device = results.getDevice(i); +#endif SnifferPacket packet; packet.address = String(device->getAddress().toString().c_str()); @@ -4034,122 +4168,362 @@ void BLE_Sniffer() { } //============================================================================= -// Target Selection Functions +// Target Selection Functions - Uses snapshot for safe data access //============================================================================= String selectTargetFromScan(const char *title) { - if (scannerData.size() == 0) { - showErrorMessage("No devices found. Run scan first."); + // Simple memory check - if heap is low, warn but continue + if (heap_caps_get_free_size(MALLOC_CAP_DEFAULT) < 10000) { + displayError("Low memory, scan may be unstable", true); + // Don't return - let the user decide + } + + // DO NOT clear scannerData here - it persists between operations + g_selectedDevice.address = ""; + g_selectedDevice.name = ""; + + bool bleWasActiveBefore = BLEConnected || (BLEDevice::getServer() != nullptr); +#if !defined(LITE_VERSION) + bleWasActiveBefore = bleWasActiveBefore || BLEStateManager::isBLEActive() || BLEStateManager::getActiveClientCount() > 0; +#endif + + if (!bleWasActiveBefore) { + if (!BLEStateManager::initBLE("Bruce-Scanner", ESP_PWR_LVL_P9)) { + displayError("Failed to init BLE"); + return ""; + } + } + + if (g_pBLEScan == nullptr) { + g_pBLEScan = NimBLEDevice::getScan(); + if (!g_pBLEScan) { + displayError("Failed to get scanner"); + return ""; + } + g_pBLEScan->setActiveScan(true); + g_pBLEScan->setInterval(SCAN_INT); + g_pBLEScan->setWindow(SCAN_WINDOW); + g_pBLEScan->setDuplicateFilter(false); + } + + // Clear previous results before scanning + g_pBLEScan->clearResults(); + + tft.fillScreen(bruceConfig.bgColor); + tft.drawRect(5, 5, tftWidth - 10, tftHeight - 10, TFT_WHITE); + + tft.setTextColor(TFT_WHITE, bruceConfig.bgColor); + tft.setTextSize(2); + String titleStr = String(title); + int maxTitleWidth = tftWidth - 20; + if (tft.textWidth(titleStr.c_str()) > maxTitleWidth) { + while (titleStr.length() > 0 && tft.textWidth((titleStr + "...").c_str()) > maxTitleWidth) { + titleStr.remove(titleStr.length() - 1); + } + titleStr += "..."; + } + tft.setCursor(10, 15); + tft.print(titleStr); + tft.setTextSize(1); + + tft.setCursor(20, 60); + tft.print("Scanning for devices..."); + + // Use fixed scan times + int activeScanTime = ACTIVE_SCAN_TIME; + int passiveScanTime = PASSIVE_SCAN_TIME; + + // If memory is very low, use reduced scan times + if (heap_caps_get_free_size(MALLOC_CAP_DEFAULT) < 15000) { + activeScanTime = 3; + passiveScanTime = 3; + } + + // === ACTIVE SCAN === + g_pBLEScan->setActiveScan(true); + tft.setCursor(20, 80); + tft.print("Active scan (" + String(activeScanTime) + "s)..."); + + try { +#if NIMBLE_V2_PLUS + BLEScanResults activeResults = g_pBLEScan->getResults(activeScanTime * 1000, false); +#else + // NimBLE 1.x API: start returns NimBLEScanResults + BLEScanResults activeResults = g_pBLEScan->start(activeScanTime, false); +#endif + + for (int i = 0; i < activeResults.getCount(); i++) { +#if NIMBLE_V2_PLUS + const NimBLEAdvertisedDevice *device = activeResults.getDevice(i); +#else + NimBLEAdvertisedDevice *device = activeResults.getDevice(i); +#endif + if (!device) continue; + + String address = String(device->getAddress().toString().c_str()); + String name = String(device->getName().c_str()); + if (name.isEmpty() || name == "(null)" || name == "null" || name == "NULL") { + name = "Unknown"; + } + int rssi = device->getRSSI(); + if (rssi == 0) rssi = -100; + + bool fastPair = false, hasHFP = false; + uint8_t deviceType = 0; + + if (device->haveServiceUUID()) { + NimBLEUUID uuid = device->getServiceUUID(); + std::string uuidStr = uuid.toString(); + if (uuidStr.find("fe2c") != std::string::npos) fastPair = true; + if (uuidStr.find("111e") != std::string::npos || uuidStr.find("111f") != std::string::npos) + hasHFP = true; + if (uuidStr.find("110e") != std::string::npos || uuidStr.find("110f") != std::string::npos) + deviceType |= 0x01; + if (uuidStr.find("1812") != std::string::npos) deviceType |= 0x02; + } + + scannerData.addDevice(name, address, rssi, fastPair, hasHFP, deviceType); + } + + // === PASSIVE SCAN === + g_pBLEScan->setActiveScan(false); + tft.setCursor(20, 100); + tft.print("Passive scan (" + String(passiveScanTime) + "s)..."); + +#if NIMBLE_V2_PLUS + BLEScanResults passiveResults = g_pBLEScan->getResults(passiveScanTime * 1000, false); +#else + BLEScanResults passiveResults = g_pBLEScan->start(passiveScanTime, false); +#endif + + for (int i = 0; i < passiveResults.getCount(); i++) { +#if NIMBLE_V2_PLUS + const NimBLEAdvertisedDevice *device = passiveResults.getDevice(i); +#else + NimBLEAdvertisedDevice *device = passiveResults.getDevice(i); +#endif + if (!device) continue; + + String address = String(device->getAddress().toString().c_str()); + String name = String(device->getName().c_str()); + if (name.isEmpty() || name == "(null)" || name == "null" || name == "NULL") { + name = "Unknown"; + } + int rssi = device->getRSSI(); + if (rssi == 0) rssi = -100; + + bool fastPair = false, hasHFP = false; + uint8_t deviceType = 0; + + if (device->haveServiceUUID()) { + NimBLEUUID uuid = device->getServiceUUID(); + std::string uuidStr = uuid.toString(); + if (uuidStr.find("fe2c") != std::string::npos) fastPair = true; + if (uuidStr.find("111e") != std::string::npos || uuidStr.find("111f") != std::string::npos) + hasHFP = true; + if (uuidStr.find("110e") != std::string::npos || uuidStr.find("110f") != std::string::npos) + deviceType |= 0x01; + if (uuidStr.find("1812") != std::string::npos) deviceType |= 0x02; + } + + scannerData.addDevice(name, address, rssi, fastPair, hasHFP, deviceType); + } + } catch (...) { + displayError("BLE scan error"); + if (g_pBLEScan) { + g_pBLEScan->clearResults(); + } return ""; } - int selected = 0, scrollOffset = 0; + // Stop the scan but DON'T clear results yet - we need them for display + if (g_pBLEScan) { + g_pBLEScan->stop(); + g_bleScanActive = false; + } + + // Get snapshot of discovered devices + DeviceSnapshot* snapshot = scannerData.getSnapshot(); + if (!snapshot || snapshot->count == 0) { + tft.fillScreen(TFT_YELLOW); + tft.drawRect(5, 5, tftWidth - 10, tftHeight - 10, TFT_BLACK); + tft.setTextColor(TFT_BLACK, TFT_YELLOW); + tft.setTextSize(2); + tft.setCursor((tftWidth - tft.textWidth("NO DEVICES")) / 2, 15); + tft.print("NO DEVICES"); + tft.setTextSize(1); + tft.setCursor(20, 60); + tft.print("No BLE devices found!"); + tft.setCursor(20, 80); + tft.print("Make sure BLE devices are"); + tft.setCursor(20, 100); + tft.print("turned on and in range."); + delay(2000); + return ""; + } + + size_t deviceCount = snapshot->count; + + // Sort with manual swap for vector + for (size_t i = 0; i < deviceCount - 1; i++) { + for (size_t j = i + 1; j < deviceCount; j++) { + bool swapNeeded = false; + if (snapshot->fastPair[j] && !snapshot->fastPair[i]) swapNeeded = true; + else if (snapshot->fastPair[j] == snapshot->fastPair[i] && + snapshot->rssi[j] > snapshot->rssi[i]) + swapNeeded = true; + + if (swapNeeded) { + std::swap(snapshot->names[i], snapshot->names[j]); + std::swap(snapshot->addresses[i], snapshot->addresses[j]); + std::swap(snapshot->rssi[i], snapshot->rssi[j]); + + // Manual swap for vector proxy references + bool tempFast = snapshot->fastPair[i]; + snapshot->fastPair[i] = snapshot->fastPair[j]; + snapshot->fastPair[j] = tempFast; + + bool tempHfp = snapshot->hfp[i]; + snapshot->hfp[i] = snapshot->hfp[j]; + snapshot->hfp[j] = tempHfp; + + std::swap(snapshot->types[i], snapshot->types[j]); + } + } + } + + // UI selection loop + int deviceItemHeight = 30, menuStartY = 60; + int maxVisibleDevices = (tftHeight - 45 - menuStartY) / deviceItemHeight; + if (maxVisibleDevices < 1) maxVisibleDevices = 1; + int selectedIdx = 0, scrollOffset = 0; int lastSelected = -1, lastScrollOffset = -1; - bool exitMenu = false; - int menuStartY = 60, menuItemHeight = 25; - size_t deviceCount = scannerData.size(); - int maxVisibleItems = (tftHeight - menuStartY - 50) / menuItemHeight; - if (maxVisibleItems > (int)deviceCount) maxVisibleItems = deviceCount; + bool exitLoop = false; - while (!exitMenu) { - if (selected != lastSelected || scrollOffset != lastScrollOffset) { + while (!exitLoop) { + if (selectedIdx != lastSelected || scrollOffset != lastScrollOffset) { tft.fillScreen(bruceConfig.bgColor); tft.drawRect(5, 5, tftWidth - 10, tftHeight - 10, TFT_WHITE); tft.setTextColor(TFT_WHITE, bruceConfig.bgColor); tft.setTextSize(2); - tft.setCursor((tftWidth - tft.textWidth(title)) / 2, 15); - tft.print(title); + tft.setCursor((tftWidth - tft.textWidth("SELECT DEVICE")) / 2, 15); + tft.print("SELECT DEVICE"); tft.setTextSize(1); tft.setTextColor(TFT_YELLOW, bruceConfig.bgColor); tft.setCursor(20, 40); - tft.print("Devices: "); + tft.print("Found: "); tft.print(deviceCount); + tft.print(" devices"); - for (int i = 0; i < maxVisibleItems && (scrollOffset + i) < (int)deviceCount; i++) { + for (int i = 0; i < maxVisibleDevices && (scrollOffset + i) < (int)deviceCount; i++) { int idx = scrollOffset + i; - int yPos = menuStartY + (i * menuItemHeight); - if (yPos + menuItemHeight > tftHeight - 45) break; - - if (idx == selected) { - tft.fillRect(20, yPos, tftWidth - 40, menuItemHeight - 3, TFT_WHITE); + int yPos = menuStartY + (i * deviceItemHeight); + if (yPos + deviceItemHeight > tftHeight - 45) break; + + String displayText = snapshot->names[idx]; + if (displayText.length() > 18) displayText = displayText.substring(0, 15) + "..."; + displayText += " (" + String(snapshot->rssi[idx]) + "dB)"; + if (snapshot->fastPair[idx]) displayText += " [FP]"; + if (snapshot->hfp[idx]) displayText += " [HFP]"; + if (snapshot->types[idx] & 0x01) displayText += " [AUDIO]"; + if (snapshot->types[idx] & 0x02) displayText += " [HID]"; + + if (idx == selectedIdx) { + tft.fillRect(15, yPos, tftWidth - 30, deviceItemHeight - 5, TFT_WHITE); tft.setTextColor(TFT_BLACK, TFT_WHITE); - tft.setCursor(25, yPos + 8); + tft.setCursor(20, yPos + 10); tft.print("> "); } else { - tft.fillRect(20, yPos, tftWidth - 40, menuItemHeight - 3, bruceConfig.bgColor); + tft.fillRect(15, yPos, tftWidth - 30, deviceItemHeight - 5, bruceConfig.bgColor); tft.setTextColor(TFT_WHITE, bruceConfig.bgColor); - tft.setCursor(25, yPos + 8); + tft.setCursor(20, yPos + 10); tft.print(" "); } - - String display = String(idx + 1) + ". " + scannerData.deviceNames[idx] + " | " + - scannerData.deviceAddresses[idx] + " | " + - String(scannerData.deviceRssi[idx]) + "dB"; - if (display.length() > 28) display = display.substring(0, 25) + "..."; - tft.print(display); + tft.print(displayText); } - if (deviceCount > (size_t)maxVisibleItems) { + if (deviceCount > (size_t)maxVisibleDevices) { tft.setTextColor(TFT_CYAN, bruceConfig.bgColor); - tft.setCursor(tftWidth - 25, menuStartY + 5); + tft.setCursor(tftWidth - 25, menuStartY + 10); if (scrollOffset > 0) tft.print("^"); - tft.setCursor(tftWidth - 25, menuStartY + (maxVisibleItems * menuItemHeight) - 20); - if (scrollOffset + maxVisibleItems < (int)deviceCount) tft.print("v"); + tft.setCursor(tftWidth - 25, menuStartY + (maxVisibleDevices * deviceItemHeight) - 15); + if (scrollOffset + maxVisibleDevices < (int)deviceCount) tft.print("v"); } tft.setTextColor(TFT_GREEN, bruceConfig.bgColor); - tft.setCursor(20, tftHeight - 35); - tft.print("SEL: Select PREV/NEXT: Navigate ESC: Back"); + tft.setCursor(20, tftHeight - 30); + tft.print("SEL: Select PREV/NEXT: Navigate"); + tft.setCursor(20, tftHeight - 20); + tft.print("ESC: Back"); - lastSelected = selected; + lastSelected = selectedIdx; lastScrollOffset = scrollOffset; } if (check(EscPress)) { - delay(200); - exitMenu = true; - return ""; + exitLoop = true; } else if (check(PrevPress)) { delay(150); - if (selected > 0) { - selected--; - if (selected < scrollOffset) scrollOffset = selected; + if (selectedIdx > 0) { + selectedIdx--; + if (selectedIdx < scrollOffset) scrollOffset = selectedIdx; } else { - selected = deviceCount - 1; - scrollOffset = std::max(0, (int)deviceCount - maxVisibleItems); + selectedIdx = deviceCount - 1; + scrollOffset = std::max(0, (int)deviceCount - maxVisibleDevices); } } else if (check(NextPress)) { delay(150); - if (selected < (int)deviceCount - 1) { - selected++; - if (selected >= scrollOffset + maxVisibleItems) scrollOffset = selected - maxVisibleItems + 1; + if (selectedIdx < (int)deviceCount - 1) { + selectedIdx++; + if (selectedIdx >= scrollOffset + maxVisibleDevices) + scrollOffset = selectedIdx - maxVisibleDevices + 1; } else { - selected = 0; + selectedIdx = 0; scrollOffset = 0; } } else if (check(SelPress)) { - delay(200); - return scannerData.deviceAddresses[selected]; + String selectedMAC = snapshot->addresses[selectedIdx]; + String selectedName = snapshot->names[selectedIdx]; + + selectedMAC.trim(); + selectedMAC.toUpperCase(); + + g_selectedDevice.address = selectedMAC; + g_selectedDevice.name = selectedName; + g_selectedDevice.rssi = snapshot->rssi[selectedIdx]; + g_selectedDevice.hasFastPair = snapshot->fastPair[selectedIdx]; + g_selectedDevice.hasHFP = snapshot->hfp[selectedIdx]; + g_selectedDevice.deviceType = snapshot->types[selectedIdx]; + + String returnMac = selectedMAC; + returnMac.trim(); + + // DO NOT clear scannerData here - keep it for potential reuse + return returnMac; } delay(50); } + + // DO NOT clear scannerData here - keep it for potential reuse return ""; } String selectMultipleTargetsFromScan(const char *title, std::vector &targets) { targets.clear(); - if (scannerData.size() == 0) { + + DeviceSnapshot* snapshot = scannerData.getSnapshot(); + if (!snapshot || snapshot->count == 0) { showErrorMessage("No devices found. Run scan first."); return ""; } - std::vector selected(scannerData.size(), false); + std::vector selected(snapshot->count, false); int currentIndex = 0, scrollOffset = 0; bool exitMenu = false; + size_t deviceCount = snapshot->count; int menuStartY = 60, menuItemHeight = 25; - size_t deviceCount = scannerData.size(); int maxVisibleItems = (tftHeight - menuStartY - 50) / menuItemHeight; if (maxVisibleItems > (int)deviceCount) maxVisibleItems = deviceCount; @@ -4187,7 +4561,7 @@ String selectMultipleTargetsFromScan(const char *title, std::vectornames[idx] + " | " + snapshot->addresses[idx]; if (display.length() > 25) display = display.substring(0, 22) + "..."; tft.print(display); } @@ -4201,8 +4575,10 @@ String selectMultipleTargetsFromScan(const char *title, std::vectoraddresses[currentIndex].c_str()), BLE_ADDR_PUBLIC )); } else { for (auto it = targets.begin(); it != targets.end(); ++it) { - if (it->toString() == scannerData.deviceAddresses[currentIndex].c_str()) { + if (it->toString() == snapshot->addresses[currentIndex].c_str()) { targets.erase(it); break; } @@ -4246,65 +4622,90 @@ String selectMultipleTargetsFromScan(const char *title, std::vector= '0' && c <= '9') || (c >= 'A' && c <= 'F') || c == ':') { - } else if (c == '|' || c == '(' || c == ' ') { - cleanAddr = cleanAddr.substring(0, i); - break; - } - } - - cleanAddr.trim(); - if (cleanAddr.length() < 17) { - for (int i = 0; i < addressInfo.length() - 17; i++) { - String substr = addressInfo.substring(i, i + 17); - bool valid = true; - for (int j = 0; j < 17; j++) { - char c = substr.charAt(j); - if (j % 3 == 2) { - if (c != ':') { - valid = false; - break; - } - } else { - if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'))) { - valid = false; - break; + if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F')) { + if (start == -1) start = i; + if (i - start + 1 >= 17) { + String possibleMac = cleanAddr.substring(start, start + 17); + bool valid = true; + for (int j = 0; j < 17; j++) { + if (j % 3 == 2) { + if (possibleMac.charAt(j) != ':') { + valid = false; + break; + } + } else { + char h = possibleMac.charAt(j); + if (!((h >= '0' && h <= '9') || (h >= 'A' && h <= 'F'))) { + valid = false; + break; + } } } + if (valid) { + return NimBLEAddress(std::string(possibleMac.c_str()), BLE_ADDR_PUBLIC); + } } - if (valid) { - cleanAddr = substr; - cleanAddr.toUpperCase(); - break; + } else if (c == ':') { + colonCount++; + } else { + if (start != -1 && colonCount < 5) { + start = -1; + colonCount = 0; } } } - - if (cleanAddr.length() < 17) { - Serial.println("[WARN] Invalid MAC address format: " + addressInfo); - return NimBLEAddress(std::string(""), BLE_ADDR_PUBLIC); + + for (int i = 0; i < addressInfo.length() - 17; i++) { + String substr = addressInfo.substring(i, i + 17); + bool valid = true; + for (int j = 0; j < 17; j++) { + char c = substr.charAt(j); + if (j % 3 == 2) { + if (c != ':') { + valid = false; + break; + } + } else { + if (!((c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f'))) { + valid = false; + break; + } + } + } + if (valid) { + substr.toUpperCase(); + return NimBLEAddress(std::string(substr.c_str()), BLE_ADDR_PUBLIC); + } } - - return NimBLEAddress(std::string(cleanAddr.c_str()), BLE_ADDR_PUBLIC); + + Serial.println("[WARN] Invalid MAC address format: " + addressInfo); + return NimBLEAddress(std::string(""), BLE_ADDR_PUBLIC); } //============================================================================= -// Attack Functions +// Attack Functions - Updated to use SelectedDevice //============================================================================= void runHFPVulnerabilityTest(NimBLEAddress target) { @@ -4532,7 +4933,7 @@ void runAdvertisingSpam(NimBLEAddress target) { } //============================================================================= -// Menu System - with optimized redraw to prevent flicker +// Menu System - Clear data ONLY at entry and exit //============================================================================= static bool welcomeShown = false; @@ -4540,27 +4941,32 @@ static bool welcomeShown = false; void showWelcomeScreen() { if (welcomeShown) return; - tft.fillScreen(TFT_GRAY); - tft.setTextSize(3); - tft.setTextColor(TFT_PURPLE, TFT_GRAY); - tft.setCursor((tftWidth - tft.textWidth("BRUCE")) / 2, 40); + tft.fillScreen(bruceConfig.bgColor); + tft.setTextSize(4); + tft.setTextColor(TFT_PURPLE, bruceConfig.bgColor); + tft.setCursor((tftWidth - tft.textWidth("BRUCE")) / 2, 35); tft.print("BRUCE"); - tft.setTextColor(TFT_BLUE, TFT_GRAY); + tft.setTextColor(TFT_BLUE, bruceConfig.bgColor); tft.setTextSize(2); - tft.setCursor((tftWidth - tft.textWidth("BLE SUITE v3.1")) / 2, 90); - tft.print("BLE SUITE v3.1"); + tft.setCursor((tftWidth - tft.textWidth("BLE SUITE")) / 2, 80); + tft.print("BLE SUITE"); - tft.setTextColor(TFT_GREEN, TFT_GRAY); - tft.setTextSize(1); - tft.setCursor((tftWidth - tft.textWidth("v3.1")) / 2, 130); + tft.setTextColor(TFT_LIGHTGREY, bruceConfig.bgColor); + tft.setTextSize(1.5); + tft.setCursor((tftWidth - tft.textWidth("v3.1")) / 2, 100); tft.print("v3.1"); - delay(1500); + delay(2000); welcomeShown = true; } void BleSuiteMenu() { + // Clear data when entering the menu + scannerData.clear(); + g_selectedDevice.address = ""; + g_selectedDevice.name = ""; + showWelcomeScreen(); const int MENU_ITEMS = 12; @@ -4590,8 +4996,8 @@ void BleSuiteMenu() { tft.setTextColor(TFT_WHITE, bruceConfig.bgColor); tft.setTextSize(2); - tft.setCursor((tftWidth - tft.textWidth("BLE SUITE v3.1")) / 2, 15); - tft.print("BLE SUITE v3.1"); + tft.setCursor((tftWidth - tft.textWidth("BLE SUITE")) / 2, 15); + tft.print("BLE SUITE"); tft.setTextSize(1); for (int i = 0; i < maxVisible && (scrollOffset + i) < MENU_ITEMS; i++) { @@ -4621,14 +5027,27 @@ void BleSuiteMenu() { } tft.setTextColor(TFT_GREEN, bruceConfig.bgColor); - tft.setCursor(20, tftHeight - 35); - tft.print("SEL: Select PREV/NEXT: Navigate ESC: Back"); + tft.setCursor(20, tftHeight - 30); + tft.print("SEL: Select PREV/NEXT: Navigate"); + tft.setCursor(20, tftHeight - 20); + tft.print("ESC: Back"); lastSelected = selected; lastScrollOffset = scrollOffset; } - if (check(EscPress)) return; + if (check(EscPress)) { + // Clear data when exiting the menu + if (g_pBLEScan) { + g_pBLEScan->stop(); + g_pBLEScan->clearResults(); + g_bleScanActive = false; + } + scannerData.clear(); + g_selectedDevice.address = ""; + g_selectedDevice.name = ""; + return; + } if (check(PrevPress)) { selected = (selected > 0) ? selected - 1 : MENU_ITEMS - 1; if (selected < scrollOffset) scrollOffset = selected; @@ -4644,6 +5063,7 @@ void BleSuiteMenu() { if (check(SelPress)) { if (selected == MENU_ITEMS - 1) { BLE_Sniffer(); + // Don't clear data - keep it for the menu } else { executeAttackWithTargetScan(selected); } @@ -4681,30 +5101,37 @@ void executeAttackWithTargetScan(int attackIndex) { NimBLEAddress target = parseAddress(targetInfo); if (!confirmAttack(target.toString().c_str())) return; - AutoCleanup cleanup([]() { BLEStateManager::deinitBLE(true); }); + SelectedDevice deviceInfo = g_selectedDevice; switch (attackIndex) { - case 0: runQuickTest(target); break; - case 1: runDeviceProfiling(target); break; - case 2: showFastPairSubMenu(target); break; - case 3: showHFPSubMenu(target); break; - case 4: showAudioSubMenu(target); break; - case 5: showHIDSubMenu(target); break; - case 6: showMemorySubMenu(target); break; - case 7: showDoSSubMenu(target); break; - case 8: showPayloadSubMenu(target); break; - case 9: showTestingSubMenu(target); break; - case 10: runUniversalAttack(target); break; + case 0: runQuickTest(target, deviceInfo); break; + case 1: runDeviceProfiling(target, deviceInfo); break; + case 2: showFastPairSubMenu(target, deviceInfo); break; + case 3: showHFPSubMenu(target, deviceInfo); break; + case 4: showAudioSubMenu(target, deviceInfo); break; + case 5: showHIDSubMenu(target, deviceInfo); break; + case 6: showMemorySubMenu(target, deviceInfo); break; + case 7: showDoSSubMenu(target, deviceInfo); break; + case 8: showPayloadSubMenu(target, deviceInfo); break; + case 9: showTestingSubMenu(target, deviceInfo); break; + case 10: runUniversalAttack(target, deviceInfo); break; } - cleanup.disable(); - showAttackProgress("Attack complete. Press any key to continue...", TFT_GREEN); while (!check(EscPress) && !check(SelPress) && !check(PrevPress) && !check(NextPress)) delay(50); + + // Clean up scan state but DON'T clear scannerData + if (g_pBLEScan) { + g_pBLEScan->stop(); + g_pBLEScan->clearResults(); + g_bleScanActive = false; + } + // Keep scannerData and g_selectedDevice for potential reuse + delay(100); } //============================================================================= -// Submenu Display - with text wrapping for long options +// Submenu Display //============================================================================= int showSubMenu(const char *title, const char *options[], int optionCount) { @@ -4713,6 +5140,7 @@ int showSubMenu(const char *title, const char *options[], int optionCount) { tft.setTextColor(TFT_WHITE, bruceConfig.bgColor); tft.setTextSize(2); + tft.setTextWrap(true, true); tft.setCursor((tftWidth - tft.textWidth(title)) / 2, 15); tft.print(title); tft.setTextSize(1); @@ -4723,14 +5151,25 @@ int showSubMenu(const char *title, const char *options[], int optionCount) { while (true) { if (selected != lastSelected || scrollOffset != lastScrollOffset) { - tft.fillRect(20, 60, tftWidth - 40, tftHeight - 100, bruceConfig.bgColor); + tft.fillRect(20, 60, tftWidth - 40, tftHeight - 115, bruceConfig.bgColor); for (int i = 0; i < maxVisible && (scrollOffset + i) < optionCount; i++) { int idx = scrollOffset + i; int yPos = 60 + (i * 25); + int availWidth = (tftWidth - 40) - 20; + String displayText = options[idx]; - if (displayText.length() > 28) { displayText = displayText.substring(0, 25) + "..."; } + + if (tft.textWidth(displayText.c_str()) > availWidth) { + String ellipsis = "..."; + int ellipsisWidth = tft.textWidth(ellipsis.c_str()); + while (displayText.length() > 0 && + tft.textWidth(displayText.c_str()) + ellipsisWidth > availWidth) { + displayText.remove(displayText.length() - 1); + } + displayText += ellipsis; + } if (idx == selected) { tft.fillRect(20, yPos, tftWidth - 40, 20, TFT_WHITE); @@ -4755,8 +5194,10 @@ int showSubMenu(const char *title, const char *options[], int optionCount) { } tft.setTextColor(TFT_GREEN, bruceConfig.bgColor); - tft.setCursor(20, tftHeight - 35); - tft.print("SEL: Select PREV/NEXT: Navigate ESC: Back"); + tft.setCursor(20, tftHeight - 30); + tft.print("SEL: Select PREV/NEXT: Navigate"); + tft.setCursor(20, tftHeight - 20); + tft.print("ESC: Back"); lastSelected = selected; lastScrollOffset = scrollOffset; @@ -4782,10 +5223,10 @@ int showSubMenu(const char *title, const char *options[], int optionCount) { } //============================================================================= -// Attack Submenus +// Attack Submenus - Updated to use SelectedDevice //============================================================================= -void showFastPairSubMenu(NimBLEAddress target) { +void showFastPairSubMenu(NimBLEAddress target, SelectedDevice deviceInfo) { const char *options[] = { "Quick Vulnerability Test", "Memory Corruption Attack", @@ -4869,7 +5310,7 @@ void showFastPairSubMenu(NimBLEAddress target) { } } -void showHFPSubMenu(NimBLEAddress target) { +void showHFPSubMenu(NimBLEAddress target, SelectedDevice deviceInfo) { const char *options[] = { "Test Vulnerability (CVE)", "Establish HFP Connection", "Full HFP Attack Chain", "HFP → HID Pivot" }; @@ -4887,7 +5328,7 @@ void showHFPSubMenu(NimBLEAddress target) { } } -void showAudioSubMenu(NimBLEAddress target) { +void showAudioSubMenu(NimBLEAddress target, SelectedDevice deviceInfo) { const char *options[] = { "AVRCP Media Control", "Audio Stack Crash", "Telephony Alert Test", "Run All Audio Tests" }; @@ -4926,7 +5367,7 @@ void showAudioSubMenu(NimBLEAddress target) { NimBLEDevice::deleteClient(pClient); } -void showHIDSubMenu(NimBLEAddress target) { +void showHIDSubMenu(NimBLEAddress target, SelectedDevice deviceInfo) { const char *options[] = { "Test HID Vulnerability", "Force HID Connection", @@ -4942,22 +5383,9 @@ void showHIDSubMenu(NimBLEAddress target) { HIDExploitEngine hid; HIDDuckyService ducky; - String deviceName = ""; - int rssi = -60; - if (xSemaphoreTake(scannerData.mutex, portMAX_DELAY)) { - for (size_t i = 0; i < scannerData.deviceAddresses.size(); i++) { - if (scannerData.deviceAddresses[i] == target.toString().c_str()) { - deviceName = scannerData.deviceNames[i]; - rssi = scannerData.deviceRssi[i]; - break; - } - } - xSemaphoreGive(scannerData.mutex); - } - switch (choice) { case 0: hid.testHIDVulnerability(target); break; - case 1: hid.forceHIDConnection(target, deviceName, rssi); break; + case 1: hid.forceHIDConnection(target, deviceInfo.name, deviceInfo.rssi); break; case 2: HIDAttackServiceClass().injectKeystrokes(target); break; case 3: { String script = getScriptFromUser(); @@ -4965,7 +5393,7 @@ void showHIDSubMenu(NimBLEAddress target) { break; } case 4: { - HIDDeviceProfile profile = hid.analyzeHIDDevice(target, deviceName, rssi); + HIDDeviceProfile profile = hid.analyzeHIDDevice(target, deviceInfo.name, deviceInfo.rssi); if (profile.isAppleDevice) hid.tryAppleMagicSpoof(target, profile); else if (profile.isWindowsDevice) hid.tryWindowsHIDBypass(target, profile); else if (profile.isAndroidDevice) hid.tryAndroidJustWorks(target, profile); @@ -4973,13 +5401,13 @@ void showHIDSubMenu(NimBLEAddress target) { } case 5: hid.testHIDVulnerability(target); - hid.forceHIDConnection(target, deviceName, rssi); + hid.forceHIDConnection(target, deviceInfo.name, deviceInfo.rssi); HIDAttackServiceClass().injectKeystrokes(target); break; } } -void showMemorySubMenu(NimBLEAddress target) { +void showMemorySubMenu(NimBLEAddress target, SelectedDevice deviceInfo) { const char *options[] = { "FastPair Memory Corruption", "FastPair State Confusion", @@ -5042,7 +5470,7 @@ void showMemorySubMenu(NimBLEAddress target) { NimBLEDevice::deleteClient(pClient); } -void showDoSSubMenu(NimBLEAddress target) { +void showDoSSubMenu(NimBLEAddress target, SelectedDevice deviceInfo) { const char *options[] = { "Connection Flood", "Advertising Spam", "Jam & Connect (NRF24)", "Protocol Fuzzer" }; @@ -5061,7 +5489,7 @@ void showDoSSubMenu(NimBLEAddress target) { } } -void showPayloadSubMenu(NimBLEAddress target) { +void showPayloadSubMenu(NimBLEAddress target, SelectedDevice deviceInfo) { const char *options[] = {"DuckyScript Injection", "PIN Brute Force", "Auth Bypass Suite"}; int choice = showSubMenu("Payload Delivery", options, 3); @@ -5078,7 +5506,7 @@ void showPayloadSubMenu(NimBLEAddress target) { } } -void showTestingSubMenu(NimBLEAddress target) { +void showTestingSubMenu(NimBLEAddress target, SelectedDevice deviceInfo) { const char *options[] = { "Write Access Test", "Audio Control Test", "Protocol Fuzzer", "HID Service Test" }; @@ -5095,39 +5523,24 @@ void showTestingSubMenu(NimBLEAddress target) { } //============================================================================= -// Attack Functions +// Attack Functions - Updated to use SelectedDevice //============================================================================= -void runUniversalAttack(NimBLEAddress target) { +void runUniversalAttack(NimBLEAddress target, SelectedDevice deviceInfo) { AutoCleanup cleanup([]() { BLEStateManager::deinitBLE(true); }); if (!confirmAttack("Execute universal attack chain (HFP + HID + FastPair)?")) return; - String deviceName = ""; - bool hasHFP = false, hasFastPair = false; - - if (xSemaphoreTake(scannerData.mutex, portMAX_DELAY)) { - for (size_t i = 0; i < scannerData.deviceAddresses.size(); i++) { - if (scannerData.deviceAddresses[i] == target.toString().c_str()) { - deviceName = scannerData.deviceNames[i]; - hasHFP = scannerData.deviceHasHFP[i]; - hasFastPair = scannerData.deviceFastPair[i]; - break; - } - } - xSemaphoreGive(scannerData.mutex); - } - std::vector lines = { "UNIVERSAL ATTACK CHAIN", - "Device: " + deviceName, - "HFP: " + String(hasHFP ? "YES" : "NO"), - "FastPair: " + String(hasFastPair ? "YES" : "NO") + "Device: " + deviceInfo.name, + "HFP: " + String(deviceInfo.hasHFP ? "YES" : "NO"), + "FastPair: " + String(deviceInfo.hasFastPair ? "YES" : "NO") }; bool hfpSuccess = false, fpSuccess = false, hidSuccess = false; - if (hasHFP) { + if (deviceInfo.hasHFP) { showAttackProgress("Phase 1: Testing HFP vulnerability...", TFT_CYAN); HFPExploitEngine hfp; hfpSuccess = hfp.executeHFPAttackChain(target); @@ -5141,7 +5554,7 @@ void runUniversalAttack(NimBLEAddress target) { } } - if (hasFastPair && (!hfpSuccess || !hidSuccess)) { + if (deviceInfo.hasFastPair && (!hfpSuccess || !hidSuccess)) { showAttackProgress("Phase 3: Testing FastPair vulnerability...", TFT_BLUE); FastPairExploitEngine fpEngine; fpSuccess = fpEngine.testVulnerability(target); @@ -5159,25 +5572,14 @@ void runUniversalAttack(NimBLEAddress target) { } } -void runQuickTest(NimBLEAddress target) { +void runQuickTest(NimBLEAddress target, SelectedDevice deviceInfo) { AutoCleanup cleanup([]() { BLEStateManager::deinitBLE(true); }); showAttackProgress("Quick testing (HFP + FastPair)...", TFT_WHITE); - bool hasHFP = false; - if (xSemaphoreTake(scannerData.mutex, portMAX_DELAY)) { - for (size_t i = 0; i < scannerData.deviceAddresses.size(); i++) { - if (scannerData.deviceAddresses[i] == target.toString().c_str()) { - hasHFP = scannerData.deviceHasHFP[i]; - break; - } - } - xSemaphoreGive(scannerData.mutex); - } - std::vector results; - if (hasHFP) { + if (deviceInfo.hasHFP) { HFPExploitEngine hfp; bool hfpVulnerable = hfp.testCVE202536911(target); results.push_back("HFP (CVE-2025-36911): " + String(hfpVulnerable ? "VULNERABLE" : "SAFE")); @@ -5198,7 +5600,7 @@ void runQuickTest(NimBLEAddress target) { cleanup.disable(); - if (hasHFP && results[0].indexOf("VULNERABLE") != -1) { + if (deviceInfo.hasHFP && results[0].indexOf("VULNERABLE") != -1) { lines.push_back("Try HFP-based attacks first!"); showDeviceInfoScreen("VULNERABLE DEVICE", lines, TFT_ORANGE, TFT_BLACK); } else if (fpVulnerable) { @@ -5208,7 +5610,7 @@ void runQuickTest(NimBLEAddress target) { } } -void runDeviceProfiling(NimBLEAddress target) { +void runDeviceProfiling(NimBLEAddress target, SelectedDevice deviceInfo) { AutoCleanup cleanup([]() { BLEStateManager::deinitBLE(true); }); if (!confirmAttack("Profile device services?")) return; @@ -5464,9 +5866,11 @@ void runAudioControlTest(NimBLEAddress target) { tft.print(displayName); } - tft.setTextColor(TFT_WHITE, bruceConfig.bgColor); - tft.setCursor(20, tftHeight - 35); - tft.print("SEL: Test PREV/NEXT: Navigate ESC: Back"); + tft.setTextColor(TFT_GREEN, bruceConfig.bgColor); + tft.setCursor(20, tftHeight - 30); + tft.print("SEL: Select PREV/NEXT: Navigate"); + tft.setCursor(20, tftHeight - 20); + tft.print("ESC: Back"); lastSelected = selectedTest; } @@ -5586,7 +5990,7 @@ void runHFPHIDPivotAttack(NimBLEAddress target) { } //============================================================================= -// UI Helpers - with text wrapping for long messages +// UI Helpers //============================================================================= void showAttackProgress(const char *message, uint16_t color) { @@ -5595,8 +5999,8 @@ void showAttackProgress(const char *message, uint16_t color) { tft.setTextColor(TFT_WHITE, bruceConfig.bgColor); tft.setTextSize(2); - tft.setCursor((tftWidth - tft.textWidth("BLE SUITE v3.1")) / 2, 15); - tft.print("BLE SUITE v3.1"); + tft.setCursor((tftWidth - tft.textWidth("BLE SUITE")) / 2, 15); + tft.print("BLE SUITE"); tft.setTextSize(1); tft.setTextColor(color, bruceConfig.bgColor); @@ -5729,8 +6133,8 @@ bool confirmAttack(const char *targetName) { tft.setCursor(20, 90); tft.println("FastPair buffer overflow exploit"); - tft.setTextColor(TFT_WHITE, bruceConfig.bgColor); - tft.setCursor(20, tftHeight - 35); + tft.setTextColor(TFT_GREEN, bruceConfig.bgColor); + tft.setCursor(20, tftHeight - 30); tft.print("SEL: Yes NEXT: No ESC: Cancel"); while (true) { @@ -5846,7 +6250,7 @@ int8_t showAdaptiveMessage( if (yPos > 140) break; } - tft.setTextColor(TFT_WHITE, bruceConfig.bgColor); + tft.setTextColor(TFT_BLACK, bruceConfig.bgColor); tft.setCursor(20, tftHeight - 35); if (buttonCount == 0) { @@ -6106,7 +6510,7 @@ void showDeviceInfoScreen( yPos = lineY; } - tft.setTextColor(TFT_WHITE, bgColor); + tft.setTextColor(TFT_BLACK, bgColor); tft.setCursor(20, tftHeight - 35); tft.print("Press any key to continue..."); diff --git a/src/modules/ble/BLE_Suite.h b/src/modules/ble/BLE_Suite.h index ef3452a00..a9d5e7002 100644 --- a/src/modules/ble/BLE_Suite.h +++ b/src/modules/ble/BLE_Suite.h @@ -1,52 +1,18 @@ #ifndef BLE_SUITE_H #define BLE_SUITE_H #if !defined(LITE_VERSION) -#include -#include "fastpair_crypto.h" + +// Include display header FIRST so TFT color definitions are available +#include "core/display.h" + #include "HFP_Exploit.h" +#include "fastpair_crypto.h" +#include #include -#include #include #include #include - -#if __has_include() -#define NIMBLE_V2_PLUS 1 -#endif - -#ifndef TFT_WHITE -#define TFT_WHITE 0xFFFF -#endif -#ifndef TFT_BLACK -#define TFT_BLACK 0x0000 -#endif -#ifndef TFT_RED -#define TFT_RED 0xF800 -#endif -#ifndef TFT_GREEN -#define TFT_GREEN 0x07E0 -#endif -#ifndef TFT_BLUE -#define TFT_BLUE 0x001F -#endif -#ifndef TFT_YELLOW -#define TFT_YELLOW 0xFFE0 -#endif -#ifndef TFT_CYAN -#define TFT_CYAN 0x07FF -#endif -#ifndef TFT_MAGENTA -#define TFT_MAGENTA 0xF81F -#endif -#ifndef TFT_ORANGE -#define TFT_ORANGE 0xFDA0 -#endif -#ifndef TFT_GRAY -#define TFT_GRAY 0x8410 -#endif -#ifndef TFT_DARKGREEN -#define TFT_DARKGREEN 0x03E0 -#endif +#include extern volatile int tftWidth; extern volatile int tftHeight; @@ -57,6 +23,43 @@ extern BruceConfig bruceConfig; bool check(int key); +//============================================================================= +// NimBLE Version Detection - Must match ble_common.h +//============================================================================= + +// Detect NimBLE 2.x by checking for features only available in v2+ +#if defined(NIMBLE_VERSION) + #if NIMBLE_VERSION >= 20000 + #define NIMBLE_V2_PLUS 1 + #endif +#elif defined(NIMBLE_CPP_VERSION) && NIMBLE_CPP_VERSION >= 2 + #define NIMBLE_V2_PLUS 1 +#elif defined(NIMBLE_VERSION_MAJOR) && NIMBLE_VERSION_MAJOR >= 2 + #define NIMBLE_V2_PLUS 1 +#elif defined(NIMBLE_VERSION_MAJOR) && NIMBLE_VERSION_MAJOR == 1 && NIMBLE_VERSION_MINOR >= 5 + #define NIMBLE_V2_PLUS 1 +#elif __has_include() + #define NIMBLE_V2_PLUS 1 +#endif + +// If none of the above matched, default to v1 behavior (safe fallback) +#ifndef NIMBLE_V2_PLUS + #define NIMBLE_V2_PLUS 0 +#endif + +//============================================================================= +// BLE Scan Constants +//============================================================================= + +#define ACTIVE_SCAN_TIME 8 +#define PASSIVE_SCAN_TIME 8 +#define SCAN_INT 100 +#define SCAN_WINDOW 99 + +//============================================================================= +// Enums +//============================================================================= + enum { BLE_ESC_PRESS = 0, BLE_SEL_PRESS = 1, @@ -80,6 +83,73 @@ enum FastPairExploitType { FP_EXPLOIT_ALL }; +//============================================================================= +// DeviceInfo and DeviceSnapshot structures +//============================================================================= + +struct DeviceInfo { + String address; + String name; + int rssi; + bool hasFastPair; + bool hasHFP; + uint8_t deviceType; +}; + +struct DeviceSnapshot { + uint32_t version; + uint32_t count; + uint32_t timestamp; + std::vector names; + std::vector addresses; + std::vector rssi; + std::vector fastPair; + std::vector hfp; + std::vector types; + + DeviceSnapshot() : version(0), count(0), timestamp(0) {} +}; + +//============================================================================= +// SelectedDevice for passing device info to attacks +//============================================================================= + +struct SelectedDevice { + String address; + String name; + int rssi; + bool hasFastPair; + bool hasHFP; + uint8_t deviceType; +}; + +//============================================================================= +// ScannerData with snapshot methods +//============================================================================= + +struct ScannerData { + std::vector deviceNames; + std::vector deviceAddresses; + std::vector deviceRssi; + std::vector deviceFastPair; + std::vector deviceHasHFP; + std::vector deviceTypes; + SemaphoreHandle_t mutex; + int foundCount; + + uint32_t dataVersion; + DeviceSnapshot* snapshotCache; + uint32_t cacheTimestamp; + + ScannerData(); + ~ScannerData(); + void addDevice(const String& name, const String& address, int rssi, bool fastPair, bool hasHFP, uint8_t type); + void clear(); + size_t size(); + DeviceSnapshot* getSnapshot(); + bool getDeviceInfo(int index, DeviceInfo &info); +}; + struct CharacteristicInfo { String uuid; bool canRead; @@ -133,22 +203,9 @@ struct DuckyCommand { int delay_ms; }; -struct ScannerData { - std::vector deviceNames; - std::vector deviceAddresses; - std::vector deviceRssi; - std::vector deviceFastPair; - std::vector deviceHasHFP; - std::vector deviceTypes; - SemaphoreHandle_t mutex; - int foundCount; - - ScannerData(); - ~ScannerData(); - void addDevice(const String& name, const String& address, int rssi, bool fastPair, bool hasHFP, uint8_t type); - void clear(); - size_t size(); -}; +//============================================================================= +// AutoCleanup Class +//============================================================================= class AutoCleanup { private: @@ -162,6 +219,10 @@ class AutoCleanup { void enable(); }; +//============================================================================= +// BLEStateManager Class +//============================================================================= + class BLEStateManager { private: static bool bleInitialized; @@ -179,6 +240,10 @@ class BLEStateManager { static size_t getActiveClientCount(); }; +//============================================================================= +// BLEAttackManager Class +//============================================================================= + class BLEAttackManager { public: void prepareForConnection(); @@ -187,7 +252,10 @@ class BLEAttackManager { DeviceProfile profileDevice(NimBLEAddress target); }; -// FastPair structs +//============================================================================= +// FastPair Structures and Functions +//============================================================================= + struct FastPairDeviceInfo { NimBLEAddress address; String name; @@ -220,6 +288,10 @@ enum FastPairVersion { FastPairVersion detectFastPairVersion(NimBLEAddress target); +//============================================================================= +// FastPairExploitEngine Class +//============================================================================= + class FastPairExploitEngine { public: std::vector scanForFastPairDevices(int duration); @@ -227,12 +299,10 @@ class FastPairExploitEngine { void spamFastPairPopups(FastPairPopupType popupType, int count); bool testVulnerability(NimBLEAddress target); - // v3.1: Smart FastPair attack with Samsung detection bool smartExploit(NimBLEAddress target); bool exploitSamsungFastPair(NimBLEAddress target); bool exploitGoogleFastPair(NimBLEAddress target); - // Public exploit methods bool executeMemoryCorruption(NimBLERemoteCharacteristic* pChar); bool executeStateConfusion(NimBLERemoteCharacteristic* pChar); bool executeCryptoOverflow(NimBLERemoteCharacteristic* pChar); @@ -258,6 +328,10 @@ class FastPairExploitEngine { void generateRandomMac(uint8_t* mac); }; +//============================================================================= +// HIDExploitEngine Class +//============================================================================= + class HIDExploitEngine { public: HIDDeviceProfile analyzeHIDDevice(NimBLEAddress target, const String& name, int rssi); @@ -276,6 +350,10 @@ class HIDExploitEngine { bool testHIDVulnerability(NimBLEAddress target); }; +//============================================================================= +// WhisperPairExploit Class +//============================================================================= + class WhisperPairExploit { public: WhisperPairExploit(); @@ -293,6 +371,10 @@ class WhisperPairExploit { bool executeAdvanced(NimBLEAddress target, int attackType); }; +//============================================================================= +// AudioAttackService Class +//============================================================================= + class AudioAttackService { public: bool findAndAttackAudioServices(NimBLEClient* pClient); @@ -304,6 +386,10 @@ class AudioAttackService { bool crashAudioStack(NimBLEAddress target); }; +//============================================================================= +// DuckyScriptEngine Class +//============================================================================= + class DuckyScriptEngine { public: struct HIDKeycode { @@ -326,6 +412,10 @@ class DuckyScriptEngine { bool scriptLoaded; }; +//============================================================================= +// HIDDuckyService Class +//============================================================================= + class HIDDuckyService { public: HIDDuckyService(); @@ -347,6 +437,10 @@ class HIDDuckyService { bool sendGUIKey(NimBLERemoteCharacteristic* pChar, char key); }; +//============================================================================= +// AuthBypassEngine Class +//============================================================================= + class AuthBypassEngine { private: struct PairedDevice { @@ -366,6 +460,10 @@ class AuthBypassEngine { bool exploitAuthBypass(NimBLEAddress target); }; +//============================================================================= +// MultiConnectionAttack Class +//============================================================================= + class MultiConnectionAttack { public: MultiConnectionAttack(); @@ -384,6 +482,10 @@ class MultiConnectionAttack { std::vector activeConnections; }; +//============================================================================= +// VulnerabilityScanner Class +//============================================================================= + class VulnerabilityScanner { private: struct VulnCheck { @@ -401,6 +503,10 @@ class VulnerabilityScanner { std::vector getVulnerabilities(); }; +//============================================================================= +// Attack Service Classes +//============================================================================= + class HIDAttackServiceClass { public: bool injectKeystrokes(NimBLEAddress target); @@ -418,6 +524,10 @@ class DoSAttackServiceClass { bool advertisingSpam(NimBLEAddress target); }; +//============================================================================= +// Debug Memory Macros +//============================================================================= + #ifdef DEBUG_MEMORY class HeapMonitor { public: @@ -445,6 +555,10 @@ class HeapMonitor { #define MEM_CHECK() #endif +//============================================================================= +// Function Declarations +//============================================================================= + void cleanupBLEStack(); NimBLEClient* attemptConnectionWithStrategies(NimBLEAddress target, String& connectionMethod); @@ -460,8 +574,32 @@ void runDuckyScriptAttack(NimBLEAddress target); void runPINBruteForce(NimBLEAddress target); void runConnectionFlood(NimBLEAddress target); void runAdvertisingSpam(NimBLEAddress target); -void runQuickTest(NimBLEAddress target); -void runDeviceProfiling(NimBLEAddress target); + +//============================================================================= +// Attack functions with SelectedDevice parameter +//============================================================================= + +void runQuickTest(NimBLEAddress target, SelectedDevice deviceInfo); +void runDeviceProfiling(NimBLEAddress target, SelectedDevice deviceInfo); +void runUniversalAttack(NimBLEAddress target, SelectedDevice deviceInfo); + +//============================================================================= +// Submenu functions with SelectedDevice parameter +//============================================================================= + +void showFastPairSubMenu(NimBLEAddress target, SelectedDevice deviceInfo); +void showHFPSubMenu(NimBLEAddress target, SelectedDevice deviceInfo); +void showAudioSubMenu(NimBLEAddress target, SelectedDevice deviceInfo); +void showHIDSubMenu(NimBLEAddress target, SelectedDevice deviceInfo); +void showMemorySubMenu(NimBLEAddress target, SelectedDevice deviceInfo); +void showDoSSubMenu(NimBLEAddress target, SelectedDevice deviceInfo); +void showPayloadSubMenu(NimBLEAddress target, SelectedDevice deviceInfo); +void showTestingSubMenu(NimBLEAddress target, SelectedDevice deviceInfo); + +//============================================================================= +// Original function declarations +//============================================================================= + void runWriteAccessTest(NimBLEAddress target); void runProtocolFuzzer(NimBLEAddress target); void runJamConnectAttack(NimBLEAddress target); @@ -500,11 +638,13 @@ void runFastPairCryptoOverflow(NimBLEAddress target); void runFastPairPopupSpam(NimBLEAddress target, FastPairPopupType type); void runFastPairAllExploits(NimBLEAddress target); void runFastPairHIDChain(NimBLEAddress target); -void runUniversalAttack(NimBLEAddress target); String selectFileFromSD(); bool loadScriptFromSD(const String &filename); -// Forward declarations for submenu functions +// BLE Sniffer +void BLE_Sniffer(); + +// Forward declarations for old submenu functions (for compatibility) void showFastPairSubMenu(NimBLEAddress target); void showHFPSubMenu(NimBLEAddress target); void showAudioSubMenu(NimBLEAddress target); diff --git a/src/modules/ble/ble_common.cpp b/src/modules/ble/ble_common.cpp index a2f580476..d529eb3bc 100644 --- a/src/modules/ble/ble_common.cpp +++ b/src/modules/ble/ble_common.cpp @@ -9,6 +9,7 @@ #if !defined(LITE_VERSION) #include "BLE_Suite.h" #endif + #define SERVICE_UUID "1bc68b2a-f3e3-11e9-81b4-2a2ae2dbcce4" #define CHARACTERISTIC_RX_UUID "1bc68da0-f3e3-11e9-81b4-2a2ae2dbcce4" #define CHARACTERISTIC_TX_UUID "1bc68efe-f3e3-11e9-81b4-2a2ae2dbcce4" @@ -20,7 +21,7 @@ bool bleNotifyRetry(NimBLECharacteristic *chr, const uint8_t *value, size_t leng if (chr == nullptr) return false; if (chr->notify(value, length)) return true; for (uint8_t i = 0; i < retries; i++) { - vTaskDelay(1); // let the host drain the pool MSYS and retry + vTaskDelay(1); if (chr->notify(value, length)) return true; } return false; @@ -30,16 +31,12 @@ bool bleNotifyRetry(NimBLECharacteristic *chr, uint8_t retries) { if (chr == nullptr) return false; if (chr->notify()) return true; for (uint8_t i = 0; i < retries; i++) { - vTaskDelay(1); // let the host drain the pool MSYS and retry + vTaskDelay(1); if (chr->notify()) return true; } return false; } -#if __has_include() -#define NIMBLE_V2_PLUS 1 -#endif - #define ENDIAN_CHANGE_U16(x) ((((x) & 0xFF00) >> 8) + (((x) & 0xFF) << 8)) BLEServer *pServer = NULL; @@ -77,55 +74,49 @@ void ble_info(const String &name, const String &address, const String &signal) { delay(300); while (!check(SelPress)) { - while (!check(SelPress)) { vTaskDelay(pdMS_TO_TICKS(1)); } // timerless debounce + while (!check(SelPress)) { yield(); } returnToMenu = true; break; } } -#ifdef NIMBLE_V2_PLUS -class AdvertisedDeviceCallbacks : public NimBLEScanCallbacks { + +//============================================================================= +// NimBLE Callbacks - Version-specific with proper lifetime management +//============================================================================= + +#if NIMBLE_V2_PLUS +class AdvertisedDeviceCallbacks : public NimBLEScanCallbacks {}; #else class AdvertisedDeviceCallbacks : public NimBLEAdvertisedDeviceCallbacks { -#endif - void onResult(NimBLEAdvertisedDevice *advertisedDevice) { - String bt_title; - String bt_name; - String bt_address; - String bt_signal; - - bt_name = advertisedDevice->getName().c_str(); - bt_title = advertisedDevice->getName().c_str(); - bt_address = advertisedDevice->getAddress().toString().c_str(); - bt_signal = String(advertisedDevice->getRSSI()); - // Serial.println("\n\nAddress - " + bt_address + "Name-"+ bt_name +"\n\n"); - if (bt_title.isEmpty()) bt_title = bt_address; - if (bt_name.isEmpty()) bt_name = ""; - // If BT name is empty, set NONAME - if (options.size() < 250) - options.emplace_back(bt_title.c_str(), [=]() { ble_info(bt_name, bt_address, bt_signal); }); - else { - Serial.println("Memory low, stopping BLE scan..."); - pBLEScan->stop(); - } - } +public: + void onResult(NimBLEAdvertisedDevice *advertisedDevice) override {} }; +#endif + +static AdvertisedDeviceCallbacks g_scanCallbacks; static bool is_ble_inited = false; void stopBLEStack() { - if (pBLEScan) pBLEScan->stop(); + if (pBLEScan) { + pBLEScan->stop(); + pBLEScan->clearResults(); + // Don't delete pBLEScan - it's owned by BLEDevice + pBLEScan = nullptr; + } + if (is_ble_inited) { #if !defined(LITE_VERSION) - if (BLEStateManager::isBLEActive() || BLEStateManager::getActiveClientCount() > 0) { - BLEStateManager::deinitBLE(true); - } else + if (BLEStateManager::isBLEActive() || BLEStateManager::getActiveClientCount() > 0) { + BLEStateManager::deinitBLE(true); + } else #endif - if (BLEDevice::getScan() != nullptr || BLEDevice::getAdvertising() != nullptr || - BLEDevice::getServer() != nullptr || BLEConnected || is_ble_inited) { - BLEDevice::deinit(); + if (BLEDevice::getScan() != nullptr || BLEDevice::getAdvertising() != nullptr || + BLEDevice::getServer() != nullptr || BLEConnected) { + BLEDevice::deinit(); + } } - pBLEScan = nullptr; pServer = nullptr; pService = nullptr; pTxCharacteristic = nullptr; @@ -155,32 +146,37 @@ bool ble_scan_setup() { } RAM_LOG("ble-scan pre-init"); - if (!radioHasMemForBle()) { - displayError("Low RAM: free WiFi/SD first", true); - returnToMenu = true; - return false; + + if (!is_ble_inited) { + if (!radioHasMemForBle()) { + displayError("Low RAM: free WiFi/SD first", true); + returnToMenu = true; + return false; + } + // Use a minimal name to save RAM + BLEDevice::init(""); + is_ble_inited = true; } - BLEDevice::init(""); + RAM_LOG("ble-scan post-init"); pBLEScan = BLEDevice::getScan(); -#ifdef NIMBLE_V2_PLUS - pBLEScan->setScanCallbacks(new NimBLEScanCallbacks()); + if (!pBLEScan) { + displayError("Failed to get scan object", true); + return false; + } + +#if NIMBLE_V2_PLUS + pBLEScan->setScanCallbacks(&g_scanCallbacks); #else - pBLEScan->setAdvertisedDeviceCallbacks(new AdvertisedDeviceCallbacks()); + pBLEScan->setAdvertisedDeviceCallbacks(&g_scanCallbacks); #endif - // Active scan uses more power, but get results faster pBLEScan->setActiveScan(true); pBLEScan->setInterval(SCAN_INT); - // Less or equal setInterval value pBLEScan->setWindow(SCAN_WINDOW); + pBLEScan->setDuplicateFilter(false); - // Bluetooth MAC Address -#ifdef NIMBLE_V2_PLUS - esp_read_mac(sta_mac, ESP_MAC_BT); -#else esp_read_mac(sta_mac, ESP_MAC_BT); -#endif sprintf( strID, @@ -200,80 +196,164 @@ void ble_scan() { displayTextLine("Scanning.."); options = {}; + options.reserve(MAX_DISPLAY_DEVICES); + bool bleWasActiveBefore = BLEConnected || (BLEDevice::getServer() != nullptr); #if !defined(LITE_VERSION) - bleWasActiveBefore = - bleWasActiveBefore || BLEStateManager::isBLEActive() || BLEStateManager::getActiveClientCount() > 0; + bleWasActiveBefore = bleWasActiveBefore || BLEStateManager::isBLEActive() || BLEStateManager::getActiveClientCount() > 0; #endif - if (!ble_scan_setup() || pBLEScan == nullptr) return; -#ifdef NIMBLE_V2_PLUS - BLEScanResults foundDevices = pBLEScan->getResults(scanTime * 1000, false); - for (int i = 0; i < foundDevices.getCount(); i++) { - const NimBLEAdvertisedDevice *advertisedDevice = foundDevices.getDevice(i); - String bt_title; - String bt_name; - String bt_address; - String bt_signal; - - bt_name = advertisedDevice->getName().c_str(); - bt_title = advertisedDevice->getName().c_str(); - bt_address = advertisedDevice->getAddress().toString().c_str(); - bt_signal = String(advertisedDevice->getRSSI()); - // Serial.println("\n\nAddress - " + bt_address + "Name-"+ bt_name +"\n\n"); - if (bt_title.isEmpty()) bt_title = bt_address; - if (bt_name.isEmpty()) bt_name = ""; - // If BT name is empty, set NONAME - if (options.size() < 250) - options.emplace_back(bt_title.c_str(), [=]() { ble_info(bt_name, bt_address, bt_signal); }); - else { - Serial.println("Memory low, stopping BLE scan..."); - pBLEScan->stop(); - } + if (!ble_scan_setup() || pBLEScan == nullptr) { + displayError("Failed to init BLE scan"); + return; } + + // Clear previous results before scanning + pBLEScan->clearResults(); + + // Use a try-catch block to handle potential exceptions + try { +#if NIMBLE_V2_PLUS + BLEScanResults foundDevices = pBLEScan->getResults(scanTime * 1000, false); #else - BLEScanResults foundDevices = pBLEScan->start(scanTime, false); + // NimBLE 1.x: start() returns results directly, time in seconds + BLEScanResults foundDevices = pBLEScan->start(scanTime, false); #endif - addOptionToMainMenu(); - - loopOptions(options); - options.clear(); + int deviceCount = foundDevices.getCount(); + int processedCount = 0; + + // Cap the number of devices to prevent memory issues + int maxToProcess = min(deviceCount, MAX_DISPLAY_DEVICES); + + for (int i = 0; i < maxToProcess && processedCount < MAX_DISPLAY_DEVICES; i++) { +#if NIMBLE_V2_PLUS + const NimBLEAdvertisedDevice *advertisedDevice = foundDevices.getDevice(i); +#else + NimBLEAdvertisedDevice *advertisedDevice = foundDevices.getDevice(i); +#endif + if (!advertisedDevice) continue; + + String bt_title; + String bt_name; + String bt_address; + String bt_signal; + + bt_name = advertisedDevice->getName().c_str(); + bt_address = advertisedDevice->getAddress().toString().c_str(); + bt_signal = String(advertisedDevice->getRSSI()); + + if (bt_name.isEmpty()) bt_name = ""; + bt_title = bt_name; + if (bt_title.isEmpty()) bt_title = bt_address; + + if (options.size() < MAX_DISPLAY_DEVICES) { + options.emplace_back(bt_title.c_str(), [=]() { + ble_info(bt_name, bt_address, bt_signal); + }); + processedCount++; + } + } - // Delete results fromBLEScan buffer to release memory - pBLEScan->clearResults(); - pBLEScan->stop(); - if (!bleWasActiveBefore) { stopBLEStack(); } + // Show "and more" if we hit the limit + if (options.size() >= MAX_DISPLAY_DEVICES) { + options.emplace_back("... and more devices", nullptr); + } + } catch (...) { + displayError("BLE scan error"); + pBLEScan->clearResults(); + return; + } + + // Stop scan + if (pBLEScan) { + pBLEScan->stop(); + // Don't clear results here - we need them for display + } + + // Only stop BLE if it wasn't active before and we're done with it + if (!bleWasActiveBefore) { +#if !defined(LITE_VERSION) + if (!BLEStateManager::isBLEActive()) { + stopBLEStack(); + } +#else + stopBLEStack(); +#endif + } + + if (!options.empty()) { + addOptionToMainMenu(); + loopOptions(options); + options.clear(); + } else { + displayError("No devices found"); + delay(1000); + } } bool initBLEServer() { uint64_t chipid = ESP.getEfuseMac(); String blename = "Bruce-" + String((uint8_t)(chipid >> 32), HEX); - BLEDevice::init(blename.c_str()); - // BLEDevice::setPower(ESP_PWR_LVL_N12); + if (!is_ble_inited) { + BLEDevice::init(blename.c_str()); + is_ble_inited = true; + } + pServer = BLEDevice::createServer(); + if (!pServer) { + displayError("Failed to create BLE server"); + return false; + } pServer->setCallbacks(new MyServerCallbacks()); pService = pServer->createService(SERVICE_UUID); + if (!pService) { + displayError("Failed to create BLE service"); + return false; + } + pTxCharacteristic = pService->createCharacteristic(CHARACTERISTIC_RX_UUID, NIMBLE_PROPERTY::NOTIFY); + if (!pTxCharacteristic) { + displayError("Failed to create TX characteristic"); + return false; + } pTxCharacteristic->addDescriptor(new NimBLE2904()); - BLECharacteristic *pRxCharacteristic = pService->createCharacteristic( + pRxCharacteristic = pService->createCharacteristic( CHARACTERISTIC_TX_UUID, NIMBLE_PROPERTY::WRITE | NIMBLE_PROPERTY::WRITE_NR ); + if (!pRxCharacteristic) { + displayError("Failed to create RX characteristic"); + return false; + } pRxCharacteristic->setCallbacks(new MyCallbacks()); +#if NIMBLE_V2_PLUS + // NimBLE 2.x: Services start automatically when server starts + // No need to call pService->start() +#else + // NimBLE 1.x: Need to call pService->start() + pService->start(); +#endif + return true; } void disPlayBLESend() { uint8_t senddata[2] = {0}; tft.fillScreen(bruceConfig.bgColor); - drawMainBorder(); // Moved up to avoid drawing screen issues + drawMainBorder(); tft.setTextSize(1); - pService->start(); + if (!pServer) { + if (!initBLEServer()) { + displayError("Failed to init BLE server"); + return; + } + } + pServer->getAdvertising()->start(); uint64_t chipid = ESP.getEfuseMac(); @@ -291,7 +371,6 @@ void disPlayBLESend() { tft.setTextColor(bruceConfig.priColor, bruceConfig.bgColor); tft.setTextSize(FM); tft.setCursor(12, 50); - // tft.printf("BLE connect!\n"); tft.printf("BLE Send\n"); tft.setTextSize(FM); } @@ -337,28 +416,28 @@ void disPlayBLESend() { } tft.setTextColor(TFT_WHITE); - pService->~NimBLEService(); pServer->getAdvertising()->stop(); -#if defined(CONFIG_IDF_TARGET_ESP32C5) - esp_bt_controller_deinit(); -#else - BLEDevice::deinit(); -#endif BLEConnected = false; } void ble_test() { printf("ble test\n"); - // if (!is_ble_inited) - // { - printf("Init ble server\n"); - initBLEServer(); - delay(100); - is_ble_inited = true; - // } + if (!is_ble_inited) { + printf("Init ble server\n"); + if (!initBLEServer()) { + displayError("Failed to init BLE server"); + return; + } + delay(100); + } disPlayBLESend(); + if (pServer) { + pServer->getAdvertising()->stop(); + } + stopBLEStack(); + printf("Quit ble test\n"); } diff --git a/src/modules/ble/ble_common.h b/src/modules/ble/ble_common.h index e4c700378..b5e08a180 100644 --- a/src/modules/ble/ble_common.h +++ b/src/modules/ble/ble_common.h @@ -1,11 +1,9 @@ #ifndef __BLE_COMMON_H__ #define __BLE_COMMON_H__ -// #include "BLE2902.h" #include #include #include - #include #include #include @@ -13,16 +11,61 @@ #include "core/display.h" #include +//============================================================================= +// NimBLE Version Detection - Must be consistent across all files +//============================================================================= + +// Detect NimBLE 2.x by checking for features only available in v2+ +#if defined(NIMBLE_VERSION) + #if NIMBLE_VERSION >= 20000 + #define NIMBLE_V2_PLUS 1 + #endif +#elif defined(NIMBLE_CPP_VERSION) && NIMBLE_CPP_VERSION >= 2 + #define NIMBLE_V2_PLUS 1 +#elif defined(NIMBLE_VERSION_MAJOR) && NIMBLE_VERSION_MAJOR >= 2 + #define NIMBLE_V2_PLUS 1 +#elif defined(NIMBLE_VERSION_MAJOR) && NIMBLE_VERSION_MAJOR == 1 && NIMBLE_VERSION_MINOR >= 5 + #define NIMBLE_V2_PLUS 1 +#elif __has_include() + #define NIMBLE_V2_PLUS 1 +#endif + +// If none of the above matched, check if NimBLEScanResults is a type +#ifndef NIMBLE_V2_PLUS + #ifdef __has_include + #if __has_include() + #if defined(ESP_IDF_VERSION) && ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) + #define NIMBLE_V2_PLUS 1 + #endif + #endif + #endif +#endif + +// If we still don't know, default to v1 behavior (safe fallback) +#ifndef NIMBLE_V2_PLUS + #define NIMBLE_V2_PLUS 0 +#endif + +//============================================================================= +// BLE Constants +//============================================================================= + #define SCANTIME 5 #define SCANTYPE ACTIVE #define SCAN_INT 100 #define SCAN_WINDOW 99 +// Maximum number of BLE devices to display to prevent memory issues +#define MAX_DISPLAY_DEVICES 100 + +// Memory protection: Reduce scan time in low-memory situations +#define SCAN_TIME_REDUCED 3 + extern BLEScan *pBLEScan; extern int scanTime; void ble_test(); -#if 0 // keep it out for now +#if 0 #ifdef BOARD_HAS_PSRAM constexpr bool FORCE_RADIO_TEARDOWN_ON_SWITCH = false; #else @@ -32,16 +75,10 @@ constexpr bool FORCE_RADIO_TEARDOWN_ON_SWITCH = true; constexpr bool FORCE_RADIO_TEARDOWN_ON_SWITCH = false; #endif -bool ble_scan_setup(); // false = aborted (e.g. not enough contiguous RAM) +bool ble_scan_setup(); void ble_scan(); void stopBLEStack(); -// GATT notification tolerant to temporary MSYS-pool exhaustion. -// NimBLECharacteristic::notify() returns false when the os_mbuf could not be -// allocated (MSYS pool full, made worse by reducing -// CONFIG_BT_NIMBLE_MSYS_*_BLOCK_COUNT). If untreated, the notification is silently -// dropped (lost HID key / BLE-serial chunk). Retry while yielding 1 tick for the -// host to drain the pool. Mirrors wifiRawTx() on the Wi-Fi side. bool bleNotifyRetry(NimBLECharacteristic *chr, const uint8_t *value, size_t length, uint8_t retries = 8); bool bleNotifyRetry(NimBLECharacteristic *chr, uint8_t retries = 8);