"""Restore pinned Arduino 3.3.11 BLE service ownership for reversible shutdown. BLEDevice owns the new wrapper's server, but its implicit destructor leaves raw service pointers behind. BLEService's destructor is private; delete it from BLEDevice's existing friend context. NUS characteristics/descriptors stay application-owned, as on the older wrappers. Never modify the shared SDK. """ from pathlib import Path import hashlib import re FRAMEWORKS_UNCHANGED = {(2, 0, 17), (3, 1, 3)} FRAMEWORK_PATCHED = (3, 3, 11) PINNED_SHA256 = "ec3735051f3263fc0101986fe33602a2593c294c6ccbd9f7b1b416c15529c3c9" PATCHED_SHA256 = "bf8fecfd6367eb0a2c23dd610336fefa17687d1f7bbc38e9d8f8642835763e87" SOURCE_DIRECTORY = ("libraries", "BLE", "src") ORIGINAL = ''' if (m_pServer != nullptr) { delete m_pServer; m_pServer = nullptr; } ''' REPAIRED = ''' if (m_pServer != nullptr) { // MeshCore: the server map owns raw service pointers. BLEDevice is a // friend of BLEServer/BLEService, whose destructor is private here. // The application has already stopped host callback dispatch. BLEService* service = m_pServer->m_serviceMap.getFirst(); while (service != nullptr) { delete service; service = m_pServer->m_serviceMap.getNext(); } delete m_pServer; m_pServer = nullptr; } ''' SERVICE_ORIGINAL = 'BLEService::~BLEService() {\n#if defined(CONFIG_NIMBLE_ENABLED)\n if (m_pSvcDef != nullptr) {\n if (m_pSvcDef->characteristics != nullptr) {\n for (int i = 0; m_pSvcDef->characteristics[i].uuid != NULL; ++i) {\n if (m_pSvcDef->characteristics[i].descriptors) {\n delete (m_pSvcDef->characteristics[i].descriptors);\n }\n }\n delete (m_pSvcDef->characteristics);\n }\n\n delete (m_pSvcDef);\n }\n#endif\n}' # Each definition is allocated with new[] by BLEService::start(). SERVICE_REPAIRED = SERVICE_ORIGINAL.replace("delete (", "delete[] (") SOURCE_PINS = { "BLEDevice.cpp": (PINNED_SHA256, PATCHED_SHA256, ORIGINAL, REPAIRED), "BLEService.cpp": ( "2b71699e1962cdc37a54ace429ebdd408b0e40b379d621d427add5bb5a93b252", "8c4d8917ffb768a9eab468b1be06be209b4b8fc065b125c821682b6579a30267", SERVICE_ORIGINAL, SERVICE_REPAIRED), } def patched_ble_source(source, framework_version, name="BLEDevice.cpp"): if framework_version in FRAMEWORKS_UNCHANGED: return source if framework_version != FRAMEWORK_PATCHED: raise RuntimeError("ESP32 BLE lifecycle fix: unreviewed framework version") source = source.replace("\r\n", "\n") try: original_digest, patched_digest, original, repaired = SOURCE_PINS[name] except KeyError as error: raise RuntimeError("ESP32 BLE lifecycle fix: unreviewed source") from error digest = hashlib.sha256(source.encode("utf-8")).hexdigest() if digest == patched_digest: return source if digest != original_digest or source.count(original) != 1: raise RuntimeError("ESP32 BLE lifecycle fix: changed pinned source; review SDK update") patched = source.replace(original, repaired, 1) if hashlib.sha256(patched.encode("utf-8")).hexdigest() != patched_digest: raise RuntimeError("ESP32 BLE lifecycle fix: unexpected transform result") return patched def framework_version(source): header = source.parents[3] / "cores" / "esp32" / "esp_arduino_version.h" try: text = header.read_text(encoding="utf-8") except OSError as error: raise RuntimeError("ESP32 BLE lifecycle fix: missing framework version header") from error entries = re.findall(r"^#define ESP_ARDUINO_VERSION_(MAJOR|MINOR|PATCH)\s+(\d+)\s*$", text, re.MULTILINE) values = dict(entries) if len(entries) != 3 or set(values) != {"MAJOR", "MINOR", "PATCH"}: raise RuntimeError("ESP32 BLE lifecycle fix: malformed framework version") return tuple(int(values[key]) for key in ("MAJOR", "MINOR", "PATCH")) def replace_ble_source(build_env, node): source = Path(node.srcnode().get_abspath()) if source.parts[-4:-1] != SOURCE_DIRECTORY or source.name not in SOURCE_PINS: return node version = framework_version(source) original = source.read_text(encoding="utf-8") patched = patched_ble_source(original, version, source.name) if patched == original: return node destination = Path(build_env.subst("$BUILD_DIR")) / "patched-esp32-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") build_env.AppendUnique(CPPPATH=[str(source.parent)]) return build_env.File(str(destination)) def install(build_env): build_env.AddBuildMiddleware(replace_ble_source, "*libraries*BLE*src*BLE*.cpp") if "Import" in globals(): Import("env") install(env)