ble mesh programming in c Bluetooth Mesh Transport Layers Explained

ble mesh programming in c Bluetooth Mesh Transport Layers Explained
Lower Transport · Upper Transport · Friend Queue · Segmentation · Encryption
Spec Pages
61 – 64
Stack Layer
Transport
Code
BlueZ Mesh
Level
Intermediate

What Are We Learning Today?

In Bluetooth Mesh, every message passes through several layers before the application ever sees it. Two of those layers — the Lower Transport Layer and the Upper Transport Layer — do the heavy lifting around splitting large messages into smaller pieces, confirming delivery, buffering messages for sleeping devices, and encrypting application data before sending.

This post breaks down exactly what each of these layers does, with diagrams and real BlueZ code to make it stick.

Key Terms in This Post

Lower Transport PDU Upper Transport PDU Segmentation Block Acknowledgment Friend Queue Low Power Node TransMIC AppKey / DevKey SeqAuth Transport Control PDU AKF / AID NetMIC

① Lower Transport Layer — The Parcel Splitter

Imagine you want to courier a large package, but the courier company only accepts boxes up to a fixed size. So you split the package into numbered smaller boxes, send them all, and the courier confirms which ones arrived. That is exactly what the lower transport layer does with mesh messages.

How Segmentation Works

When a message is larger than a single PDU can carry, the lower transport layer cuts it into segments. Each segment is numbered starting from zero. On the receiving side, every arrived segment is ticked off in a Block Acknowledgment (Block ACK) value — one bit per segment. When all bits are set, reassembly is complete and the full message is handed to the upper transport layer.

Sender Network Receiver
Large message (e.g. 40 bytes)
Too big for one PDU
Split into segments:
Seg 0  |  Seg 1  |  Seg 2
→→→ Receives segments
Marks Block ACK bits:
✓ Seg0   ✓ Seg1   ✓ Seg2
✓ All delivered — done! ← Block ACK ← All bits set → Reassemble
↑ Pass to upper transport

Two Flavours of Segmented Messages

Segmented Access Message

Carries real application data — sensor readings, light control commands, etc. Once reassembled, it goes to the upper transport access path.

Segmented Control Message

Carries internal mesh control info — Friend management, heartbeats. Once reassembled, processed by the transport control path.

Receiving a Lower Transport PDU — Decision Tree

PDU Type Received Action Taken
Segmented message or Segment Acknowledgment Goes into the segmentation/reassembly engine
Unsegmented message Sent directly to the upper transport access handler
Transport Control PDU Processed based on the 7-bit Opcode value

BlueZ — Watching Segmentation in Action

Start a BlueZ mesh session and use btmon to see transport PDUs on the wire:

# Terminal 1 — capture Bluetooth traffic
sudo btmon

# Terminal 2 — run the mesh daemon
sudo bluetooth-meshd --nodetach --debug

# Terminal 3 — launch config client
mesh-cfgclient

# Inside mesh-cfgclient, send a Composition Data Get request.
# This triggers an upper-transport access message. If the config
# response is larger than ~11 bytes, the stack segments it
# and you will see multiple HCI_ACL packets in btmon, one per segment.
target 0x0100
get-composition 0x00

In the btmon output, each segmented Lower Transport PDU has the SEG bit set to 1, along with SegO (segment offset) and SegN (last segment number) fields in the header.

② Friend Queue — A Mailbox for Sleeping Nodes

Bluetooth Mesh supports battery-operated devices called Low Power Nodes (LPN). These nodes sleep most of the time to save energy. But while they sleep, other nodes may send them messages. Someone needs to hold those messages until the LPN wakes up. That someone is the Friend node.

Every Friend node keeps a per-LPN buffer called the Friend Queue. Think of it as a private mailbox for each LPN.

Any mesh node

Sends a message destined for the LPN

Friend Node

Intercepts message
Decrements TTL by 1
Stores in Friend Queue

Low Power Node

Sleeping… 😴
Wakes up → Polls Friend
← Gets queued messages

Friend Queue Rules You Must Know

🔒 PDU stored unchanged — The CTL, TTL, SEQ, SRC, and DST fields are all saved alongside the Lower Transport PDU exactly as received. The Friend node does not alter any PDU field during storage.
⏳ TTL gate — A message only enters the Friend Queue if its TTL is 2 or more. The TTL is decremented by 1 before storage so the LPN gets the correct value when it polls.
📃 Segmented messages — wait for full reassembly — If the incoming message is segmented (Segmented Access or Segmented Control), it is only stored in the Friend Queue after the complete Upper Transport PDU has been reassembled AND the Friend node has acknowledged all segments.
🗑 Queue full? Oldest loses — When the queue is full and a new message must enter, the oldest non-Friend-Update entries are dropped first. The implementation may need to drop several old messages at once. Friend Update messages are protected from eviction.
📌 Duplicate Segment ACKs — If a new Segment Acknowledgment arrives that has the same source, destination, and SeqAuth as an existing one already in the queue but carries a lower IV Index or sequence number, the older one is discarded.
🛡 Security update triggers Friend Update — When the Friend node detects a security change (valid Secure Network Beacon received, or Key Refresh Phase changed), it automatically inserts a Friend Update message into the queue.

The Friend Poll / Deliver Cycle

Low Power Node Friend Node
Wakes up, sends Friend Poll Checks its Friend Queue for this LPN
Queue empty? → Generates and enqueues a fresh Friend Update
← Receives oldest queued entry Sends oldest entry from Friend Queue
Sends ACK → Receives ACK → discards delivered entry
Goes back to sleep 😴 Waits for the next Friend Poll

BlueZ — Enabling Friend Feature on a Node

# Provision the node first, then target it
mesh-cfgclient

target 0x0100   # address of the node you want to be a Friend

# Read current features (bit 0 = Relay, bit 1 = Proxy,
#                         bit 2 = Friend, bit 3 = Low Power)
get-node-comp 0x00

# Enable Friend feature — set bit 2 in the feature bitmask
# (use the Set Config Model Publication or mesh-cfgclient commands
#  depending on your BlueZ version)
set-features 0x0004   # bit 2 = Friend feature ON

# Once enabled, bluetooth-meshd will automatically manage
# the Friend Queue for any LPN that establishes a friendship
# with this node. You can watch pairings in the daemon log:
# sudo bluetooth-meshd --nodetach --debug 2>&1 | grep -i friend

BlueZ Source — Friend Queue Logic (Conceptual)

/*
 * BlueZ: mesh/friend.c
 * The friend queue is a linked list of Lower Transport PDUs
 * per LPN. Relevant constants and flow (simplified):
 */

#define FRND_QUEUE_MAX   32   /* max entries per LPN */

struct frnd_queue_entry {
    uint8_t  ctl;       /* CTL bit from Network PDU */
    uint8_t  ttl;       /* TTL already decremented by 1 */
    uint32_t seq;       /* SEQ from Network PDU */
    uint16_t src;       /* SRC address */
    uint16_t dst;       /* DST address */
    uint8_t  data[384]; /* Lower Transport PDU payload */
    uint16_t data_len;
};

/*
 * When a message arrives destined for an LPN:
 * 1. Check TTL >= 2, then decrement TTL.
 * 2. If segmented, wait until full reassembly + block-ack sent.
 * 3. If queue full, pop oldest non-Friend-Update entry.
 * 4. Push new entry to tail.
 *
 * When Friend Poll arrives from LPN:
 * 1. If queue empty, create Friend Update and push it.
 * 2. Send head entry to LPN.
 * 3. On ACK from LPN, remove head entry.
 */

③ Upper Transport Layer — Encryption and Authentication

The upper transport layer sits above the lower transport layer. Its main job is to encrypt application data before sending and decrypt and verify it on the receiving side. It uses two types of keys for this.

Application Key (AppKey)

Used for normal application messages — sensor data, light commands, switch events. Shared across a group of nodes that belong to the same application.

Device Key (DevKey)

Used for configuration messages unique to a specific node. Only the provisioner and that node share the DevKey.

There are two formats for the Upper Transport PDU depending on whether it carries application data or control information:

CTL = 0 → Upper Transport Access PDU
Carries encrypted application payload + TransMIC
CTL = 1 → Upper Transport Control PDU
Carries internal control messages (no AppKey encryption)

④ Upper Transport Access PDU — Structure & Fields

PDU Structure

Encrypted Access Payload
1 to 380 bytes  (when TransMIC is 32-bit)
1 to 376 bytes  (when TransMIC is 64-bit)
TransMIC
4 bytes (32-bit)
or 8 bytes (64-bit)
▲ Encrypted using AppKey or DevKey. Opaque to transport — not inspected here. ▲ Tamper seal

What is TransMIC?

TransMIC (Transport Message Integrity Check) is a tamper seal on the encrypted payload. The sender computes a fingerprint over the encrypted data and appends it. The receiver recomputes the fingerprint independently — if the two values don’t match, the message is silently discarded.

32-bit TransMIC (4 bytes)

Used for unsegmented messages. Always 32-bit for data messages that fit in one packet.

64-bit TransMIC (8 bytes)

Used for segmented messages when the SZMIC field in the Lower Transport PDU is set to 1. Stronger integrity.

Control messages (CTL = 1) do not carry a TransMIC. Their integrity is covered by the 64-bit NetMIC at the network layer.

AppKey vs DevKey — AKF and AID Fields

The lower transport layer carries two small fields that tell the receiver which key was used for encryption:

Key Type AKF Field AID Field Typical Use
Application Key (AppKey) 1 Application key identifier (6 bits) Sensor reports, light control, switch events
Device Key (DevKey) 0 0b000000 (all zeros) Configuration Model messages

Transmitting — Key Rules

One segmented message at a time per destination — The upper transport layer must wait until the previous segmented PDU to a given destination is either fully acknowledged or cancelled before sending the next one.
SEQ allocation — A sequence number (SEQ) is allocated for every message. For segmented messages, this SEQ maps to the 24 lowest bits of SeqAuth, which the receiver uses to decrypt and prevent replay attacks.

Receiving — What Happens on the Other Side

PDU Type Receive Action
Upper Transport Access PDU Decrypt with matching AppKey/DevKey → verify TransMIC → check replay cache → deliver to access layer with source, destination, and key context.
Upper Transport Control PDU Check destination unicast address → if it matches this node, process the control message. If Friend feature is active, also check if destination is in the Friend Subscription List and store in Friend Queue if needed.

BlueZ — Sending an Access Message with AppKey

# In mesh-cfgclient, after provisioning a node at 0x0100
# and binding an AppKey to a model on that node:

# Bind AppKey index 0 to the Generic OnOff Server model (0x1000)
target 0x0100
bind 0 0 0x1000   # app_index=0, elem_addr=0, model_id=0x1000

# Now messages to that model use AppKey at upper transport layer:
# AKF = 1, AID = (derived from AppKey bytes)
# The stack encrypts the access payload with AppKey,
# appends TransMIC (32-bit for unsegmented, 64-bit for SZMIC=1)

# You can watch the debug output from the daemon:
# sudo bluetooth-meshd --nodetach --debug 2>&1 | grep -i "trans\|appkey\|encrypt"

BlueZ Source — Upper Transport TX Concept

/*
 * BlueZ: mesh/transport.c (simplified concept)
 * Transmitting an access payload at upper transport layer
 */

static bool upper_tx_access(struct mesh_net *net,
                              uint16_t src, uint16_t dst,
                              uint8_t  key_aid,    /* AID */
                              bool     akf,         /* AKF = 1 for AppKey */
                              uint8_t  *payload,
                              uint16_t payload_len)
{
    uint8_t enc_buf[380];
    uint8_t trans_mic[8];   /* up to 64-bit TransMIC */
    uint32_t seq;

    /* Allocate a fresh sequence number */
    seq = mesh_net_next_seq(net);

    /* Encrypt access payload using AppKey / DevKey
     * and compute TransMIC (AES-CCM under the hood) */
    mesh_crypto_app_encrypt(net, akf, key_aid, seq, src, dst,
                             payload, payload_len,
                             enc_buf, trans_mic);

    /* Set AKF=akf, AID=key_aid in Lower Transport header,
     * then pass to lower transport layer for segmentation
     * or direct delivery */
    lower_tx_access(net, src, dst, enc_buf,
                    payload_len + sizeof(trans_mic));
    return true;
}

⑤ Upper Transport Control PDU — Internal Mesh Control

When the CTL bit in the Network PDU is 1, there is no application data. Instead, the Upper Transport PDU carries a control message generated internally by the mesh stack. Examples include Friend Poll, Friend Update, Heartbeat, and path management messages.

These messages are not encrypted with an AppKey. They rely on the 64-bit NetMIC at the network layer for authentication only. Each control message type is identified by a 7-bit opcode in the Lower Transport header.

Payload Size Guide

Because segmenting control messages adds overhead, the spec recommends fitting them in the fewest packets possible. The table below shows the maximum useful payload for each packet count:

Packets Used Max Useful Payload (bytes) Notes
1 11 Unsegmented — preferred when it fits
1 8 Segmented single packet
2 16
3 24
n n × 8 General formula
32 (max) 256 Absolute upper limit

BlueZ — Transport Control Messages in Practice

/*
 * BlueZ generates Transport Control messages internally.
 * You don't craft them manually — the stack handles it.
 *
 * Example: Friend Poll (opcode 0x01) is sent automatically
 * by a Low Power Node to request buffered messages.
 *
 * You can observe control message opcodes in debug output:
 */

sudo bluetooth-meshd --nodetach --debug 2>&1 | grep "opcode\|ctl\|friend"

/*
 * In BlueZ source (mesh/net.c / mesh/friend.c), control PDUs
 * are built using mesh_net_send_ctl_msg() or similar helpers:
 *
 * Parameters:
 *   net_idx  - which subnet to use (picks the NetKey)
 *   src      - source unicast address
 *   dst      - destination unicast address
 *   opcode   - 7-bit control opcode (e.g. 0x01 = Friend Poll)
 *   params   - opcode-specific parameters
 *   param_len- length of params in bytes
 *
 * The function:
 * 1. Sets CTL = 1 in network header
 * 2. Places opcode in lower transport header (no AKF/AID)
 * 3. Encrypts + authenticates only at network layer (NetMIC 64-bit)
 * 4. Passes to lower transport (segments if param_len > 11)
 */

⑥ How the Layers Interact — Full Picture

Here is how an application message flows down through the transport layers on the sending side and back up on the receiving side:

Sender (TX) Receiver (RX)
Access Layer
Provides application payload (plain bytes)
Access Layer
Receives decrypted, verified payload
Upper Transport Layer
Encrypts with AppKey/DevKey
Computes TransMIC
Sets AKF, AID, SEQ
Upper Transport Layer
Decrypts payload
Verifies TransMIC
Checks replay cache
Lower Transport Layer
Segments if too large
Sends segments with SegO/SegN headers
Waits for Block ACK
Lower Transport Layer
Receives segments
Updates Block ACK bits
Reassembles when all segments received
Network Layer
Adds NetKey encryption + 32-bit NetMIC
Adds TTL, SRC, DST to header
Network Layer
Verifies NetMIC
Decrypts network header
Checks TTL, forwards or delivers
Advertising Bearer
Sends as BLE advertising packets
Advertising Bearer
Receives BLE advertising packets

✅ Quick Recap

Lower Transport Layer

Splits big messages into numbered segments. Uses Block ACK to confirm delivery. Reassembles and passes complete messages up.

Friend Queue

Holds messages for sleeping Low Power Nodes. FIFO delivery on Friend Poll. Friend Update messages are always protected from eviction.

Upper Transport Access PDU

Encrypts payload with AppKey or DevKey. Adds TransMIC (32-bit or 64-bit) for integrity. Max payload 376–380 bytes.

Upper Transport Control PDU

No AppKey encryption. CTL = 1. Uses 64-bit NetMIC only. Max 256 bytes. Identified by 7-bit opcode in the lower transport header.

More Bluetooth Mesh on EmbeddedPathashala

We cover the full mesh protocol stack — for free. Check out our other posts on Network Layer, Provisioning, and the Access Layer.

Leave a Reply

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