Expose live nRF52 advertiser and SoftDevice state in opt-in trace

This commit is contained in:
mikecarper committed 2026-10-19 20:38:12 -07:00
1 parent e0442880e1
commit 59e2b5bfe4
3 files changed
+339

No files matched your search

+10
View File
@@ -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<uint32_t>& 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);
+66
View File
@@ -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");
+263
View File
@@ -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 <cassert>
#include <cstdint>
#include <cstdio>
#include <cstring>
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 <cassert>
#include <cstdint>
#include <cstdio>
#include <cstring>
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)