From 59e2b5bfe4433c585c263c606ba7ec9e09393e3a Mon Sep 17 00:00:00 2001 From: mikecarper Date: Mon, 19 Oct 2026 20:37:05 -0700 Subject: [PATCH] Expose live nRF52 advertiser and SoftDevice state in opt-in trace --- src/helpers/nrf52/BleDebugTrace.h | 10 + src/helpers/nrf52/SerialBLEInterface.cpp | 66 ++++++ test/test_nrf52_ble_trace.py | 263 +++++++++++++++++++++++ 3 files changed, 339 insertions(+) diff --git a/src/helpers/nrf52/BleDebugTrace.h b/src/helpers/nrf52/BleDebugTrace.h index 29b937bde..25d4f7a0d 100644 --- a/src/helpers/nrf52/BleDebugTrace.h +++ b/src/helpers/nrf52/BleDebugTrace.h @@ -32,6 +32,8 @@ extern "C" { extern MeshBleTraceBuffer mesh_ble_trace; } void meshBleTraceState(char* reply, size_t capacity); void meshBleTraceIdentity(char* reply, size_t capacity); void meshBleTraceAdvertising(char* reply, size_t capacity); +void meshBleTraceAdvertiser(char* reply, size_t capacity); +void meshBleTraceStack(char* reply, size_t capacity); static inline void meshBleTraceMax(std::atomic& value, uint32_t sample) { uint32_t old = value.load(std::memory_order_relaxed); while (sample > old && !value.compare_exchange_weak(old, sample, @@ -79,6 +81,14 @@ static inline void meshBleTraceDispatchExit(uint32_t event, uint32_t handle) { } static inline bool meshBleTraceCommand(const char* command, char* reply, size_t capacity) { + if (!strcmp(command, "get bluetooth.trace.stack")) { + meshBleTraceStack(reply, capacity); + return true; + } + if (!strcmp(command, "get bluetooth.trace.advertiser")) { + meshBleTraceAdvertiser(reply, capacity); + return true; + } if (!strcmp(command, "get bluetooth.trace.advertising") || !strcmp(command, "get ble.trace.advertising")) { meshBleTraceAdvertising(reply, capacity); diff --git a/src/helpers/nrf52/SerialBLEInterface.cpp b/src/helpers/nrf52/SerialBLEInterface.cpp index efa2e8f9e..e0552d8ba 100644 --- a/src/helpers/nrf52/SerialBLEInterface.cpp +++ b/src/helpers/nrf52/SerialBLEInterface.cpp @@ -95,6 +95,72 @@ void meshBleTraceIdentity(char* reply, size_t capacity) { (unsigned)Bluefruit.ScanResponse.count()); } +void meshBleTraceStack(char* reply, size_t capacity) { + uint8_t enabled = 0; + if (!instance || mesh_nrf52::softdeviceIsEnabled(enabled) != NRF_SUCCESS || !enabled) { + snprintf(reply, capacity, "> Bluetooth not initialized"); + return; + } + // Query the running stack: a build's SDK setting or bootloader version + // label alone cannot prove which SoftDevice firmware is installed. + ble_version_t version = {}; + const uint32_t status = sd_ble_version_get(&version); + __asm__ __volatile__("" ::: "memory"); + if (status != NRF_SUCCESS) { + snprintf(reply, capacity, "> status=%lu", (unsigned long)status); + return; + } + snprintf(reply, capacity, "> status=0,link_layer=%u,company=%u,fwid=0x%04x", + (unsigned)version.version_number, (unsigned)version.company_id, + (unsigned)version.subversion_number); +} + +void meshBleTraceAdvertiser(char* reply, size_t capacity) { + uint8_t enabled = 0; + if (!instance || mesh_nrf52::softdeviceIsEnabled(enabled) != NRF_SUCCESS || !enabled) { + snprintf(reply, capacity, "> Bluetooth not initialized"); + return; + } + // Query the SoftDevice rather than Bluefruit's cached running flag. Its + // advertising handle is private; bounded read-only enumeration avoids + // assuming its value or depending on the framework's object layout. + unsigned active = 0, idle = 0, invalid = 0; + unsigned first_handle = BLE_GAP_ADV_SET_HANDLE_NOT_SET; + uint32_t unexpected = NRF_SUCCESS; + ble_gap_addr_t first_address = {}; + for (unsigned handle = 0; handle < BLE_GAP_ADV_SET_HANDLE_NOT_SET; ++handle) { + ble_gap_addr_t address = {}; + const uint32_t status = sd_ble_gap_adv_addr_get((uint8_t)handle, &address); + __asm__ __volatile__("" ::: "memory"); + if (status == NRF_SUCCESS) { + if (!active) { + first_handle = handle; + first_address = address; + } + ++active; + } else if (status == NRF_ERROR_INVALID_STATE) { + ++idle; + } else if (status == BLE_ERROR_INVALID_ADV_HANDLE) { + ++invalid; + } else { + unexpected = status; + break; + } + } + ble_gap_privacy_params_t privacy = {}; + const uint32_t privacy_status = sd_ble_gap_privacy_get(&privacy); + __asm__ __volatile__("" ::: "memory"); + snprintf(reply, capacity, + "> cached=%u,active=%u,idle=%u,invalid=%u,error=%lu,h=%u,t=%u,mac=%02x%02x%02x%02x%02x%02x,privacy_status=%lu,privacy=%u,cycle=%u", + (unsigned)Bluefruit.Advertising.isRunning(), active, idle, invalid, + (unsigned long)unexpected, first_handle, (unsigned)first_address.addr_type, + first_address.addr[5], first_address.addr[4], first_address.addr[3], + first_address.addr[2], first_address.addr[1], first_address.addr[0], + (unsigned long)privacy_status, + privacy_status == NRF_SUCCESS ? (unsigned)privacy.privacy_mode : 0u, + privacy_status == NRF_SUCCESS ? (unsigned)privacy.private_addr_cycle_s : 0u); +} + void meshBleTraceAdvertising(char* reply, size_t capacity) { if (!instance) { snprintf(reply, capacity, "> Bluetooth not initialized"); diff --git a/test/test_nrf52_ble_trace.py b/test/test_nrf52_ble_trace.py index 426e09769..6af68c14c 100644 --- a/test/test_nrf52_ble_trace.py +++ b/test/test_nrf52_ble_trace.py @@ -30,6 +30,12 @@ void meshBleTraceIdentity(char* reply, size_t capacity) { void meshBleTraceAdvertising(char* reply, size_t capacity) { snprintf(reply,capacity,"> advertising"); } +void meshBleTraceAdvertiser(char* reply, size_t capacity) { + snprintf(reply,capacity,"> advertiser"); +} +void meshBleTraceStack(char* reply, size_t capacity) { + snprintf(reply,capacity,"> stack"); +} int main() { char reply[160]; assert(!meshBleTraceCommand("get radio",reply,sizeof(reply))); @@ -57,6 +63,19 @@ int main() { assert(!strcmp(reply,"> advertising")); } assert(!meshBleTraceCommand("get ble.trace.advertising extra",reply,sizeof(reply))); + assert(meshBleTraceCommand("get bluetooth.trace.advertiser",reply,sizeof(reply))); + assert(!strcmp(reply,"> advertiser")); + for (const char* command : {"get bluetooth.trace.advertiser extra", + "get bluetooth.trace.advertiserx", "get ble.trace.advertiser"}) { + assert(!meshBleTraceCommand(command,reply,sizeof(reply))); + } + assert(meshBleTraceCommand("get bluetooth.trace.stack",reply,sizeof(reply))); + assert(!strcmp(reply,"> stack")); + for (const char* command : {"get bluetooth.trace.stack extra", + "get bluetooth.trace.stackx", "get bluetooth.trace.stack ", + "get ble.trace.stack"}) { + assert(!meshBleTraceCommand(command,reply,sizeof(reply))); + } meshBleTraceDispatchEnter(0x10,0); assert(mesh_ble_trace.dispatch_event.load()==0x10); meshBleTraceDispatchExit(0x10,0); @@ -223,6 +242,250 @@ int main() { result = subprocess.run([str(binary)], capture_output=True, text=True) self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + def test_real_advertiser_query_uses_live_sdk_state_and_bounded_handle_enumeration(self): + source = (ROOT / "src/helpers/nrf52/SerialBLEInterface.cpp").read_text() + start = source.index("void meshBleTraceAdvertiser(") + implementation = source[start:source.index("\n}\n", start) + 3] + with tempfile.TemporaryDirectory() as directory: + work = Path(directory) + cpp = work / "advertiser.cpp" + cpp.write_text(r''' +#include +#include +#include +#include +static constexpr uint32_t NRF_SUCCESS = 0; +static constexpr uint32_t NRF_ERROR_INVALID_STATE = 8; +static constexpr uint32_t BLE_ERROR_INVALID_ADV_HANDLE = 0x3004; +static constexpr uint8_t BLE_GAP_ADV_SET_HANDLE_NOT_SET = 255; +struct ble_gap_addr_t { uint8_t addr_type; uint8_t addr[6]; }; +struct ble_gap_irk_t { uint8_t irk[16]; }; +struct ble_gap_privacy_params_t { + uint8_t privacy_mode; + uint8_t private_addr_type; + uint16_t private_addr_cycle_s; + ble_gap_irk_t* p_device_irk; +}; +static void* instance; +static uint8_t enabled; +static uint32_t enabled_result, privacy_result; +static unsigned enabled_calls, advertiser_calls, privacy_calls; +static uint32_t statuses[255]; +static unsigned handle_calls[255]; +static ble_gap_privacy_params_t privacy_value; +namespace mesh_nrf52 { +static uint32_t softdeviceIsEnabled(uint8_t& value) { + ++enabled_calls; value = enabled; return enabled_result; +} +} +static struct { + struct { bool running; bool isRunning() { return running; } } Advertising; +} Bluefruit; +static uint32_t sd_ble_gap_adv_addr_get(uint8_t handle, ble_gap_addr_t* address) { + // In particular, never probe the sentinel 255 or wrap the handle loop. + assert(handle < BLE_GAP_ADV_SET_HANDLE_NOT_SET); + assert(handle_calls[handle]++ == 0); + ++advertiser_calls; + // Failed calls may leave arbitrary bytes; only successful output is usable. + *address = {1, {0xc6, 0xbb, 0xcd, 0x86, 0xa0, 0xd1}}; + return statuses[handle]; +} +static uint32_t sd_ble_gap_privacy_get(ble_gap_privacy_params_t* value) { + ++privacy_calls; + // NULL suppresses IRK output; this diagnostic must never request key data. + assert(value->p_device_irk == nullptr); + // The SDK's failed output must not become reported privacy settings. + *value = privacy_result == NRF_SUCCESS + ? privacy_value : ble_gap_privacy_params_t{255, 255, 65535, nullptr}; + return privacy_result; +} +static void reset() { + instance = &enabled; enabled = 1; + enabled_result = privacy_result = NRF_SUCCESS; + enabled_calls = advertiser_calls = privacy_calls = 0; + Bluefruit.Advertising.running = false; + privacy_value = {}; + for (unsigned i = 0; i < 255; ++i) { + statuses[i] = BLE_ERROR_INVALID_ADV_HANDLE; handle_calls[i] = 0; + } +} +@IMPLEMENTATION@ +int main() { + char reply[172]; + reset(); instance = nullptr; + meshBleTraceAdvertiser(reply, sizeof(reply)); + assert(!strcmp(reply, "> Bluetooth not initialized")); + assert(enabled_calls == 0 && advertiser_calls == 0 && privacy_calls == 0); + reset(); enabled = 0; + meshBleTraceAdvertiser(reply, sizeof(reply)); + assert(!strcmp(reply, "> Bluetooth not initialized")); + assert(enabled_calls == 1 && advertiser_calls == 0 && privacy_calls == 0); + reset(); enabled_result = UINT32_MAX; + meshBleTraceAdvertiser(reply, sizeof(reply)); + assert(!strcmp(reply, "> Bluetooth not initialized")); + assert(enabled_calls == 1 && advertiser_calls == 0 && privacy_calls == 0); + + // A nonzero live handle wins even when the framework cache says stopped. + reset(); statuses[37] = NRF_SUCCESS; privacy_value = {2, 0, 60, nullptr}; + meshBleTraceAdvertiser(reply, sizeof(reply)); + assert(!strcmp(reply, "> cached=0,active=1,idle=0,invalid=254,error=0,h=37,t=1,mac=d1a086cdbbc6,privacy_status=0,privacy=2,cycle=60")); + assert(advertiser_calls == 255 && privacy_calls == 1); + for (unsigned i = 0; i < 255; ++i) assert(handle_calls[i] == 1); + + // An existing inactive set must not be mistaken for cached advertising. + reset(); statuses[37] = NRF_ERROR_INVALID_STATE; Bluefruit.Advertising.running = true; + meshBleTraceAdvertiser(reply, sizeof(reply)); + assert(!strcmp(reply, "> cached=1,active=0,idle=1,invalid=254,error=0,h=255,t=0,mac=000000000000,privacy_status=0,privacy=0,cycle=0")); + assert(advertiser_calls == 255 && privacy_calls == 1); + + // Count all valid states and retain the first ACTIVE handle/address. + reset(); statuses[2] = NRF_ERROR_INVALID_STATE; + statuses[37] = statuses[254] = NRF_SUCCESS; + meshBleTraceAdvertiser(reply, sizeof(reply)); + assert(strstr(reply, "active=2,idle=1,invalid=252,error=0,h=37,t=1,mac=d1a086cdbbc6")); + assert(advertiser_calls == 255 && handle_calls[254] == 1); + + reset(); statuses[9] = 0xdeadbeef; + meshBleTraceAdvertiser(reply, sizeof(reply)); + assert(strstr(reply, "active=0,idle=0,invalid=9,error=3735928559,h=255,t=0,mac=000000000000")); + assert(advertiser_calls == 10 && handle_calls[10] == 0 && privacy_calls == 1); + reset(); statuses[254] = NRF_SUCCESS; privacy_result = UINT32_MAX; + meshBleTraceAdvertiser(reply, sizeof(reply)); + assert(strstr(reply, "active=1,idle=0,invalid=254,error=0,h=254,t=1,mac=d1a086cdbbc6")); + assert(strstr(reply, "privacy_status=4294967295,privacy=0,cycle=0")); + assert(strlen(reply) < sizeof(reply) - 1); + + // snprintf bounds include tiny buffers, zero capacity and a null/zero sink. + reset(); char guarded[] = {'A', 'x', 'x', 'x', 'x', 'x', 'B'}; + meshBleTraceAdvertiser(guarded + 1, 5); + assert(guarded[0] == 'A' && guarded[5] == 0 && guarded[6] == 'B'); + reset(); char single = 'X'; + meshBleTraceAdvertiser(&single, 1); + assert(single == 0); + reset(); char untouched = 'X'; + meshBleTraceAdvertiser(&untouched, 0); + assert(untouched == 'X'); + reset(); meshBleTraceAdvertiser(nullptr, 0); +} +'''.replace("@IMPLEMENTATION@", implementation)) + binary = work / "advertiser" + compiled = subprocess.run([ + "c++", "-std=c++17", "-O2", "-flto", "-Wall", "-Wextra", "-Werror", + "-fsanitize=address,undefined", "-fno-omit-frame-pointer", + "-fno-pie", "-no-pie", str(cpp), "-o", str(binary), + ], capture_output=True, text=True) + self.assertEqual(compiled.returncode, 0, compiled.stderr) + result = subprocess.run([str(binary)], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + def test_real_stack_query_reports_live_version_and_sdk_failures_with_bounds(self): + source = (ROOT / "src/helpers/nrf52/SerialBLEInterface.cpp").read_text() + start = source.index("void meshBleTraceStack(") + implementation = source[start:source.index("\n}\n", start) + 3] + with tempfile.TemporaryDirectory() as directory: + work = Path(directory) + cpp = work / "stack.cpp" + cpp.write_text(r''' +#include +#include +#include +#include +static constexpr uint32_t NRF_SUCCESS = 0; +struct ble_version_t { + uint8_t version_number; + uint16_t company_id; + uint16_t subversion_number; +}; +static void* instance; +static unsigned calls; +static unsigned enabled_calls; +static uint8_t enabled = 1; +static uint32_t enabled_status; +static uint32_t version_status; +static ble_version_t live_version; +namespace mesh_nrf52 { +static uint32_t softdeviceIsEnabled(uint8_t& value) { + assert(instance); + ++enabled_calls; + value = enabled; + return enabled_status; +} +} +static uint32_t sd_ble_version_get(ble_version_t* version) { + assert(instance && enabled_status == NRF_SUCCESS && enabled); + assert(version->version_number == 0 && version->company_id == 0 + && version->subversion_number == 0); + ++calls; + // A failed SDK call can overwrite every output field. Its data is unusable. + *version = live_version; + return version_status; +} +@IMPLEMENTATION@ +int main() { + char reply[172]; + meshBleTraceStack(reply, sizeof(reply)); + assert(!strcmp(reply, "> Bluetooth not initialized")); + assert(enabled_calls == 0 && calls == 0); + + instance = reply; + enabled = 0; + meshBleTraceStack(reply, sizeof(reply)); + assert(!strcmp(reply, "> Bluetooth not initialized")); + assert(enabled_calls == 1 && calls == 0); + enabled = 1; + enabled_status = UINT32_MAX; + meshBleTraceStack(reply, sizeof(reply)); + assert(!strcmp(reply, "> Bluetooth not initialized")); + assert(enabled_calls == 2 && calls == 0); + enabled_status = NRF_SUCCESS; + live_version = {9, 89, 0x0123}; + meshBleTraceStack(reply, sizeof(reply)); + assert(!strcmp(reply, "> status=0,link_layer=9,company=89,fwid=0x0123")); + assert(calls == 1); + live_version.subversion_number = 0x00b6; + meshBleTraceStack(reply, sizeof(reply)); + assert(!strcmp(reply, "> status=0,link_layer=9,company=89,fwid=0x00b6")); + assert(calls == 2); + + live_version = {UINT8_MAX, UINT16_MAX, UINT16_MAX}; + meshBleTraceStack(reply, sizeof(reply)); + assert(!strcmp(reply, "> status=0,link_layer=255,company=65535,fwid=0xffff")); + version_status = 8; + meshBleTraceStack(reply, sizeof(reply)); + assert(!strcmp(reply, "> status=8")); + version_status = UINT32_MAX; + meshBleTraceStack(reply, sizeof(reply)); + assert(!strcmp(reply, "> status=4294967295")); + + // Exercise every return path with guarded tiny, one-byte and zero sinks. + for (unsigned mode = 0; mode < 5; ++mode) { + instance = mode == 0 ? nullptr : reply; + enabled = mode == 1 ? 0 : 1; + enabled_status = mode == 2 ? UINT32_MAX : NRF_SUCCESS; + version_status = mode == 4 ? UINT32_MAX : NRF_SUCCESS; + char guarded[] = {'A', 'x', 'x', 'x', 'x', 'x', 'B'}; + meshBleTraceStack(guarded + 1, 5); + assert(guarded[0] == 'A' && guarded[5] == 0 && guarded[6] == 'B'); + char single = 'X'; + meshBleTraceStack(&single, 1); + assert(single == 0); + char untouched = 'X'; + meshBleTraceStack(&untouched, 0); + assert(untouched == 'X'); + meshBleTraceStack(nullptr, 0); + } +} +'''.replace("@IMPLEMENTATION@", implementation)) + binary = work / "stack" + compiled = subprocess.run([ + "c++", "-std=c++17", "-O2", "-flto", "-Wall", "-Wextra", "-Werror", + "-fsanitize=address,undefined", "-fno-omit-frame-pointer", + "-fno-pie", "-no-pie", str(cpp), "-o", str(binary), + ], capture_output=True, text=True) + self.assertEqual(compiled.returncode, 0, compiled.stderr) + result = subprocess.run([str(binary)], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + def test_concurrent_reader_never_accepts_mixed_records(self): with tempfile.TemporaryDirectory() as directory: work = Path(directory)