Transport Layer
Control Messages
Low Power
Node Support
Friendship
Protocol
What is the Friend Feature?
In a Bluetooth Mesh network, most nodes are always on — they relay messages, listen continuously, and forward traffic. But some devices run on small batteries: sensors, switches, door locks. These are called Low Power Nodes (LPN).
An LPN cannot afford to stay awake all the time. So it finds a helper — a Friend node — that stays awake on its behalf. The Friend node stores messages for the LPN while it sleeps. When the LPN wakes up, it polls the Friend and collects its messages.
This relationship between an LPN and a Friend node is called a Friendship. The messages that establish, maintain, and tear down this friendship are called Transport Control messages — and that is exactly what this tutorial covers.
Keywords
Before diving into individual messages, here is how the overall friendship process flows:
Friend Request
picks best Friend
periodically
(group addresses)
between polls
sends Friend Offer
starts queuing messages
queued messages
on behalf of LPN
stores messages
Think of the Friend node as a post office for the LPN. The LPN checks in periodically to collect its mail, then goes back to sleep.
When a Friend node hears a Friend Request from an LPN, it replies with a Friend Offer message. This message tells the LPN what the Friend node can offer — how long it will keep a receive window open, how many messages it can queue, and how many subscription addresses it can track.
| Field | Size (bytes) | What it means |
|---|---|---|
| ReceiveWindow | 1 | How long (in ms) the Friend node keeps its radio window open after an LPN poll. Range: 1–255 ms. |
| QueueSize | 1 | Number of messages the Friend can store for the LPN while it sleeps. |
| SubscriptionListSize | 1 | How many group/virtual addresses the Friend node can track for the LPN. |
| RSSI | 1 | Signal strength (dBm) measured by the Friend node when it received the Friend Request. Signed 8-bit. 0x7F means “not available”. |
| FriendCounter | 2 | Running count of how many Friend Offer messages this Friend node has ever sent (used for deduplication). |
Key rules:
- TTL must be set to 0 — this message must not be relayed.
- Must use master security credentials.
- LPN picks the best offer based on RSSI and capabilities.
ReceiveWindow visualised:
Within ReceiveWindow, the LPN keeps its radio on and listens for the Friend’s response.
What happens when an LPN wants to switch to a new Friend? Or when it establishes a completely new friendship? The new Friend node sends a Friend Clear message to the old Friend node to terminate the previous friendship.
Has new relationship
with LPN
Removes LPN from
its queue
| Message | Field | Size | Purpose |
|---|---|---|---|
| Friend Clear (0x05) |
LPNAddress | 2 | Unicast address of the LPN being removed from old friendship |
| LPNCounter | 2 | Counter value from the new Friend Request (proves freshness) | |
| Friend Clear Confirm (0x06) |
LPNAddress | 2 | Same unicast address echoed back |
| LPNCounter | 2 | Same LPNCounter echoed back |
An LPN can be part of groups — for example, “all lights in room 3” or “all temperature sensors on floor 2”. These are represented as group addresses and virtual addresses.
Since the LPN sleeps, the Friend node must know which group addresses to watch for and store messages from. The LPN tells the Friend node its subscriptions using three messages:
Adds group/virtual addresses
Removes group/virtual addresses
Acknowledges Add or Remove
| Field | Size | Description |
|---|---|---|
| TransactionNumber | 1 | Identifies each transaction. The Confirm echoes this to match request and response. |
| AddressList | 2 × N | List of group/virtual addresses to add or remove. N = number of addresses. |
The Heartbeat message is sent by any mesh node to help other nodes figure out the network topology. It is not specific to the Friend feature, but it is part of the same Transport Control message layer.
By counting hops taken for a Heartbeat to arrive, a node can determine how many relay hops away the sender is. This is used to configure TTL values, identify unreachable nodes, and debug mesh connectivity.
| Opcode | Message | Direction | Security |
|---|---|---|---|
0x03 |
Friend Request | LPN → All Friends | Master |
0x04 |
Friend Offer | Friend → LPN | Master |
0x05 |
Friend Clear | New Friend → Old Friend | Master |
0x06 |
Friend Clear Confirm | Old Friend → New Friend | Master |
0x07 |
Subscription List Add | LPN → Friend | Friendship |
0x08 |
Subscription List Remove | LPN → Friend | Friendship |
0x09 |
Subscription List Confirm | Friend → LPN | Friendship |
0x0A |
Heartbeat | Any → Any | Master |
BlueZ includes bluetooth-meshd, the Linux Bluetooth Mesh daemon. You can experiment with mesh nodes, including the Friend feature, using the mesh-cfgclient and meshctl tools.
Step 1 — Check if meshd is running:
sudo systemctl status bluetooth-meshd
# or start it manually:
sudo /usr/libexec/bluetooth/bluetooth-meshd --nodetach -d
Step 2 — Launch meshctl (interactive mesh shell):
meshctl
Step 3 — Create or join a mesh network:
[meshctl]# create
# This provisions the local node and creates a new network
# You will get a UUID for the node
Step 4 — Enable the Friend feature on a node via config client:
[meshctl]# menu config
[config]# target 0100
# 0100 is the unicast address of the node
# Set Friend feature ON (bit 1 of feature field)
[config]# set-friend on
Step 5 — Configure heartbeat publication (to monitor topology):
[config]# hb-pub set 0100 FFFF 0A 0A 0 0
# Arguments: dst count period TTL netkey-index features
# FFFF = all-nodes address
# TTL 0A = 10 hops max
Step 6 — Inspect raw mesh network traffic with btmon:
sudo btmon
# Look for Network PDU events in the output
# You can filter for opcodes 0x03 through 0x09 in transport control
Step 7 — Python snippet to inspect mesh config via D-Bus:
import dbus
bus = dbus.SystemBus()
mesh_obj = bus.get_object("org.bluez.mesh", "/org/bluez/mesh")
mgr = dbus.Interface(mesh_obj, "org.freedesktop.DBus.ObjectManager")
objects = mgr.GetManagedObjects()
for path, interfaces in objects.items():
if "org.bluez.mesh.Node1" in interfaces:
props = interfaces["org.bluez.mesh.Node1"]
print(f"Node: {path}")
print(f" Features: {props.get('Features', 'N/A')}")
# Features includes friend, relay, proxy flags
Features property in BlueZ mesh D-Bus API returns a dict with keys like friend, relay, proxy, lowPower — each a boolean. This maps directly to the Feature field in the mesh Heartbeat message.Step 8 — Monitor friend establishment in meshd debug logs:
sudo /usr/libexec/bluetooth/bluetooth-meshd --nodetach -d 2>&1 | grep -i friend
# Look for lines like:
# "Friend Request received from 00AA"
# "Sending Friend Offer to 00AA"
# "Friendship established with LPN 00AA"
Q: Why does Friend Offer include RSSI?
The LPN uses RSSI as one criterion to pick the best Friend. A closer Friend with stronger signal is preferred — fewer retransmissions, more reliable message delivery.
Q: Why is TTL=0 used for friendship messages?
Friendship is a direct, one-hop relationship. These messages should not be relayed across the network — they are private between LPN and Friend node.
Q: What security credentials does Subscription List Add use?
Friendship security credentials — not master credentials. This isolates LPN↔Friend communication from the rest of the network.
Q: What is the retry strategy for Friend Clear?
Exponential backoff: retries at 2s, 4s, 8s, 16s … until a Friend Clear Confirm is received or the LPN’s Poll Timeout expires.
Q: What is the max addresses in one Subscription List Add?
For an unsegmented control message: 5 addresses. More addresses require a segmented message.
Q: What does Heartbeat TTL tell the receiver?
The initial TTL minus received TTL = number of hops taken. Receivers use this to map mesh topology and configure their own TTL for reaching the sender.
Keep Learning Bluetooth Mesh
This post covered Transport Control messages for the Friend feature (Sections 3.6.5.4 – 3.6.5.10 of the Mesh Profile Specification).
Next up: Friend Poll, Friend Update, and the Mesh Security model
