From 5172e5852696c1808b07e6a44ec065dcc81c4c62 Mon Sep 17 00:00:00 2001 From: Ben Meadors Date: Wed, 12 Aug 2026 18:15:18 -0500 Subject: [PATCH] fix(nimble): stop leaking BLE2904 descriptor on BLE re-setup (#11453) setupService() re-runs on every Bluetooth re-enable cycle, and every other callback object in it is deliberately function-local static for exactly that reason - the battery level descriptor was the one heap allocation that was missed. The framework never frees descriptors (~BLECharacteristic has an empty body and BLEDevice::deinit only deletes server/advertising/scan), so each cycle leaked one BLE2904. Make it static like its neighbors. --- src/nimble/NimbleBluetooth.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/nimble/NimbleBluetooth.cpp b/src/nimble/NimbleBluetooth.cpp index 36f5f9d0e..e01c32c72 100644 --- a/src/nimble/NimbleBluetooth.cpp +++ b/src/nimble/NimbleBluetooth.cpp @@ -1025,13 +1025,16 @@ void NimbleBluetooth::setupService() // Setup the battery service BLEService *batteryService = bleServer->createService(BLEUUID((uint16_t)0x180f)); // 0x180F is the Battery Service - BLE2904 *batteryLevelDescriptor = new BLE2904(); - batteryLevelDescriptor->setFormat(BLE2904::FORMAT_UINT8); - batteryLevelDescriptor->setNamespace(1); - batteryLevelDescriptor->setUnit(0x27ad); + // Static like the callback objects above: setupService() re-runs on every BLE re-enable, and + // the framework never frees descriptors (~BLECharacteristic is empty), so a heap allocation + // here leaks one BLE2904 per cycle. + static BLE2904 batteryLevelDescriptor; + batteryLevelDescriptor.setFormat(BLE2904::FORMAT_UINT8); + batteryLevelDescriptor.setNamespace(1); + batteryLevelDescriptor.setUnit(0x27ad); BatteryCharacteristic = batteryService->createCharacteristic( // 0x2A19 is the Battery Level characteristic) (uint16_t)0x2a19, BLECharacteristic::PROPERTY_READ | BLECharacteristic::PROPERTY_NOTIFY); - BatteryCharacteristic->addDescriptor(batteryLevelDescriptor); + BatteryCharacteristic->addDescriptor(&batteryLevelDescriptor); // Seed an initial 0-100 level so an early read of 0x2A19 returns a valid value. uint8_t initialLevel = (powerStatus && powerStatus->getHasBattery()) ? powerStatus->getBatteryChargePercent() : 0; if (initialLevel > 100)