Bluetooth Mesh tutorial : Low Power Node & Friend Feature Explained

Bluetooth Mesh tutorial : Low Power Node & Friend Feature Explained
How battery-powered mesh devices sleep and still receive messages — with BlueZ examples
⏱️ 12 min read
📡 Mesh Profile §3.6.6
🔋 LPN / Friend

What Problem Does This Solve?

In a Bluetooth Mesh network, every node normally listens to the radio constantly — which drains a battery fast. A temperature sensor on a coin cell can’t afford to do that. So the spec defines two special roles: the Low Power Node (LPN) and the Friend Node.

The LPN sleeps most of the time. The Friend stays awake, collects all messages meant for the LPN, and hands them over when the LPN wakes up and asks. Think of the Friend as a mailbox and the LPN as someone who only checks their mail once in a while.

Key Terms in This Post

Low Power Node (LPN) Friend Node Friendship Friend Poll Friend Update Friend Request / Offer PollTimeout ReceiveDelay / ReceiveWindow LPNCounter Friend Queue FSN (Friend Sequence Number) TTL = 0

1. What Is a Low Power Node?

A Low Power Node (LPN) is a mesh node that cannot afford to keep its radio on all the time. Examples include door sensors, occupancy detectors, and environmental sensors running on small batteries.

The spec says an LPN must support three operations:

  • Low Power Establishment — finding and pairing with a Friend
  • Low Power Messaging — polling the Friend for queued messages
  • Low Power Management — telling the Friend which group addresses to track

All control messages from an LPN go out as Unsegmented Control messages with TTL = 0, meaning they don’t relay through other nodes — they’re point-to-point with the Friend.

2. How Friendship is Established (LPN ↔ Friend)

Before the LPN can sleep and still receive messages, it needs to “pair” with a nearby Friend node. This pairing is called friendship establishment. Here’s the step-by-step:

Low Power Node Friend Node(s)
Send Friend Request
TTL=0, DST=all-friends
includes LPNCounter
Receives Request
Multiple Friends may reply
Wait 100ms, then listen
up to 1 second
Send Friend Offer
Select best Friend,
send Friend Poll
FSN=0, within 1s
Receives Friend Poll
Receives Friend Update
✅ Friendship Established!
Send Friend Update
After ReceiveDelay ms

A few important details from the spec worth noting:

  • The LPN keeps a counter called LPNCounter (2 bytes, starts at 0). It goes into every Friend Request and is used to derive the security keys for that friendship. It increments after each request.
  • If no Friend Offer arrives, the LPN can try again — but it must wait at least 1.1 seconds between consecutive Friend Requests.
  • If the LPN tries ~6 times with no success, it may signal a deeper problem: an invalid IV Index, which triggers the IV Index Recovery procedure.

BlueZ: Checking LPN / Friend Support

BlueZ exposes mesh node features via D-Bus. You can query or configure Low Power Node behaviour through the org.bluez.mesh.Node1 interface. Below is a quick reference using gdbus and the BlueZ mesh daemon (bluetooth-meshd).

Check if meshd is running:

# Start the BlueZ mesh daemon
sudo bluetooth-meshd --nodetach --debug

# In another terminal, check D-Bus mesh objects
gdbus introspect --system \
  --dest org.bluez.mesh \
  --object-path /org/bluez/mesh

Enable Low Power Feature when creating a node (mesh-cfgclient example):

/* When provisioning a node, set its features bitmask.
 * Bit 2 = Low Power feature
 * Bit 1 = Friend feature
 * These are set in the node's composition data (Page 0).
 */

/* Example: read composition data page 0 via config client */
mesh-cfgclient
[mesh-cfgclient]# connect
[mesh-cfgclient]# get-composition 0x0100 0

Friend Poll timer — conceptual flow in C (BlueZ mesh source style):

/*
 * Simplified version of how BlueZ mesh handles
 * the PollTimeout for a Low Power Node.
 * Source reference: mesh/friend.c in BlueZ source
 */

#include <stdint.h>
#include <stdbool.h>

#define POLL_TIMEOUT_MS   10000   /* 10 seconds example */
#define RECEIVE_DELAY_MS  100
#define RECEIVE_WINDOW_MS 250

struct lpn_state {
    uint16_t lpn_counter;      /* LPNCounter, 2 bytes */
    uint8_t  fsn;              /* Friend Sequence Number (0 or 1) */
    bool     friendship_active;
    uint32_t poll_timeout_ms;
};

/* Called when LPN wakes up to request queued messages */
void lpn_send_friend_poll(struct lpn_state *lpn)
{
    /* FSN field reflects current Friend Sequence Number */
    uint8_t fsn = lpn->fsn;

    /* Build and send Friend Poll with TTL=0 */
    /* mesh_send_friend_poll(dst_friend_addr, fsn); */

    /* Wait for Friend Update/message within ReceiveWindow */
    /* If no reply: retry up to 3 times */
}

/* Toggle FSN after receiving a non-duplicate response */
void lpn_on_valid_response(struct lpn_state *lpn)
{
    lpn->fsn ^= 1;  /* Toggle between 0 and 1 */
}

/* Increment LPNCounter before each Friend Request */
void lpn_send_friend_request(struct lpn_state *lpn)
{
    uint16_t counter = lpn->lpn_counter;
    /* mesh_send_friend_request(counter); */
    lpn->lpn_counter++;  /* wraps at 0xFFFF -> 0x0000 */
}

3. How the LPN Collects Messages (Low Power Messaging)

Once friendship is established, the LPN doesn’t passively listen — it actively asks. This is the Friend Poll → Friend Update/Message cycle. The LPN wakes up periodically, sends a Friend Poll, and the Friend replies with whatever is queued.

Low Power Node Direction Friend Node (queue)
Friend Poll (FSN=0)
Message 1 (MD=1 → more data)
Toggle FSN → FSN=1
Friend Poll (FSN=1)
Friend Update [IV Index] (MD=1)
Toggle FSN → FSN=0
Friend Poll (FSN=0)
Message 2 (MD=1)
Toggle FSN → FSN=1
Friend Poll (FSN=1)
Queue empty — LPN can sleep 😴 Friend Update [MD=0]
No more data

The MD (More Data) bit in Friend Update is the key signal. As long as MD=1, the LPN keeps polling. When it sees MD=0, the queue is empty and the LPN can go back to sleep.

The FSN (Friend Sequence Number) is a single bit (0 or 1) that toggles on every successful exchange. If the LPN gets a duplicate response (same PDU as last time), it does NOT toggle FSN — this acts as a simple duplicate-detection mechanism.

Timing Parameters You Must Know

Parameter Meaning Who Sets It
ReceiveDelay Minimum ms Friend waits before replying after Friend Poll Negotiated during establishment
ReceiveWindow Time window in which LPN listens for reply after poll Friend node advertises in Offer
PollTimeout Max time Friend keeps the friendship alive without a Poll LPN proposes, Friend accepts
PollInterval How often LPN wakes to poll (must be < PollTimeout) LPN application layer

If the LPN doesn’t poll before PollTimeout expires, the friendship is automatically terminated. The LPN then has to go through establishment again.

4. Subscription Management (Low Power Management)

The Friend node needs to know which group addresses or virtual addresses to watch on behalf of the LPN. Without this, it would miss mesh messages sent to groups the LPN belongs to.

The LPN manages this via two messages:

Message Purpose When Sent
Friend Subscription List Add Tell Friend to start buffering messages for these addresses Any time LPN subscribes to a new group
Friend Subscription List Remove Tell Friend to stop buffering for these addresses Any time LPN unsubscribes
Friend Subscription List Confirm Friend acknowledges the add/remove request Sent by Friend in response

Each Add or Remove message carries a TransactionNumber (starts at 0x00, increments for each new request). The Confirm message echoes this number back so the LPN can verify the Friend processed the right request.

/* Pseudocode: LPN subscription management via BlueZ mesh D-Bus
 * Real API: org.bluez.mesh.Node1 → AddAppKey, etc.
 * For subscription list updates, the mesh app layer sends
 * Friend Subscription List Add/Remove via the transport layer.
 */

struct subscription_msg {
    uint8_t  transaction_number;   /* increments per new request */
    uint16_t addresses[10];        /* group/virtual addresses */
    uint8_t  address_count;
};

void lpn_add_subscription(struct lpn_state *lpn,
                           uint16_t group_addr)
{
    struct subscription_msg msg = {
        .transaction_number = lpn->txn_number++,
        .addresses          = { group_addr },
        .address_count      = 1,
    };

    /* Send Friend Subscription List Add, then wait for Confirm */
    /* mesh_send_friend_sub_add(&msg); */
}

void lpn_remove_subscription(struct lpn_state *lpn,
                              uint16_t group_addr)
{
    struct subscription_msg msg = {
        .transaction_number = lpn->txn_number++,
        .addresses          = { group_addr },
        .address_count      = 1,
    };

    /* Send Friend Subscription List Remove */
    /* mesh_send_friend_sub_remove(&msg); */
}

5. What About Large Messages? (Segmentation & Reassembly)

Normal mesh nodes handle reassembly of segmented messages themselves. For a Low Power Node, this is different — the LPN can’t stay awake long enough to collect all segments in real time.

So the Friend node takes over reassembly. Here’s what happens:

Other Mesh Node Friend Node LPN
Segment 0 Receive Seg 0 😴 Sleeping
Segment 1 Receive Seg 1 😴 Sleeping
Segment ACK → sends to sender
Friend handles this!
😴 Sleeping
Segment 2 (last) Reassembly Complete ✅
Place in Friend Queue
😴 Sleeping
Waiting with full message queued Wakes up
Friend Poll →
Delivers full reassembled message Receives complete message ✅

The Friend node acts as a proxy — it reassembles the segmented message on behalf of the LPN and sends segment acknowledgments back to the original sender. The LPN never deals with individual segments directly; it always gets a complete, reassembled message from its Friend.

6. Ending a Friendship

Friendship can end in two ways:

How What Happens
LPN sends Friend Clear Clean termination. LPN sends with TTL=0 to the Friend.
PollTimeout expires LPN failed to poll in time. Friend drops the friendship automatically.

After friendship ends, if the LPN still needs to receive mesh messages, it must restart the establishment process from scratch — new Friend Request, new LPNCounter value, new security material.

7. Interview Q&A Cheatsheet
Question Answer
Why is TTL set to 0 for LPN messages? LPN ↔ Friend communication is direct/local. Setting TTL=0 prevents the message from being relayed by other nodes.
What is the purpose of the MD bit? MD=1 in Friend Update means the Friend queue has more messages waiting. MD=0 means it is empty and the LPN can stop polling and sleep.
What is the FSN used for? It is a 1-bit toggle in Friend Poll. It helps the Friend detect if a Poll is a retry (same FSN) or a new request (toggled FSN).
Who does segmented message reassembly for an LPN? The Friend node. It reassembles and places the complete message in the Friend Queue, so the LPN always receives whole messages.
What happens if LPN doesn’t poll before PollTimeout? The Friend terminates the friendship. The LPN must re-establish friendship from scratch.
What is LPNCounter and why does it matter? A 2-byte counter included in Friend Request messages. It is used to derive the friendship security keys. It increments after every Friend Request.

Continue Learning Bluetooth Mesh

This post is part of the EmbeddedPathashala Bluetooth Classic & Mesh series.

Leave a Reply

Your email address will not be published. Required fields are marked *