Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,19 @@ merged/signed hex, no DFU zip (OTA is explicitly out of scope).

## App behavior to preserve

- **No pairing/bonding:** `CONFIG_BT_SMP=n` — this sample has no encryption or bonding. A
real product handling sensitive data should set `CONFIG_BT_SMP=y` and use bonding

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should add this to the list of configs listed at https://docs.memfault.com/docs/mcu/mds#restricting-access and add a link to that doc here

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, great idea, did it!

(`CONFIG_BT_BONDABLE=y`, the default) instead. Because of this, we can't use the
`CONFIG_BT_MDS_PERM_RW_ENCRYPT` option described in [Restricting Access to
MDS](https://docs.memfault.com/docs/mcu/mds#restricting-access-to-mds) — that requires a
bonded, encrypted link — so this app falls back to the custom `access_enable` callback
from that same doc (see MDS access control below).
- **MDS access control:** register `bt_mds_cb` with an `access_enable` callback that gates
MDS access to the secured/connected gateway link (`CONFIG_BT_SMP=y` is required).
- **Heartbeat-on-connect:** in `security_changed`, once the link is secured, call
`memfault_metrics_heartbeat_debug_trigger()` once so the device shows up in Memfault
immediately instead of waiting for the periodic timer.
MDS access to the first connected gateway link (tracked via `mds_conn` in `connected()`,
since there is no security level to check without `CONFIG_BT_SMP`).
- **Heartbeat-on-connect:** in `connected()`, once the gateway link is captured as
`mds_conn`, call `memfault_metrics_heartbeat_debug_trigger()` once so the device shows up
in Memfault immediately instead of waiting for the periodic timer.
- **Crash button (demo-only):** map an LBS button to a forced fault (e.g. `k_oops`) to
demonstrate a coredump. Comment it clearly as demo-only.
- Keep the LBS LED/button behavior so it remains a recognizable LBS device for the guide —
Expand Down
7 changes: 5 additions & 2 deletions app/prj.conf
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_SHELL=y
# MDS access control needs a secured link
CONFIG_BT_SMP=y
# Encryption/pairing/bonding disabled for this sample. MDS access is instead
# gated to the first connected gateway link (see mds_access_enable() in
# main.c). A real product handling sensitive data should set CONFIG_BT_SMP=y
# and use bonding (CONFIG_BT_BONDABLE=y, the default) instead.
CONFIG_BT_SMP=n
CONFIG_BT_DEVICE_NAME="Quickstart_Bluetooth"
CONFIG_NCS_APPLICATION_BOOT_BANNER_STRING="Quickstart Bluetooth"
CONFIG_BT_LBS=y
Expand Down
97 changes: 18 additions & 79 deletions app/src/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@
* quickstart-bluetooth application.
*
* The peripheral_lbs button/LED base with Memfault observability over the
* Memfault Diagnostic Service (MDS). The device serves Memfault chunks to a
* secured gateway connection; the gateway performs the HTTPS upload. The device
* never does on-device HTTP/TLS.
* Memfault Diagnostic Service (MDS). The device serves Memfault chunks to the
* first connected gateway; the gateway performs the HTTPS upload. The device
* never does on-device HTTP/TLS. No pairing/encryption (CONFIG_BT_SMP=n). A
* real product handling sensitive data should enable BT_SMP and bonding.
*
* Controls (nRF54L15 DK):
* Button 0 LBS button characteristic (standard LBS — central sees the press)
Expand Down Expand Up @@ -107,6 +108,17 @@ static void connected(struct bt_conn *conn, uint8_t err)

printk("Connected\n");
dk_set_led_on(CON_STATUS_LED);

/* No pairing/encryption in this sample (CONFIG_BT_SMP=n). The first
* connected central is treated as the MDS-authorized gateway. Capture a
* heartbeat immediately so the device (with its software/hardware
* version and serial) appears in Memfault within seconds instead of
* waiting for the periodic heartbeat timer.
*/
if (!mds_conn) {
mds_conn = conn;
memfault_metrics_heartbeat_debug_trigger();
}
}

static void disconnected(struct bt_conn *conn, uint8_t reason)
Expand All @@ -120,31 +132,6 @@ static void disconnected(struct bt_conn *conn, uint8_t reason)
}
}

static void security_changed(struct bt_conn *conn, bt_security_t level, enum bt_security_err err)
{
char addr[BT_ADDR_LE_STR_LEN];

bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));

if (err) {
printk("Security failed: %s level %u err %d %s\n", addr, level, err,
bt_security_err_to_str(err));
return;
}

printk("Security changed: %s level %u\n", addr, level);

/* Once the gateway link is secured, mark it as the MDS-authorized
* connection and capture a heartbeat immediately so the device (with its
* software/hardware version and serial) appears in Memfault within
* seconds instead of waiting for the periodic heartbeat timer.
*/
if (level >= BT_SECURITY_L2 && !mds_conn) {
mds_conn = conn;
memfault_metrics_heartbeat_debug_trigger();
}
}

static void recycled_cb(void)
{
advertising_start();
Expand All @@ -153,46 +140,10 @@ static void recycled_cb(void)
BT_CONN_CB_DEFINE(conn_callbacks) = {
.connected = connected,
.disconnected = disconnected,
.security_changed = security_changed,
.recycled = recycled_cb,
};

static void pairing_complete(struct bt_conn *conn, bool bonded)
{
char addr[BT_ADDR_LE_STR_LEN];

bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
printk("Pairing completed: %s, bonded: %d\n", addr, bonded);
}

static void pairing_failed(struct bt_conn *conn, enum bt_security_err reason)
{
char addr[BT_ADDR_LE_STR_LEN];

bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
printk("Pairing failed conn: %s, reason %d %s\n", addr, reason,
bt_security_err_to_str(reason));
}

static void auth_cancel(struct bt_conn *conn)
{
char addr[BT_ADDR_LE_STR_LEN];

bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
printk("Pairing cancelled: %s\n", addr);
}

/* Just-works pairing (no passkey entry) — the gateway initiates security. */
static struct bt_conn_auth_cb conn_auth_callbacks = {
.cancel = auth_cancel,
};

static struct bt_conn_auth_info_cb conn_auth_info_callbacks = {
.pairing_complete = pairing_complete,
.pairing_failed = pairing_failed,
};

/* Gate MDS access to the secured gateway connection only. */
/* Gate MDS access to the first connected gateway link only. */
static bool mds_access_enable(struct bt_conn *conn)
{
return (mds_conn && conn == mds_conn);
Expand Down Expand Up @@ -264,18 +215,6 @@ int main(void)
return 0;
}

err = bt_conn_auth_cb_register(&conn_auth_callbacks);
if (err) {
printk("Failed to register authorization callbacks (err %d)\n", err);
return 0;
}

err = bt_conn_auth_info_cb_register(&conn_auth_info_callbacks);
if (err) {
printk("Failed to register authorization info callbacks (err %d)\n", err);
return 0;
}

err = bt_enable(NULL);
if (err) {
printk("Bluetooth init failed (err %d)\n", err);
Expand All @@ -285,8 +224,8 @@ int main(void)
printk("Bluetooth initialized\n");

#if defined(CONFIG_SETTINGS)
/* Loads the stored Memfault project key (memfault/project_key) and BT
* bonds. The runtime key is applied here at boot, not live.
/* Loads the stored Memfault project key (memfault/project_key). The
* runtime key is applied here at boot, not live.
*/
settings_load();

Expand Down
16 changes: 8 additions & 8 deletions test/gateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
A small Node.js gateway that acts as the phone/desktop side of the Memfault
Diagnostic Service (MDS) path, so you can verify the `quickstart-bluetooth`
firmware end-to-end **without the mobile app**. It connects to the DK over
Bluetooth Low Energy, secures the link, drains Memfault chunks from the MDS
data-export characteristic, and (optionally) forwards them to the Memfault
cloud — exactly what the *nRF Connect for Desktop Quick Start* gateway does.
Bluetooth Low Energy, drains Memfault chunks from the MDS data-export
characteristic, and (optionally) forwards them to the Memfault cloud —
exactly what the *nRF Connect for Desktop Quick Start* gateway does.

It is a Node/[`@abandonware/noble`](https://github.com/abandonware/noble) port of
Memfault's Web Bluetooth example, <https://github.com/memfault/web-ble-example>.
Expand All @@ -16,8 +16,8 @@ Memfault's Web Bluetooth example, <https://github.com/memfault/web-ble-example>.
- **Node.js 18+** (uses the built-in `fetch`).
- A `nrf54l15dk` flashed with this firmware and advertising as `Quickstart_Bluetooth`.
- On first run, macOS will ask to grant your terminal **Bluetooth** permission
(System Settings → Privacy & Security → Bluetooth). The just-works pairing the
firmware uses is handled automatically by CoreBluetooth — no manual dialog.
(System Settings → Privacy & Security → Bluetooth). The firmware does not use
pairing/encryption, so there's no pairing dialog to handle.

## Install

Expand All @@ -37,8 +37,8 @@ npm run discover # or: node discover.js

## Run the gateway

**Dry run (default) — BLE only, no upload.** Connects, secures, and hexdumps the
drained chunks so you can confirm streaming works without touching the cloud:
**Dry run (default) — BLE only, no upload.** Connects and hexdumps the drained
chunks so you can confirm streaming works without touching the cloud:

```sh
npm run gateway # or: node gateway.js
Expand All @@ -62,7 +62,7 @@ node gateway.js --upload

## Exercising a coredump

1. Run the gateway once so the device is known/paired.
1. Run the gateway once to confirm connectivity.
2. Press **Button 2** on the DK (the demo crash → `k_oops`; on a `0`-labelled DK
silkscreen this is the button labelled **`1`**). The device captures a
RAM-backed coredump and reboots. Don't power-cycle — a RAM-backed coredump
Expand Down
5 changes: 2 additions & 3 deletions test/gateway/gateway.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* Node port of memfault/web-ble-example's mds.js, using @abandonware/noble.
* Upstream reference: https://github.com/memfault/web-ble-example
*
* Default: BLE-only dry run (connect, secure, drain chunks, hexdump) — NO upload.
* Default: BLE-only dry run (connect, drain chunks, hexdump) — NO upload.
* Pass --upload to actually POST chunks to the Memfault data URI read from the device.
*
* Usage: node gateway.js [--upload] [--name Quickstart_Bluetooth] [--seconds 30]
Expand Down Expand Up @@ -126,8 +126,7 @@ async function run(peripheral) {
);
const byUuid = Object.fromEntries(characteristics.map((c) => [c.uuid, c]));

// First encrypted read triggers CoreBluetooth pairing/security.
log("Reading supported features (this triggers pairing if not yet secured)…");
log("Reading supported features…");
const feat = await byUuid[CHAR.supportedFeatures].readAsync();
log(` SupportedFeatures: 0x${feat[0].toString(16)}`);

Expand Down