Files
MeshCore/scripts/nrf52_ble_cccd_fix.py
T
9d52b1fd68 Add saved LED controls across supported boards
Add get leds and set leds on|off for Companion, repeater, room server and
sensor builds. Preserve the setting across reboots and cover GPIO, RGB,
Bluetooth, notification and expander indicators without changing radio
amplifier controls or screen backlights.

RAK Companions blink while advertising and keep the Bluetooth LED off once
connected, including late timer callbacks.

Adapted from meshcore-dev/MeshCore#3605 and #2694.
Validated with host regressions and firmware builds for SenseCAP Solar,
RAK4631, T096, Heltec V4, T-Beam, Xiao S3, Pico W and Wio E5 Mini.

Co-authored-by: R0ck <develop@hectospark.co.uk>
Co-authored-by: Nick Dunklee <nick@dunklee.org>
2026-10-20 08:59:39 -07:00

231 lines
9.4 KiB
Python

"""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.
Cache the dispatch role before a disconnect deletes its BLEConnection. The
remaining peripheral/central routing must not dereference that freed object.
"""
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)'
)
DISCONNECT_PATCHES = (
(
" BLEConnection* conn = this->Connection(conn_hdl);\n\n"
' LOG_LV2("BLE",',
" BLEConnection* conn = this->Connection(conn_hdl);\n"
" // Keep role routing valid after disconnect deletes the connection.\n"
" uint8_t conn_role = conn ? conn->getRole() : BLE_GAP_ROLE_INVALID;\n\n"
' LOG_LV2("BLE",',
),
(
" conn = _connection[conn_hdl];\n\n"
" // Invoke connect callback\n",
" conn = _connection[conn_hdl];\n"
" conn_role = conn->getRole();\n\n"
" // Invoke connect callback\n",
),
(
" if ( (conn == NULL) || (conn->getRole() == BLE_GAP_ROLE_PERIPH) )\n",
" if ( (conn_role == BLE_GAP_ROLE_INVALID) || (conn_role == BLE_GAP_ROLE_PERIPH) )\n",
),
(
" if ( (conn == NULL) || (conn->getRole() == BLE_GAP_ROLE_CENTRAL) )\n",
" if ( (conn_role == BLE_GAP_ROLE_INVALID) || (conn_role == BLE_GAP_ROLE_CENTRAL) )\n",
),
)
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")
source = patched_disconnect_source(source)
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_disconnect_source(source):
source = source.replace("\r\n", "\n")
counts = [(source.count(old), source.count(fixed))
for old, fixed in DISCONNECT_PATCHES]
if all(old == 0 and fixed == 1 for old, fixed in counts):
return source
if not all(old == 1 and fixed == 0 for old, fixed in counts):
raise RuntimeError(
"nRF52 BLE disconnect fix: unrecognized framework dispatcher; review before building"
)
for old, fixed in DISCONNECT_PATCHES:
source = source.replace(old, fixed)
return source
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 <helpers/nrf52/BleDebugTrace.h>
#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 patched_led_source(source):
"""A queued advertising blink must not relight a disabled indicator."""
source = source.replace("\r\n", "\n")
old = """ (void) xTimer;
digitalToggle(LED_BLUE);
"""
new = """ (void) xTimer;
// MeshCore saved LED policy also covers already queued timer callbacks.
if (LED_BLUE < 0 || (uint32_t)LED_BLUE >= PINS_COUNT) return;
if (digitalPinToPinName(LED_BLUE) == 0xff) return;
#ifdef DISABLE_BLE_LED
const bool enabled = false;
#elif defined(RAK_BOARD) && defined(BLE_PIN_CODE)
const bool enabled = mesh::leds::enabled() && !Bluefruit.connected();
#else
const bool enabled = mesh::leds::enabled();
#endif
if (enabled) digitalToggle(LED_BLUE);
else digitalWrite(LED_BLUE, !LED_STATE_ON);
"""
include = '#include "bluefruit.h"\n'
policy_include = '#include <helpers/LedPolicy.h>\n'
old_connected = " digitalWrite(LED_BLUE, on_off ? LED_STATE_ON : (1-LED_STATE_ON) );"
new_connected = """ // RAK Companions blink while advertising, then go dark when connected.
#if defined(DISABLE_BLE_LED) || (defined(RAK_BOARD) && defined(BLE_PIN_CODE))
on_off = false;
#else
on_off = on_off && mesh::leds::enabled();
#endif
if (LED_BLUE >= 0 && (uint32_t)LED_BLUE < PINS_COUNT
&& digitalPinToPinName(LED_BLUE) != 0xff)
digitalWrite(LED_BLUE, on_off ? LED_STATE_ON : (1-LED_STATE_ON));"""
if (new in source and new_connected in source
and policy_include in source and old not in source
and old_connected not in source):
return source
if (source.count(old) != 1 or source.count(include) != 1
or source.count(old_connected) != 1):
raise RuntimeError("nRF52 BLE LED policy: unrecognized timer callback")
return source.replace(old, new).replace(old_connected, new_connected).replace(
include, include + policy_include)
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)
patched = patched_led_source(patched)
build_env.AppendUnique(CPPPATH=[build_env.subst("$PROJECT_DIR/src")])
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")