From af865170598ca3ce62dd74907e8e27ffa2c890ca Mon Sep 17 00:00:00 2001 From: mikecarper Date: Wed, 23 Sep 2026 01:28:49 -0700 Subject: [PATCH] fix(nrf52): avoid CCCD flash reset on Full Companion --- platformio.ini | 1 + scripts/nrf52_ble_cccd_fix.py | 53 +++++++++++++++++++++++++++++++++ test/test_nrf52_ble_cccd_fix.py | 32 ++++++++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 scripts/nrf52_ble_cccd_fix.py create mode 100644 test/test_nrf52_ble_cccd_fix.py diff --git a/platformio.ini b/platformio.ini index 5926a96a7..35879b694 100644 --- a/platformio.ini +++ b/platformio.ini @@ -155,6 +155,7 @@ platform_packages = extra_scripts = create-uf2.py pre:scripts/nrf52_usb_power_fix.py pre:scripts/nrf52_internal_flash_fix.py + pre:scripts/nrf52_ble_cccd_fix.py pre:scripts/nrf52_internal_bootloader_link.py ; Nordic's builder otherwise overwrites the LTO-aware archive tools. pre:scripts/nrf52_lto_toolchain.py diff --git a/scripts/nrf52_ble_cccd_fix.py b/scripts/nrf52_ble_cccd_fix.py new file mode 100644 index 000000000..754446b99 --- /dev/null +++ b/scripts/nrf52_ble_cccd_fix.py @@ -0,0 +1,53 @@ +"""Avoid Bluefruit's unstable live CCCD persistence on nRF52 Full Companions. + +The pinned core saves every bonded CCCD write from its callback worker into +InternalFS. On a T1000-E Full Companion this reproducibly resets the radio as +the client enables notifications, even after formatting InternalFS. Bond-key +persistence still works. The Companion client re-subscribes on connection; +this workaround sacrifices CCCD persistence, not key-based reconnection. +Patch a private build copy; never edit PlatformIO's shared framework cache. +""" + +from pathlib import Path + + +OLD_SAVE = """ { + conn->saveCccd(); + } +""" +FIXED_SAVE = """ { +#if !defined(COMPANION_RADIO_FULL) || !COMPANION_RADIO_FULL + conn->saveCccd(); +#endif + } +""" + + +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 replace_framework_source(build_env, node): + source = Path(node.srcnode().get_abspath()) + if source.name != "BLEGatt.cpp" or source.parent.name != "src": + return node + patched = patched_source(source.read_text(encoding="utf-8")) + build_env.AppendUnique(CPPPATH=[str(source.parent)]) + destination = Path(build_env.subst("$BUILD_DIR")) / "patched-nrf52-ble" / "BLEGatt.cpp" + 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("nRF52 BLE: Full Companion CCCD persistence workaround enabled") + return build_env.File(str(destination)) + + +if "Import" in globals(): + Import("env") + env.AddBuildMiddleware(replace_framework_source, "*BLEGatt.cpp") diff --git a/test/test_nrf52_ble_cccd_fix.py b/test/test_nrf52_ble_cccd_fix.py new file mode 100644 index 000000000..26bda7549 --- /dev/null +++ b/test/test_nrf52_ble_cccd_fix.py @@ -0,0 +1,32 @@ +"""Guard the nRF52 Full Companion CCCD build-local workaround.""" + +from importlib.util import module_from_spec, spec_from_file_location +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +SCRIPT = ROOT / "scripts/nrf52_ble_cccd_fix.py" +spec = spec_from_file_location("nrf52_ble_cccd_fix", SCRIPT) +module = module_from_spec(spec) +spec.loader.exec_module(module) + + +class Nrf52BleCccdFixTest(unittest.TestCase): + def test_only_full_companions_skip_persistence(self): + patched = module.patched_source("before\n" + module.OLD_SAVE + "after\n") + self.assertIn("!defined(COMPANION_RADIO_FULL) || !COMPANION_RADIO_FULL", patched) + self.assertIn("conn->saveCccd();", patched) + self.assertEqual(module.patched_source(patched), patched) + + def test_unrecognized_core_fails_closed(self): + with self.assertRaises(RuntimeError): + module.patched_source("changed SDK layout") + + def test_nrf52_base_loads_workaround(self): + ini = (ROOT / "platformio.ini").read_text(encoding="utf-8") + self.assertIn("pre:scripts/nrf52_ble_cccd_fix.py", ini) + + +if __name__ == "__main__": + unittest.main()