mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-23 21:33:53 +00:00
Expose live nRF52 advertiser and SoftDevice state in opt-in trace
This commit is contained in:
1 parent
e0442880e1
commit
59e2b5bfe4
3 files changed
+339
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user