Bluetooth Mesh development in c : Friend & Low Power Node

Bluetooth Mesh development in c : Friend & Low Power Node

How battery-powered mesh nodes save power using the Friend feature — explained simply with BlueZ examples

3
Key Messages
LPN
Low Power Node
BlueZ
Code Examples
§3.6.5
Mesh Spec

What Problem Does the Friend Feature Solve?

In a Bluetooth Mesh network every node must be able to receive messages — but receiving radio constantly drains a battery very fast. A door sensor or a remote switch cannot afford to keep its radio on all day.

The Friend feature solves this. A permanently powered device nearby — called the Friend node — stays awake and collects incoming messages on behalf of the sleepy device. The sleepy device is called the Low Power Node (LPN). The LPN wakes up periodically, asks its Friend for any stored messages, and goes back to sleep.

Think of it like a post box outside your house. Your postman (the Friend) collects letters all day. You (the LPN) open the box only when you want to check — you do not have to stand at the door waiting.

Key Terms in This Post

Low Power Node (LPN) Friend Node Friend Queue Friend Poll (0x01) Friend Update (0x02) Friend Request (0x03) FSN — Friend Sequence Number MD — More Data flag PollTimeout Criteria Field IV Index Friendship Security

🔗 LPN and Friend — Architecture at a Glance

The LPN and its Friend share a special relationship called a friendship. The Friend holds messages in a buffer called the Friend Queue until the LPN comes to collect them.

📱
Low Power Node
Sleeps most of the time.
Wakes briefly to fetch messages.
e.g. Door sensor, Motion detector,
Battery switch

🔋 Long battery life

Friend Request (0x03)

Friend Offer

Friend Poll (0x01)

Friend Update (0x02) + Data
Friendship Security Credentials
TTL = 0 (direct, no relay)
🗄️
Friend Node
Always awake. Stores mesh messages
in its Friend Queue for the LPN.
e.g. Mains-powered relay,
Smart hub, Always-on light
⚡ Mains powered

📋 The Full Message Sequence

Here is the step-by-step conversation between an LPN and its Friend — from initial discovery right through to fetching stored messages on every wake-up cycle.

Low Power Node (LPN)
Friend Node
① Wakes up. No friend yet.
Friend Request
Receives broadcast from LPN
Receives offer(s), picks the best Friend
Friend Offer
② Sends Friend Offer back
Friendship established ✓
③ Wakes up again — any messages?
Friend Poll
Checks Friend Queue…
Gets network status + knows if more messages are waiting
Friend Update + Data
④ Delivers queued messages
↩ LPN goes back to sleep

Steps ③ and ④ repeat on every LPN wake-up cycle for as long as the friendship lasts.

📨 Message 1 — Friend Poll (Opcode 0x01)

The LPN sends this every time it wakes up. It is essentially saying: “Hey Friend, send me the next stored message.”

This is the smallest control message in the mesh spec — just one byte, with only two fields.

Packet Layout (8 bits = 1 byte)

7 6 5 4 3 2 1 0
Padding — 7 bits, always 0b0000000 FSN
Reserved — no information here 0 or 1

How FSN Works — The Ping-Pong Bit

Step LPN sends FSN What it means
First Poll ever FSN = 0 Please send me the first message
After receiving message #1 FSN = 1 Got it! Send me the next one
After receiving message #2 FSN = 0 Got it! Send me the next one
If FSN does NOT change Same FSN Friend knows: last message was lost — re-send it
💡 Why TTL = 0?
Both Friend Poll and Friend Update carry TTL set to zero. This means the message travels only one hop — directly between the LPN and its Friend. It must never be relayed across the mesh network.

BlueZ — Monitoring Friend Poll with btmon

# Run btmon to capture all Bluetooth HCI traffic to a file
sudo btmon -w mesh_capture.btsnoop

# In a second terminal — start the BlueZ mesh daemon
sudo mesh-cfgd &

# Live view: filter for Transport Control messages (opcode 0x01 = Friend Poll)
sudo btmon | grep -A 8 "Transport Control"

BlueZ — Building a Friend Poll Message in C

/* friend_poll.c
 *
 * Friend Poll packet: 1 byte
 *   Bits [7:1]  Padding  — must be 0b0000000
 *   Bit  [0]    FSN      — toggles between 0 and 1
 *
 * Opcode 0x01 lives in the Transport Control header,
 * not inside this payload.
 */

#include <stdint.h>
#include <stdio.h>

#define FRIEND_POLL_OPCODE  0x01

/* Build a Friend Poll byte.
 * fsn must be 0 or 1 only.
 */
static uint8_t build_friend_poll(uint8_t fsn)
{
    return (uint8_t)(fsn & 0x01);   /* upper 7 bits stay 0 */
}

int main(void)
{
    uint8_t fsn = 0;

    /* Simulate a few poll cycles */
    for (int i = 0; i < 4; i++) {
        uint8_t poll_byte = build_friend_poll(fsn);
        printf("Poll #%d  FSN=%u  byte=0x%02X\n", i + 1, fsn, poll_byte);
        fsn ^= 1;   /* toggle FSN after each acknowledged message */
    }

    return 0;
}
# Compile with BlueZ headers available
gcc -o friend_poll friend_poll.c

# Expected output:
# Poll #1  FSN=0  byte=0x00
# Poll #2  FSN=1  byte=0x01
# Poll #3  FSN=0  byte=0x00
# Poll #4  FSN=1  byte=0x01

📬 Message 2 — Friend Update (Opcode 0x02)

The Friend node sends this in response to a Friend Poll. It does two jobs in one message:

  • Keeps the LPN in sync with the current network security state (IV Index, Key Refresh phase)
  • Tells the LPN whether more messages are still waiting in the Friend Queue

Total size: 6 bytes.

Packet Layout

Flags
1 byte
IV Index
4 bytes
MD
1 byte
Key Refresh Flag (bit 0)
IV Update Flag (bit 1)
Bits 2–7: Reserved
Current IV Index
used by this network
0 = Queue empty
1 = More messages

The Flags Byte — Bit by Bit

7 6 5 4 3 2 1 0
Reserved for Future Use — always 0 IV Update Flag Key Refresh Flag

Flag Value = 0 Value = 1
Key Refresh Flag
bit 0
No key refresh happening — use current Network Key Key Refresh Phase 2 active — switch to the new Network Key now
IV Update Flag
bit 1
Normal operation IV Update in progress — network is bumping the IV Index counter
MD (More Data)
separate byte
Friend Queue is empty — LPN can go back to sleep More messages waiting — LPN should send another Friend Poll
💡 What is the IV Index?
Every mesh message is encrypted using a sequence number plus the IV Index. When sequence numbers are about to run out (after 2²⁴ messages), the whole network increments the IV Index together. The Friend Update keeps the LPN in sync with this counter even while it was sleeping.

BlueZ — Parsing a Friend Update in C

/* friend_update.c
 *
 * Friend Update payload: 6 bytes
 *   Byte 0     : Flags  (bit0=KeyRefresh, bit1=IVUpdate, bits2-7=RFU)
 *   Bytes 1–4  : IV Index (big-endian, most significant byte first)
 *   Byte 5     : MD  (0=queue empty, 1=more data)
 */

#include <stdint.h>
#include <stdio.h>
#include <arpa/inet.h>  /* ntohl() — available via libc on Linux */

#define FRIEND_UPDATE_OPCODE  0x02

#define FLAG_KEY_REFRESH  (1u << 0)
#define FLAG_IV_UPDATE    (1u << 1)

void parse_friend_update(const uint8_t *buf, size_t len)
{
    if (len < 6) {
        fprintf(stderr, "Too short for Friend Update\n");
        return;
    }

    uint8_t  flags    = buf[0];
    /* IV Index is 4 bytes big-endian starting at buf[1] */
    uint32_t iv_index = ((uint32_t)buf[1] << 24)
                      | ((uint32_t)buf[2] << 16)
                      | ((uint32_t)buf[3] <<  8)
                      |  (uint32_t)buf[4];
    uint8_t  md       = buf[5];

    printf("=== Friend Update ===\n");
    printf("Key Refresh active : %s\n",
           (flags & FLAG_KEY_REFRESH) ? "YES — switch to new key" : "NO");
    printf("IV Update active   : %s\n",
           (flags & FLAG_IV_UPDATE)   ? "YES — IV bump in progress" : "NO");
    printf("IV Index           : 0x%08X\n", iv_index);
    printf("Friend Queue       : %s\n",
           md == 0 ? "EMPTY — go to sleep" : "MORE DATA — send another Poll");
}

int main(void)
{
    /* Example: flags=0x00, IV Index=0x00000064, MD=0x01 */
    uint8_t example[] = { 0x00, 0x00, 0x00, 0x00, 0x64, 0x01 };
    parse_friend_update(example, sizeof(example));
    return 0;
}
# Compile
gcc -o friend_update friend_update.c

# Expected output:
# === Friend Update ===
# Key Refresh active : NO
# IV Update active   : NO
# IV Index           : 0x00000064
# Friend Queue       : MORE DATA — send another Poll

🔍 Message 3 — Friend Request (Opcode 0x03)

Before any polling can happen the LPN must first find a Friend. It does this by broadcasting a Friend Request to all nearby Friend-capable nodes at once. Each willing Friend calculates how suitable it is, waits a short delay, then replies with a Friend Offer. The LPN picks the best offer.

Total size: 10 bytes.

Packet Layout

Criteria
1 byte
ReceiveDelay
1 byte
PollTimeout
3 bytes
PreviousAddress
2 bytes
NumElements
1 byte
LPNCounter
2 bytes
What the LPN
needs from a Friend
Wait time after
waking (ms)
Max gap between
polls (×100ms)
Old Friend’s address
or 0x0000
Number of
LPN elements
Count of Friend
Requests sent

The Criteria Byte — Zoomed In

This single byte tells potential Friend nodes what the LPN requires. A Friend that cannot meet these requirements should not reply.

7
RFU
6 — 5
RSSIFactor
4 — 3
ReceiveWindowFactor
2 — 1 — 0
MinQueueSizeLog
Reserved
= 0
Weight for RSSI
in Friend selection
Weight for receive
window in selection
Min Friend Queue size
= log₂(N) messages

RSSIFactor values
2-bit value Factor
0b00 1
0b01 1.5
0b10 2
0b11 2.5
MinQueueSizeLog values
3-bit value Min slots (N)
0b000 Prohibited
0b001 2
0b010 4
0b011 8
0b100 16
0b101 32
0b110 64
0b111 128

All Fields Explained Simply

Field Valid Range What it does
RSSIFactor 1, 1.5, 2, 2.5 Controls how much the Friend’s signal strength affects Friend Offer delay. Higher = prefer closer Friends.
ReceiveWindowFactor 1, 1.5, 2, 2.5 Controls how much the Friend’s receive window size affects the delay. Higher = prefer Friends with longer listening windows.
MinQueueSizeLog 0b001 to 0b111 Minimum Friend Queue depth the LPN needs. Value 3 means at least 8 message slots (log₂(8)=3).
ReceiveDelay 10–255 ms How long the LPN waits after sending a Friend Poll before opening its radio to receive. Must be at least 10 ms.
PollTimeout 1 sec – ~35 min Maximum time between Friend Polls. If the LPN misses this deadline, the Friend terminates the friendship.
PreviousAddress Any unicast or 0x0000 Unicast address of the last Friend this LPN was paired with. The old Friend clears its queue for this LPN when it sees this. Use 0x0000 on first boot.
NumElements 1–255 Number of elements on this LPN. The Friend uses this to figure out the full range of unicast addresses the LPN occupies.
LPNCounter 0–65535 Running count of Friend Requests this LPN has ever sent. Lets Friend nodes distinguish a new request from a stale re-broadcast.

BlueZ — Building a Friend Request in C

/* friend_request.c
 *
 * Friend Request payload: 10 bytes
 *   Criteria      1 byte  (RFU[7], RSSIFactor[6:5], RWFactor[4:3], MinQueueSizeLog[2:0])
 *   ReceiveDelay  1 byte  (min 0x0A = 10ms)
 *   PollTimeout   3 bytes (big-endian, unit = 100ms, max 0x34BBFF)
 *   PrevAddress   2 bytes (little-endian, 0x0000 if no previous friend)
 *   NumElements   1 byte
 *   LPNCounter    2 bytes (little-endian)
 */

#include <stdint.h>
#include <string.h>
#include <stdio.h>

#define FRIEND_REQUEST_OPCODE  0x03

/* Criteria field bit packing */
#define CRITERIA_RSSI_FACTOR(x)    (((x) & 0x03u) << 5)
#define CRITERIA_RW_FACTOR(x)      (((x) & 0x03u) << 3)
#define CRITERIA_MIN_QUEUE_LOG(x)  ( (x) & 0x07u)

typedef struct {
    uint8_t  criteria;
    uint8_t  receive_delay;
    uint8_t  poll_timeout[3];   /* 24-bit, big-endian */
    uint8_t  prev_address[2];   /* 16-bit, little-endian */
    uint8_t  num_elements;
    uint8_t  lpn_counter[2];    /* 16-bit, little-endian */
} __attribute__((packed)) friend_request_t;

void build_friend_request(friend_request_t *req,
                          uint16_t poll_timeout_100ms,
                          uint16_t prev_addr,
                          uint8_t  num_elements,
                          uint16_t lpn_counter)
{
    memset(req, 0, sizeof(*req));

    /*
     * Criteria:
     *   RSSIFactor         = 0b01  (factor 1.5 — moderate RSSI weight)
     *   ReceiveWindowFactor= 0b01  (factor 1.5 — moderate window weight)
     *   MinQueueSizeLog    = 0b011 (N=8 message slots minimum)
     */
    req->criteria = CRITERIA_RSSI_FACTOR(1)
                  | CRITERIA_RW_FACTOR(1)
                  | CRITERIA_MIN_QUEUE_LOG(3);

    req->receive_delay = 0x0A;          /* 10ms — minimum allowed */

    /* PollTimeout: big-endian 24-bit */
    req->poll_timeout[0] = (poll_timeout_100ms >> 16) & 0xFF;
    req->poll_timeout[1] = (poll_timeout_100ms >>  8) & 0xFF;
    req->poll_timeout[2] =  poll_timeout_100ms        & 0xFF;

    /* PreviousAddress: little-endian */
    req->prev_address[0] =  prev_addr        & 0xFF;
    req->prev_address[1] = (prev_addr >> 8)  & 0xFF;

    req->num_elements = num_elements;

    /* LPNCounter: little-endian */
    req->lpn_counter[0] =  lpn_counter        & 0xFF;
    req->lpn_counter[1] = (lpn_counter >> 8)  & 0xFF;
}

int main(void)
{
    friend_request_t req;

    /*
     * Poll timeout = 100 units × 100ms = 10 seconds
     * No previous friend (0x0000)
     * 1 element
     * First ever request (counter = 1)
     */
    build_friend_request(&req, 100, 0x0000, 1, 1);

    printf("Criteria byte    : 0x%02X\n", req.criteria);
    printf("Receive Delay    : %d ms\n",  req.receive_delay);
    printf("Poll Timeout     : %d00 ms\n", req.poll_timeout[2]);
    printf("Prev Address     : 0x%02X%02X\n",
           req.prev_address[1], req.prev_address[0]);
    printf("Num Elements     : %d\n",     req.num_elements);
    printf("LPN Counter      : %d\n",
           req.lpn_counter[0] | (req.lpn_counter[1] << 8));

    return 0;
}
# Install BlueZ development headers (Ubuntu / Debian)
sudo apt-get install libbluetooth-dev bluetooth bluez

# Compile
gcc -o friend_request friend_request.c

# Run
./friend_request

# Expected output:
# Criteria byte    : 0x4B
# Receive Delay    : 10 ms
# Poll Timeout     : 10000 ms
# Prev Address     : 0x0000
# Num Elements     : 1
# LPN Counter      : 1

✅ Quick Reference — All Three Messages
Message Opcode Size Direction One-line purpose
Friend Request 0x03 10 bytes LPN → All Friends (broadcast) “I need a Friend — here are my requirements”
Friend Poll 0x01 1 byte LPN → Friend (unicast) “Any messages for me? Acknowledge my last one with FSN”
Friend Update 0x02 6 bytes Friend → LPN (unicast) “Here is the network state. MD tells you if the queue is empty”

Key rules to remember:

  • All three messages use friendship security credentials — they are not encrypted with the regular network key.
  • Friend Poll and Friend Update always set TTL = 0 — they never relay through the mesh.
  • The LPN keeps polling as long as MD = 1 (more data). It stops and sleeps only when MD = 0 (queue empty).

Next Up: Friend Offer & Friendship Establishment

Now that you know how the LPN asks for a Friend and how they exchange messages, the next post covers how Friend nodes calculate their Friend Offer Delay and how the complete establishment handshake works end to end.

Leave a Reply

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