"""Apply nRF52 Bluefruit fixes in private build copies of framework sources. Keep bonded CCCD persistence enabled, including Full Companions. The global SoftDeviceSvcCompat.h guard protects the framework's system-attribute queries from LTO miscompilation. The previous Full-only RAM workaround lost client notification subscriptions after reboot while retaining the pairing keys. Keep the RAM cache as a warm reconnect optimization, with the framework's persistent bond record as the cold boot fallback. Patch a private build copy; never edit PlatformIO's shared framework cache. With the pinned GCC 14/LTO toolchain, the optimized Bluefruit.begin() fails on the RAK4631 repeater: S140 rejects a valid single-peripheral role request with NRF_ERROR_RESOURCES. The unoptimized method starts DFU successfully. """ from pathlib import Path OLD_SAVE = """ { conn->saveCccd(); } """ FIXED_SAVE = """ { // SoftDeviceSvcCompat.h protects system-attribute query side effects. conn->saveCccd(); } """ OLD_LOAD = " if ( !loadCccd() ) sd_ble_gatts_sys_attr_set(_conn_hdl, NULL, 0, 0);\n" FIXED_LOAD = """#if defined(COMPANION_RADIO_FULL) && COMPANION_RADIO_FULL if ( !mesh_nrf52_restore_ram_cccd(_conn_hdl, &_bond_id_addr) && !loadCccd() ) { sd_ble_gatts_sys_attr_set(_conn_hdl, NULL, 0, 0); } #else if ( !loadCccd() ) sd_ble_gatts_sys_attr_set(_conn_hdl, NULL, 0, 0); #endif """ OLD_CONNECTION_INCLUDE = '#include "bluefruit.h"\n' FIXED_CONNECTION_INCLUDE = """#include "bluefruit.h" #if defined(COMPANION_RADIO_FULL) && COMPANION_RADIO_FULL bool mesh_nrf52_restore_ram_cccd(uint16_t conn_handle, const ble_gap_addr_t* peer); #endif """ OLD_BEGIN = "bool AdafruitBluefruit::begin(uint8_t prph_count, uint8_t central_count)" FIXED_BEGIN = ( 'bool __attribute__((noinline, optimize("O0"))) ' 'AdafruitBluefruit::begin(uint8_t prph_count, uint8_t central_count)' ) def patched_source(source): source = source.replace("\r\n", "\n") if FIXED_SAVE in source and OLD_SAVE not in source: return source if source.count(OLD_SAVE) != 1: raise RuntimeError( "nRF52 BLE CCCD fix: unrecognized framework source; review before building" ) return source.replace(OLD_SAVE, FIXED_SAVE) def patched_connection_source(source): source = source.replace("\r\n", "\n") if FIXED_LOAD in source and FIXED_CONNECTION_INCLUDE in source: return source if source.count(OLD_LOAD) != 1 or source.count(OLD_CONNECTION_INCLUDE) != 1: raise RuntimeError( "nRF52 BLE CCCD fix: unrecognized BLEConnection source; review before building" ) return source.replace(OLD_CONNECTION_INCLUDE, FIXED_CONNECTION_INCLUDE).replace( OLD_LOAD, FIXED_LOAD ) def patched_bluefruit_source(source): source = source.replace("\r\n", "\n") if FIXED_BEGIN in source and OLD_BEGIN not in source: return source if source.count(OLD_BEGIN) != 1: raise RuntimeError( "nRF52 BLE startup fix: unrecognized framework source; review before building" ) return source.replace(OLD_BEGIN, FIXED_BEGIN) def patched_trace_source(source): """Bracket real BLE dispatch in diagnostic builds, including stuck handlers.""" include = '#include "bluefruit.h"\n' entry = 'void AdafruitBluefruit::_ble_handler(ble_evt_t* evt)\n{\n' exit_marker = ' if (_event_cb) _event_cb(evt);\n' marker = '// MeshCore diagnostic dispatch trace' if marker in source: return source if any(source.count(value) != 1 for value in (include, entry, exit_marker)): raise RuntimeError("nRF52 BLE trace: unrecognized event dispatcher") source = source.replace(include, include + ''' #if defined(MESH_NRF52_BLE_TRACE) && MESH_NRF52_BLE_TRACE #include #endif ''') source = source.replace(entry, entry + ''' // MeshCore diagnostic dispatch trace #if defined(MESH_NRF52_BLE_TRACE) && MESH_NRF52_BLE_TRACE meshBleTraceDispatchEnter(evt->header.evt_id, evt->evt.common_evt.conn_handle); #endif ''') return source.replace(exit_marker, exit_marker + '''#if defined(MESH_NRF52_BLE_TRACE) && MESH_NRF52_BLE_TRACE meshBleTraceDispatchExit(evt->header.evt_id, evt->evt.common_evt.conn_handle); #endif ''') def replace_framework_source(build_env, node): source = Path(node.srcnode().get_abspath()) if source.name not in ("BLEGatt.cpp", "BLEConnection.cpp", "bluefruit.cpp") or source.parent.name != "src": return node original = source.read_text(encoding="utf-8") if source.name == "BLEGatt.cpp": patched = patched_source(original) elif source.name == "BLEConnection.cpp": patched = patched_connection_source(original) else: patched = patched_bluefruit_source(original) if "MESH_NRF52_BLE_TRACE" in str(build_env.get("CPPDEFINES", [])): patched = patched_trace_source(patched) build_env.AppendUnique(CPPPATH=[build_env.subst("$PROJECT_DIR/src")]) build_env.AppendUnique(CPPPATH=[str(source.parent)]) destination = Path(build_env.subst("$BUILD_DIR")) / "patched-nrf52-ble" / source.name destination.parent.mkdir(parents=True, exist_ok=True) if not destination.exists() or destination.read_text(encoding="utf-8") != patched: destination.write_text(patched, encoding="utf-8") print(f"nRF52 BLE: private framework fix in {source.name}") return build_env.File(str(destination)) if "Import" in globals(): Import("env") env.AddBuildMiddleware(replace_framework_source, "*BLEGatt.cpp") env.AddBuildMiddleware(replace_framework_source, "*BLEConnection.cpp") env.AddBuildMiddleware(replace_framework_source, "*bluefruit.cpp")