patch_bluefruit.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. """
  2. Bluefruit BLE Patch Script
  3. Patches Bluefruit library to fix semaphore leak bug that causes device lockup
  4. when BLE central disconnects unexpectedly (e.g., going out of range, supervision timeout).
  5. Patches applied:
  6. 1. BLEConnection.h: Add _hvn_qsize member to track semaphore queue size
  7. 2. BLEConnection.cpp: Store hvn_qsize and restore semaphore on disconnect
  8. Bug description:
  9. - When a BLE central disconnects unexpectedly (reason=8 supervision timeout),
  10. the BLE_GATTS_EVT_HVN_TX_COMPLETE event may never fire
  11. - This leaves the _hvn_sem counting semaphore in a decremented state
  12. - Since BLEConnection objects are reused (destructor never called), the
  13. semaphore count is never restored
  14. - Eventually all semaphore counts are exhausted and notify() blocks/fails
  15. """
  16. from pathlib import Path
  17. Import("env") # pylint: disable=undefined-variable
  18. def _patch_ble_connection_header(source: Path) -> bool:
  19. """
  20. Add _hvn_qsize member variable to BLEConnection class.
  21. This is needed to restore the semaphore to its correct count on disconnect.
  22. Returns True if patch was applied or already applied, False on error.
  23. """
  24. try:
  25. content = source.read_text()
  26. # Check if already patched
  27. if "_hvn_qsize" in content:
  28. return True # Already patched
  29. # Find the location to insert - after _phy declaration
  30. original_pattern = ''' uint8_t _phy;
  31. uint8_t _role;'''
  32. patched_pattern = ''' uint8_t _phy;
  33. uint8_t _hvn_qsize;
  34. uint8_t _role;'''
  35. if original_pattern not in content:
  36. print("Bluefruit patch: WARNING - BLEConnection.h pattern not found")
  37. return False
  38. content = content.replace(original_pattern, patched_pattern)
  39. source.write_text(content)
  40. # Verify
  41. if "_hvn_qsize" not in source.read_text():
  42. return False
  43. return True
  44. except Exception as e:
  45. print(f"Bluefruit patch: ERROR patching BLEConnection.h: {e}")
  46. return False
  47. def _patch_ble_connection_source(source: Path) -> bool:
  48. """
  49. Patch BLEConnection.cpp to:
  50. 1. Store hvn_qsize in constructor
  51. 2. Restore _hvn_sem semaphore to full count on disconnect
  52. Returns True if patch was applied or already applied, False on error.
  53. """
  54. try:
  55. content = source.read_text()
  56. # Check if already patched (look for the restore loop)
  57. if "uxSemaphoreGetCount(_hvn_sem)" in content:
  58. return True # Already patched
  59. # Patch 1: Store queue size in constructor
  60. constructor_original = ''' _hvn_sem = xSemaphoreCreateCounting(hvn_qsize, hvn_qsize);'''
  61. constructor_patched = ''' _hvn_qsize = hvn_qsize;
  62. _hvn_sem = xSemaphoreCreateCounting(hvn_qsize, hvn_qsize);'''
  63. if constructor_original not in content:
  64. print("Bluefruit patch: WARNING - BLEConnection.cpp constructor pattern not found")
  65. return False
  66. content = content.replace(constructor_original, constructor_patched)
  67. # Patch 2: Restore semaphore on disconnect
  68. disconnect_original = ''' case BLE_GAP_EVT_DISCONNECTED:
  69. // mark as disconnected
  70. _connected = false;
  71. break;'''
  72. disconnect_patched = ''' case BLE_GAP_EVT_DISCONNECTED:
  73. // Restore notification semaphore to full count
  74. // This fixes lockup when disconnect occurs with notifications in flight
  75. while (uxSemaphoreGetCount(_hvn_sem) < _hvn_qsize) {
  76. xSemaphoreGive(_hvn_sem);
  77. }
  78. // Release indication semaphore if waiting
  79. if (_hvc_sem) {
  80. _hvc_received = false;
  81. xSemaphoreGive(_hvc_sem);
  82. }
  83. // mark as disconnected
  84. _connected = false;
  85. break;'''
  86. if disconnect_original not in content:
  87. print("Bluefruit patch: WARNING - BLEConnection.cpp disconnect pattern not found")
  88. return False
  89. content = content.replace(disconnect_original, disconnect_patched)
  90. source.write_text(content)
  91. # Verify
  92. verify_content = source.read_text()
  93. if "uxSemaphoreGetCount(_hvn_sem)" not in verify_content:
  94. return False
  95. if "_hvn_qsize = hvn_qsize" not in verify_content:
  96. return False
  97. return True
  98. except Exception as e:
  99. print(f"Bluefruit patch: ERROR patching BLEConnection.cpp: {e}")
  100. return False
  101. def _apply_bluefruit_patches(target, source, env): # pylint: disable=unused-argument
  102. framework_path = env.get("PLATFORMFW_DIR")
  103. if not framework_path:
  104. framework_path = env.PioPlatform().get_package_dir("framework-arduinoadafruitnrf52")
  105. if not framework_path:
  106. print("Bluefruit patch: ERROR - framework directory not found")
  107. env.Exit(1)
  108. return
  109. framework_dir = Path(framework_path)
  110. bluefruit_lib = framework_dir / "libraries" / "Bluefruit52Lib" / "src"
  111. patch_failed = False
  112. # Patch BLEConnection.h
  113. conn_header = bluefruit_lib / "BLEConnection.h"
  114. if conn_header.exists():
  115. before = conn_header.read_text()
  116. success = _patch_ble_connection_header(conn_header)
  117. after = conn_header.read_text()
  118. if success:
  119. if before != after:
  120. print("Bluefruit patch: OK - Applied BLEConnection.h fix (added _hvn_qsize member)")
  121. else:
  122. print("Bluefruit patch: OK - BLEConnection.h already patched")
  123. else:
  124. print("Bluefruit patch: FAILED - BLEConnection.h")
  125. patch_failed = True
  126. else:
  127. print(f"Bluefruit patch: ERROR - BLEConnection.h not found at {conn_header}")
  128. patch_failed = True
  129. # Patch BLEConnection.cpp
  130. conn_source = bluefruit_lib / "BLEConnection.cpp"
  131. if conn_source.exists():
  132. before = conn_source.read_text()
  133. success = _patch_ble_connection_source(conn_source)
  134. after = conn_source.read_text()
  135. if success:
  136. if before != after:
  137. print("Bluefruit patch: OK - Applied BLEConnection.cpp fix (restore semaphore on disconnect)")
  138. else:
  139. print("Bluefruit patch: OK - BLEConnection.cpp already patched")
  140. else:
  141. print("Bluefruit patch: FAILED - BLEConnection.cpp")
  142. patch_failed = True
  143. else:
  144. print(f"Bluefruit patch: ERROR - BLEConnection.cpp not found at {conn_source}")
  145. patch_failed = True
  146. if patch_failed:
  147. print("Bluefruit patch: CRITICAL - Patch failed! Build aborted.")
  148. env.Exit(1)
  149. # Register the patch to run before build
  150. bluefruit_action = env.VerboseAction(_apply_bluefruit_patches, "Applying Bluefruit BLE patches...")
  151. env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", bluefruit_action)
  152. # Also run immediately to patch before any compilation
  153. _apply_bluefruit_patches(None, None, env)