diff --git a/arch/nrf52/extra_scripts/patch_bluefruit.py b/arch/nrf52/extra_scripts/patch_bluefruit.py new file mode 100644 index 00000000..b43bffb5 --- /dev/null +++ b/arch/nrf52/extra_scripts/patch_bluefruit.py @@ -0,0 +1,198 @@ +""" +Bluefruit BLE Patch Script + +Patches Bluefruit library to fix semaphore leak bug that causes device lockup +when BLE central disconnects unexpectedly (e.g., going out of range, supervision timeout). + +Patches applied: +1. BLEConnection.h: Add _hvn_qsize member to track semaphore queue size +2. BLEConnection.cpp: Store hvn_qsize and restore semaphore on disconnect + +Bug description: +- When a BLE central disconnects unexpectedly (reason=8 supervision timeout), + the BLE_GATTS_EVT_HVN_TX_COMPLETE event may never fire +- This leaves the _hvn_sem counting semaphore in a decremented state +- Since BLEConnection objects are reused (destructor never called), the + semaphore count is never restored +- Eventually all semaphore counts are exhausted and notify() blocks/fails + +""" + +from pathlib import Path + +Import("env") # pylint: disable=undefined-variable + + +def _patch_ble_connection_header(source: Path) -> bool: + """ + Add _hvn_qsize member variable to BLEConnection class. + + This is needed to restore the semaphore to its correct count on disconnect. + + Returns True if patch was applied or already applied, False on error. + """ + try: + content = source.read_text() + + # Check if already patched + if "_hvn_qsize" in content: + return True # Already patched + + # Find the location to insert - after _phy declaration + original_pattern = ''' uint8_t _phy; + + uint8_t _role;''' + + patched_pattern = ''' uint8_t _phy; + uint8_t _hvn_qsize; + + uint8_t _role;''' + + if original_pattern not in content: + print("Bluefruit patch: WARNING - BLEConnection.h pattern not found") + return False + + content = content.replace(original_pattern, patched_pattern) + source.write_text(content) + + # Verify + if "_hvn_qsize" not in source.read_text(): + return False + + return True + except Exception as e: + print(f"Bluefruit patch: ERROR patching BLEConnection.h: {e}") + return False + + +def _patch_ble_connection_source(source: Path) -> bool: + """ + Patch BLEConnection.cpp to: + 1. Store hvn_qsize in constructor + 2. Restore _hvn_sem semaphore to full count on disconnect + + Returns True if patch was applied or already applied, False on error. + """ + try: + content = source.read_text() + + # Check if already patched (look for the restore loop) + if "uxSemaphoreGetCount(_hvn_sem)" in content: + return True # Already patched + + # Patch 1: Store queue size in constructor + constructor_original = ''' _hvn_sem = xSemaphoreCreateCounting(hvn_qsize, hvn_qsize);''' + + constructor_patched = ''' _hvn_qsize = hvn_qsize; + _hvn_sem = xSemaphoreCreateCounting(hvn_qsize, hvn_qsize);''' + + if constructor_original not in content: + print("Bluefruit patch: WARNING - BLEConnection.cpp constructor pattern not found") + return False + + content = content.replace(constructor_original, constructor_patched) + + # Patch 2: Restore semaphore on disconnect + disconnect_original = ''' case BLE_GAP_EVT_DISCONNECTED: + // mark as disconnected + _connected = false; + break;''' + + disconnect_patched = ''' case BLE_GAP_EVT_DISCONNECTED: + // Restore notification semaphore to full count + // This fixes lockup when disconnect occurs with notifications in flight + while (uxSemaphoreGetCount(_hvn_sem) < _hvn_qsize) { + xSemaphoreGive(_hvn_sem); + } + // Release indication semaphore if waiting + if (_hvc_sem) { + _hvc_received = false; + xSemaphoreGive(_hvc_sem); + } + // mark as disconnected + _connected = false; + break;''' + + if disconnect_original not in content: + print("Bluefruit patch: WARNING - BLEConnection.cpp disconnect pattern not found") + return False + + content = content.replace(disconnect_original, disconnect_patched) + source.write_text(content) + + # Verify + verify_content = source.read_text() + if "uxSemaphoreGetCount(_hvn_sem)" not in verify_content: + return False + if "_hvn_qsize = hvn_qsize" not in verify_content: + return False + + return True + except Exception as e: + print(f"Bluefruit patch: ERROR patching BLEConnection.cpp: {e}") + return False + + +def _apply_bluefruit_patches(target, source, env): # pylint: disable=unused-argument + framework_path = env.get("PLATFORMFW_DIR") + if not framework_path: + framework_path = env.PioPlatform().get_package_dir("framework-arduinoadafruitnrf52") + + if not framework_path: + print("Bluefruit patch: ERROR - framework directory not found") + env.Exit(1) + return + + framework_dir = Path(framework_path) + bluefruit_lib = framework_dir / "libraries" / "Bluefruit52Lib" / "src" + patch_failed = False + + # Patch BLEConnection.h + conn_header = bluefruit_lib / "BLEConnection.h" + if conn_header.exists(): + before = conn_header.read_text() + success = _patch_ble_connection_header(conn_header) + after = conn_header.read_text() + + if success: + if before != after: + print("Bluefruit patch: OK - Applied BLEConnection.h fix (added _hvn_qsize member)") + else: + print("Bluefruit patch: OK - BLEConnection.h already patched") + else: + print("Bluefruit patch: FAILED - BLEConnection.h") + patch_failed = True + else: + print(f"Bluefruit patch: ERROR - BLEConnection.h not found at {conn_header}") + patch_failed = True + + # Patch BLEConnection.cpp + conn_source = bluefruit_lib / "BLEConnection.cpp" + if conn_source.exists(): + before = conn_source.read_text() + success = _patch_ble_connection_source(conn_source) + after = conn_source.read_text() + + if success: + if before != after: + print("Bluefruit patch: OK - Applied BLEConnection.cpp fix (restore semaphore on disconnect)") + else: + print("Bluefruit patch: OK - BLEConnection.cpp already patched") + else: + print("Bluefruit patch: FAILED - BLEConnection.cpp") + patch_failed = True + else: + print(f"Bluefruit patch: ERROR - BLEConnection.cpp not found at {conn_source}") + patch_failed = True + + if patch_failed: + print("Bluefruit patch: CRITICAL - Patch failed! Build aborted.") + env.Exit(1) + + +# Register the patch to run before build +bluefruit_action = env.VerboseAction(_apply_bluefruit_patches, "Applying Bluefruit BLE patches...") +env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", bluefruit_action) + +# Also run immediately to patch before any compilation +_apply_bluefruit_patches(None, None, env) diff --git a/platformio.ini b/platformio.ini index 3907cf64..75d37e86 100644 --- a/platformio.ini +++ b/platformio.ini @@ -79,7 +79,9 @@ extends = arduino_base platform = nordicnrf52 platform_packages = framework-arduinoadafruitnrf52 @ 1.10700.0 -extra_scripts = create-uf2.py +extra_scripts = + create-uf2.py + arch/nrf52/extra_scripts/patch_bluefruit.py build_flags = ${arduino_base.build_flags} -D NRF52_PLATFORM -D LFS_NO_ASSERT=1