bleBluetooth Mesh Series • Lower Transport Layer
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.
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
| 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.
The mesh network needs a way to identify each unique segmented transaction so it can detect retransmissions and discarded messages. That identifier is SeqAuth.
| IV Index 32 bits — shared across the whole network |
SEQ 24 bits — per-node sequence counter |
IV Index =
0x58437AF2, SEQ = 0x647262, SeqZero (received) = 0x1849→ SeqAuth =
0x58437AF2645849Why 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:
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
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.
more
0b00001111 = 0x0000000FSender sees bits 4 and 5 are zero → retransmits only segments 4 and 5
0x00000000 → receiver is busy / out of memory → sender cancels the entire message immediately, no retryPart 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 |
segment arrives
started
→ incomplete timer
restarted each time
→ Final ACK + reassemble ✓
→ Abort, discard ✗
Part 5: Reassembly — What the Receiver Actually Does
Here is exactly what happens on the receiving node when a segmented message comes in:
SegO × 8 in the buffer. Set bit SegO in the BlockAck value to 1.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.
Reassembles message
Stores in Friend Queue
Sends ACK (OBO=1)
(asleep)
Friend Queue later
| 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
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.
