aboutsummaryrefslogtreecommitdiff
path: root/arch/nrf52/extra_scripts/patch_bluefruit.py
blob: b43bffb5db85fe51b2eb8462f56e66d23b4a5a5b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
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)