ble mesh programming in c Message Segmentation & Reassembly in Bluetooth Mesh

bleBluetooth Mesh Series • Lower Transport Layer

ble mesh programming in c Message Segmentation & Reassembly in Bluetooth Mesh

Learn how the lower transport layer chops large messages into segments, tracks which ones arrived using a bitmap, and rebuilds the original data at the other end.

~15 min
Read Time
Mesh Profile
Spec Section 3.5.3
Intermediate
Level

Why Does Segmentation Exist?

A Bluetooth Mesh packet rides on top of BLE advertisements. By the time you account for network and transport headers, only about 11 bytes of payload are available per packet at the lower transport layer for unsegmented messages.

That’s fine for simple on/off commands. But real-world mesh applications often need to send bigger payloads — firmware update chunks, sensor configuration blobs, vendor model data. The lower transport layer handles this by slicing the large payload into small segments, transmitting each one as a separate packet, and then reassembling them at the receiver.

Think of it the same way email attachments work: you split a large file across several messages, each one is delivered independently, and the recipient collects all pieces before opening them.

Terms You’ll Encounter in This Post

Upper Transport PDU Lower Transport PDU SeqAuth SeqZero SegO SegN BlockAck Segment ACK message Incomplete Timer Acknowledgment Timer OBO (On Behalf Of) Low Power Node Friend Node

Quick Glossary — Plain English
Term What It Means
Upper Transport PDU The big data block that needs sending. Comes from the layer above.
Lower Transport PDU One segment — one radio advertisement packet — carrying a chunk of the original data.
SeqAuth A 56-bit unique ID for each segmented transaction. Built from IV Index (32 bits) + SEQ number (24 bits). No two messages ever share the same SeqAuth.
SeqZero A 13-bit shorthand of the SEQ number carried in every segment header so the receiver can identify which transaction it belongs to.
SegO Segment Offset — zero-based index of this particular chunk (0, 1, 2 …)
SegN Last Segment Number — tells the receiver exactly how many chunks to expect in total.
BlockAck A 32-bit bitmap. Bit N = 1 means segment N was received. All zeros = receiver is rejecting the message.
OBO On Behalf Of. Set to 1 when a Friend node acknowledges segments on behalf of a sleeping Low Power node.

Part 1: How a Message Gets Segmented

When the lower transport layer receives an Upper Transport PDU that is too large for a single packet, it slices it into chunks of up to 8 bytes each and wraps each chunk in a Lower Transport header.

Every segment carries three critical fields in its header:

  • SeqZero — which transaction does this segment belong to?
  • SegO — which chunk number is this? (starts at 0)
  • SegN — what is the last chunk’s number? (tells receiver total count)

Carrying SegN in every single segment is intentional. Even if you receive segment #3 first, you immediately know the full message spans SegN+1 chunks and can allocate the right buffer size before any other segments arrive.

42-byte Upper Transport PDU split into 6 Lower Transport Segments

Upper Transport PDU — 42 bytes

Lower transport layer segments the PDU (max 8 bytes per segment)

SegO=0  |  SegN=5
SeqZero
Bytes 0–7
SegO=1  |  SegN=5
SeqZero
Bytes 8–15
SegO=2  |  SegN=5
SeqZero
Bytes 16–23
SegO=3  |  SegN=5
SeqZero
Bytes 24–31
SegO=4  |  SegN=5
SeqZero
Bytes 32–39
SegO=5  |  SegN=5
SeqZero
Bytes 40–41
SegN=5 in every segment tells the receiver: total segments = SegN+1 = 6 (numbered 0 through 5)

SeqAuth and SeqZero — The Message Identity System

The mesh network needs a way to identify each unique segmented transaction so it can detect retransmissions and discarded messages. That identifier is SeqAuth.

SeqAuth = IV Index (32 bits) + SEQ Number (24 bits) = 56-bit unique ID
IV Index
32 bits — shared across the whole network
SEQ
24 bits — per-node sequence counter
Worked example from the spec:
IV Index = 0x58437AF2, SEQ = 0x647262, SeqZero (received) = 0x1849
→ SeqAuth = 0x58437AF2645849
⚠ Hard limit: A segmented message cannot be sent once SEQ has advanced 8192 steps beyond SeqAuth. If the message is still unacknowledged by then, the lower transport layer cancels the entire delivery.

Why SeqZero instead of the full SEQ? Segment headers have limited space. SeqZero is a 13-bit slice of SEQ that fits in the header. The receiver reconstructs the full SeqAuth by combining SeqZero with the known IV Index and the most recently seen SEQ value — this works reliably as long as the receiver hasn’t moved 8192 sequence numbers ahead of the sender.

Part 2: Sending Segments — Unicast vs Group Address

Sender behaviour after segmenting a message depends entirely on the destination address type:

🔵 Unicast Address (one-to-one)

✓ Receiver sends a Segment Acknowledgment back

✓ Sender retransmits only missing segments (identified by BlockAck)

✓ Segment transmission timer: 200 + 50 × TTL ms

✓ Each segment sent at least twice

✓ BlockAck = 0x00000000 → receiver is rejecting, cancel immediately

🟢 Group / Virtual Address (one-to-many)

No Segment Acknowledgment expected from anyone

✓ All segments are transmitted

✓ Recommended: send each segment multiple times with random delays between repetitions

⚠ Delivery is best-effort — treat it as unacknowledged

Unicast Segment Exchange — How It Flows

SENDER
1. Segment the PDU and send all segments
2. Start tx timer (200+50×TTL ms)
3. Receive partial ACK → reset timer, retransmit missing segments
4. All segments ACKed → Done ✓

Segments 0,1,2…

BlockAck (partial)

Retransmit missing

BlockAck all bits=1

RECEIVER
1. Store each segment in buffer, set bit in BlockAck
2. Start ack timer (150+50×TTL ms) and incomplete timer (10 s)
3. Ack timer fires → send current BlockAck (even if incomplete)
4. All segs in → Final ACK + hand to upper transport

If the ack timer fires before all segments arrive, a partial BlockAck is sent. The sender reads which bits are still 0 and retransmits only those segments, not the entire message.

Part 3: BlockAck — The Segment Receipt Bitmap

How BlockAck Tracks What Arrived

BlockAck is a 32-bit field. Each bit position N represents segment N. Bit = 1 means received. Bit = 0 means missing. One 4-byte field can cover up to 32 segments, which is the maximum for a single segmented message.

Example: 6-segment message, segments 0–3 received, 4 and 5 still missing
1
1
1
1
0
0
0
0
…24
more
S0
S1
S2
S3
S4
S5
b6
b7
Received Missing — sender will retransmit Unused (always 0)
BlockAck sent = 0b00001111 = 0x0000000F
Sender sees bits 4 and 5 are zero → retransmits only segments 4 and 5
Special case: BlockAck = 0x00000000 → receiver is busy / out of memory → sender cancels the entire message immediately, no retry

Part 4: Three Timers That Keep Everything Moving

Three timers govern the whole segmentation protocol. If you ever debug mesh transport issues in BlueZ, these are the first numbers to check.

Timer Side Minimum Duration What Happens When It Fires
Segment Tx Timer Sender 200 + 50 × TTL ms No ACK arrived → retransmit all unacknowledged segments
Acknowledgment Timer Receiver 150 + 50 × TTL ms Send current BlockAck back to sender (partial or complete)
Incomplete Timer Receiver 10 000 ms (10 s) No new segment for 10 s → abort transaction, discard partial data
TTL matters: With TTL=10, the sender’s tx timer is at least 700 ms and the receiver’s ack timer is at least 650 ms. Higher TTL = more hops = longer round-trip = more time budgeted. The formula automatically accounts for network depth.

Receiver Timer Lifecycle
First
segment arrives
Both timers
started
More segs arrive
→ incomplete timer
restarted each time
All segs received
→ Final ACK + reassemble ✓
Incomplete timer fires
→ Abort, discard ✗

Part 5: Reassembly — What the Receiver Actually Does

Step-by-Step Reassembly Process

Here is exactly what happens on the receiving node when a segmented message comes in:

1
Check SeqAuth. Is this a new transaction or a duplicate of something already processed? If SeqAuth is smaller than the stored sequence authentication value, discard the segment.
2
Allocate a buffer. Use SegN to calculate the total size — you know it immediately from the very first segment you receive. No need to wait for all segments before allocating.
3
Write each segment into the buffer. Place the payload bytes at offset SegO × 8 in the buffer. Set bit SegO in the BlockAck value to 1.
4
Restart the incomplete timer every time a new segment arrives. If no segment arrives for 10 seconds, the transaction is considered failed.
5
Send partial ACKs when the acknowledgment timer fires, even if the message isn’t complete yet. The sender uses this to know which segments got through.
6
When all SegN+1 segments are received — send a final ACK with all bits set, cancel both timers, pass the reassembled data up to the upper transport layer.
Already received this message before? If the same SeqAuth arrives again (a duplicate), the receiver immediately sends the saved BlockAck with all bits set — no reprocessing, just a fast ACK.

Part 6: Low Power Node — The Special Case

A Low Power node (LPN) turns off its radio most of the time to conserve battery. It cannot receive segments directly or acknowledge them — it’s asleep during transmission. The Friend node handles everything on its behalf.

Segmented Message to a Low Power Node

SENDER
Sends all segments

Segments

FRIEND NODE
Receives all segments
Reassembles message
Stores in Friend Queue
Sends ACK (OBO=1)

BlockAck OBO=1

LPN
(asleep)
Wakes & polls
Friend Queue later
⚠ OBO=1 does not mean delivered. It only means the Friend node has the message in its queue. The message can still be discarded if the queue fills up with newer messages for that LPN, or if the friendship is terminated before the LPN polls.

Behaviour Normal Node Low Power Node
Receives segments directly Yes No (Friend does it)
Sends Segment ACKs Yes No (Friend sends OBO=1)
Has incomplete timer Yes No
Partial message dropped if friendship ends N/A Yes, cancelled immediately

Part 7: Observing This in BlueZ

Where BlueZ Implements Lower Transport Segmentation

In BlueZ, Bluetooth Mesh support is handled by bluetooth-meshd. The lower transport segmentation logic — timers, BlockAck tracking, retransmission — lives in mesh/transport.c inside the BlueZ source tree. The timer values you’ve learned (200+50×TTL, 150+50×TTL, 10 s) are implemented there.

Step 1: Confirm meshd is present on your system

# Check BlueZ version
bluetoothd --version

# Find the mesh daemon binary
ls /usr/libexec/bluetooth/bluetooth-meshd

# Or if installed via package manager
which bluetooth-meshd

Step 2: Start meshd with debug output

# Run with debug logging — lets you see transport events in real time
sudo bluetooth-meshd --nodetach --debug 2>&1 | tee meshd.log

# In another terminal, filter for segmentation-related output
tail -f meshd.log | grep -iE "seg|block.?ack|transport|seqauth"

Step 3: Use meshctl to provision a device and attach to the mesh

# Launch the BlueZ mesh control CLI
meshctl

# Scan for unprovisioned devices nearby
[meshctl]# discover-unprovisioned on

# Provision a device (replace UUID with what appears in scan output)
[meshctl]# provision 00112233445566778899aabbccddeeff

# Once provisioned, switch to the main mesh menu
[meshctl]# menu main

Step 4: Send a payload large enough to trigger segmentation

A payload larger than 11 bytes forces the lower transport layer to segment. In meshctl, the send command takes a destination unicast address, app key index, and hex-encoded payload:

# Send to unicast 0x0005 using app key index 0
# This 16-byte payload (>11 bytes) will be segmented into 2 segments
[meshctl]# send 0005 0 0102030405060708090a0b0c0d0e0f10

# Expected debug log output from bluetooth-meshd:
# transport: Tx SegO=0 SegN=1 SeqAuth=0x00000001000100 dst=0x0005
# transport: Tx SegO=1 SegN=1 SeqAuth=0x00000001000100 dst=0x0005
# transport: Seg ACK src=0x0005 BlockAck=0x00000001  (partial)
# transport: Seg ACK src=0x0005 BlockAck=0x00000003  (all bits set, done)

Step 5: Check timer constants in BlueZ source

# Clone BlueZ source to inspect the implementation
git clone https://git.kernel.org/pub/scm/bluetooth/bluez.git
cd bluez

# Find where timer values are set in the mesh transport code
grep -n "200\|150\|incomplete\|seg_tx\|ack_timer" mesh/transport.c | head -30

What you’ll find in mesh/transport.c (illustrative structure — exact symbol names vary by BlueZ version):

/*
 * Segment transmission timer — used by sender (unicast only)
 * Started each time Lower Transport PDUs are sent
 * Minimum: 200 + 50 * TTL  milliseconds
 */
static uint32_t seg_tx_time_ms(uint8_t ttl)
{
    return (200 + (50 * ttl));
}

/*
 * Acknowledgment timer — used by receiver (unicast destination only)
 * Fires to send a BlockAck even if not all segments arrived yet
 * Minimum: 150 + 50 * TTL  milliseconds
 */
static uint32_t ack_delay_ms(uint8_t ttl)
{
    return (150 + (50 * ttl));
}

/*
 * Incomplete timer — receiver side
 * Maximum gap allowed between arriving segments of the same transaction
 * If this fires, the partial message is discarded
 */
#define INCOMPLETE_TIMEOUT_SEC  10

Note: The function and macro names above are simplified for clarity. The exact naming in your installed BlueZ version may differ slightly. The timer formulas match the Mesh Profile specification exactly.

Tip: Watch for BlockAck=0x0 in logs

# This line in meshd output means the remote node rejected the message
# (it was busy or out of reassembly resources)
# transport: Seg ACK src=0x0005 BlockAck=0x00000000 — cancelling Upper Transport PDU

# When you see this, the sender immediately gives up.
# No retransmission happens. Higher layer is notified of the cancellation.

Summary

Concept One-Line Takeaway
Segmentation Large PDU sliced into ≤8-byte chunks; every chunk carries SegO, SegN, SeqZero
SeqAuth 56-bit unique transaction ID (IV Index + SEQ); prevents duplicate processing
BlockAck 32-bit bitmap; bit N=1 means segment N received; 0x00000000 = cancel now
Unicast delivery Reliable; sender retransmits missing segments based on BlockAck feedback
Group/Virtual delivery Best-effort; all segments sent multiple times, no ACK, delivery not confirmed
Three timers Tx: 200+50×TTL ms (sender) | Ack: 150+50×TTL ms (receiver) | Incomplete: 10 s (receiver)
Low Power Node Friend node RX and ACKs all segments (OBO=1); LPN polls queue when it wakes

Continue the Bluetooth Mesh Series

Next up: the Upper Transport Layer — encryption, MIC validation, and how access messages are secured before segmentation ever kicks in.

Leave a Reply

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