Bluetooth Mesh tutorial : Segmentation & Reassembly (SAR)
How the Lower Transport Layer splits large messages into small packets — and puts them back together
Why Does Bluetooth Mesh Need SAR?
Every message in Bluetooth Mesh travels as a Network PDU. A single Network PDU can carry at most 15 octets of Upper Transport data in an unsegmented packet.
But real-world operations routinely produce larger payloads. For example, provisioning a new node involves sending an AppKey (16 bytes) plus NetKey indices plus an opcode — easily 20–30 octets. That blows past the 15-octet limit.
Segmentation and Reassembly (SAR) is the mechanism the Lower Transport Layer uses to handle this. It chops the large Upper Transport PDU into smaller segments, sends each one as its own Network PDU, and the receiver reassembles them back into the original message. A bitmap-based acknowledgment scheme ensures missing segments are retransmitted without resending the whole message.
The easiest way to understand SAR is to trace a single large message through the stack.
Flow: A 24-octet AppKey message getting segmented
| Access Layer | → | Opcode (1B) + NetKeyIndex (2B) + AppKeyIndex (1B) + AppKey (16B) = 20 octets |
| ⬇ Upper Transport Layer encrypts + adds 4-byte TransMIC ⬇ |
| Upper Transport Layer | → | Encrypted Access PDU + TransMIC (4B) = 24 octets (too big for one packet) |
| ⬇ Lower Transport Layer slices into 12-octet segments ⬇ |
| Lower Transport Layer | → |
|
| ⬇ Each segment goes to Network Layer → gets its own SEQ number, encryption, NetMIC ⬇ |
| Network Layer | → |
|
The maximum size of each segment depends on the PDU type being segmented:
Example — Access PDU of 42 octets (12-octet segments):
| Segment | Octet Range | Size |
|---|---|---|
| Segment 0 | 0 – 11 | 12 octets |
| Segment 1 | 12 – 23 | 12 octets |
| Segment 2 | 24 – 35 | 12 octets |
| Segment 3 (last) | 36 – 41 | 6 octets (shorter) |
When segments arrive out of order (Bluetooth Mesh uses flooding — packets can take different relay paths), the receiver needs a way to identify which segments belong to the same Upper Transport PDU. This is done using SeqAuth.
SeqAuth = IV Index (32 bits) + SEQ of first segment (24 bits) = 56 bits total
| IV Index 32 bits — most significant |
SEQ (Sequence Number) 24 bits — least significant |
| Combined = 56-bit SeqAuth | Formed using the SEQ of the first segment only | |
SeqZero = lower 13 bits of the SEQ part:
| SEQ (24 bits) | |
| bits 23 → 13 (not transmitted in segment header) |
bits 12 → 0 = SeqZero 13 bits — sent in every segment and ACK |
This is the format of the Lower Transport PDU when it carries a segment of an Upper Transport Access PDU.
Segmented Access Message (Lower Transport Header):
| SEG 1 bit |
AKF 1 bit |
AID 6 bits |
SZMic 1 bit |
SeqZero 13 bits |
SegO 5 bits |
SegN 5 bits |
Segment m up to 12 octets |
| SEG=1 → segmented | AKF → AppKey flag | AID → AppKey identifier | SZMic → 0=32-bit TransMIC, 1=64-bit TransMIC | SeqZero → links all segments | SegO → this segment’s position (0-based) | SegN → position of last segment | |||||||
Transport Control messages (Friend Poll, Heartbeat, Proxy Config) can also be too large for one PDU. They get segmented into 8-octet chunks instead of 12.
Segmented Control Message (Lower Transport Header):
| SEG 1 bit |
Opcode 7 bits |
RFU 1 bit |
SeqZero 13 bits |
SegO 5 bits |
SegN 5 bits |
Segment m up to 8 octets |
| SEG=1 → segmented | Opcode → identifies the Control message type (0x01–0x7F). 0x00 is reserved and must not be transmitted | All segments of the same PDU carry identical Opcode, SeqZero, and SegN | ||||||
When the receiver collects segments, it sends back a Segment Acknowledgment message. This is a special Transport Control message (opcode 0x00) that reports exactly which segments arrived.
Segment Acknowledgment Message Format:
| SEG 1 bit = 0 |
Opcode 7 bits = 0x00 |
OBO 1 bit |
SeqZero 13 bits |
RFU 2 bits |
BlockAck 32 bits |
| SEG=0 → this is an unsegmented control message | Opcode=0x00 → identifies it as a Segment Acknowledgment | SeqZero → matches the SeqZero of the message being acknowledged | BlockAck → bitmap showing which segments were received | |||||
The BlockAck Field — A 32-bit Bitmap:
| BlockAck (32 bits) — bit n = 1 means Segment n was received ✓, bit n = 0 means Segment n is missing ✗ | |||||||
| bit 0 Seg 0 |
bit 1 Seg 1 |
bit 2 Seg 2 |
bit 3 Seg 3 |
bit 4 Seg 4 |
…. …. |
bit 30 Seg 30 |
bit 31 Seg 31 |
| 1 | 1 | 0 | 1 | 0 | 0…0 | 0 | 0 |
👆 BlockAck = 0b...1011 = 0x0000000B → Segments 0, 1, 3 received ✅ | Segment 2 missing ❌ → Sender retransmits Segment 2 only |
|||||||
In a Friend/LPN setup, a sleeping Low Power Node relies on its Friend to buffer messages. When the Friend acknowledges segments on the LPN’s behalf, it sets OBO = 1. A node directly receiving the message sets OBO = 0.
BlueZ includes a mesh daemon (bluetooth-meshd) and tools to interact with it. Here is how to capture and decode segmented mesh traffic on your Linux machine.
1. Start the BlueZ Mesh Daemon
# Start in debug mode to see mesh activity in real time
sudo /usr/libexec/bluetooth/bluetooth-meshd --nodetach --debug
# Or if installed as a systemd service
sudo systemctl start bluetooth-meshd
sudo journalctl -u bluetooth-meshd -f # follow the logs
2. Capture Raw HCI Traffic with btmon
# Open a separate terminal and start btmon
# This captures all HCI events including mesh advertising PDUs
sudo btmon -w /tmp/mesh_capture.log
# Later, replay the capture for analysis
btmon -r /tmp/mesh_capture.log | grep -A5 "LE Advertising"
3. Use meshctl to Send a Large Message
# Launch the mesh control tool
meshctl
# Attach to the mesh daemon using your provisioned node token
[meshctl]# join /var/lib/bluetooth/mesh
# List known nodes on the network
[meshctl]# list
# Navigate to a model menu and send a message that will trigger SAR
# (any message whose Upper Transport PDU exceeds 15 octets)
[meshctl]# menu config
[meshctl]# target 0x0003
# AppKey Add is a good example — it sends a 16-byte key + indices
[meshctl]# appkey-add 0
4. Manually Parse a Segmented Access PDU Header in C
/*
* parse_seg_access.c
* Decodes the Lower Transport header of a Segmented Access message.
* Compile: gcc parse_seg_access.c -o parse_seg_access
*/
#include <stdio.h>
#include <stdint.h>
void parse_seg_access_hdr(const uint8_t *b)
{
/* Byte 0: SEG(1) AKF(1) AID(6) */
uint8_t seg = (b[0] >> 7) & 0x01;
uint8_t akf = (b[0] >> 6) & 0x01;
uint8_t aid = b[0] & 0x3F;
/* Byte 1: SZMic(1) SeqZero_high(7) */
/* Byte 2: SeqZero_low(6) SegO_high(2) */
/* Byte 3: SegO_low(3) SegN(5) */
uint8_t szmic = (b[1] >> 7) & 0x01;
uint16_t seqzero = (((uint16_t)(b[1] & 0x7F)) << 6)
| ((b[2] >> 2) & 0x3F);
uint8_t sego = ((b[2] & 0x03) << 3)
| ((b[3] >> 5) & 0x07);
uint8_t segn = b[3] & 0x1F;
printf("SEG = %u (1 = segmented)\n", seg);
printf("AKF = %u AID = 0x%02X\n", akf, aid);
printf("SZMic = %u (%s TransMIC)\n",
szmic, szmic ? "64-bit" : "32-bit");
printf("SeqZero= 0x%04X\n", seqzero);
printf("SegO = %u (this is segment #%u)\n", sego, sego);
printf("SegN = %u (last segment is #%u, total = %u)\n",
segn, segn, segn + 1);
}
int main(void)
{
/*
* Example: 4-segment message, currently on Segment 2 of 4
* b[0] = 0x80 → SEG=1 AKF=0 AID=0
* b[1] = 0x01 → SZMic=0, SeqZero high bits
* b[2] = 0x28 → SeqZero low bits, SegO high bits
* b[3] = 0x43 → SegO low bits, SegN
*/
uint8_t hdr[] = { 0x80, 0x01, 0x28, 0x43 };
parse_seg_access_hdr(hdr);
return 0;
}
5. Decode a BlockAck Bitmap
/*
* check_blockack.c
* Shows which segments were acknowledged in a Segment Acknowledgment.
* Compile: gcc check_blockack.c -o check_blockack
*/
#include <stdio.h>
#include <stdint.h>
void check_blockack(uint32_t blockack, uint8_t segn)
{
printf("BlockAck = 0x%08X (SegN = %u, total %u segments)\n\n",
blockack, segn, segn + 1);
for (int i = 0; i <= segn; i++) {
if (blockack & (1u << i))
printf(" Segment %2d : RECEIVED [bit %d = 1]\n", i, i);
else
printf(" Segment %2d : MISSING [bit %d = 0] <-- retransmit\n",
i, i);
}
}
int main(void)
{
/*
* 4-segment message. Segments 0, 1, 3 received. Segment 2 missing.
* Binary: bit3=1 bit2=0 bit1=1 bit0=1 = 0b1011 = 0x0000000B
*/
check_blockack(0x0000000B, 3);
return 0;
}
/*
* Expected output:
* Segment 0 : RECEIVED [bit 0 = 1]
* Segment 1 : RECEIVED [bit 1 = 1]
* Segment 2 : MISSING [bit 2 = 0] <-- retransmit
* Segment 3 : RECEIVED [bit 3 = 1]
*/
6. Watch Mesh Daemon Logs for SAR Activity
# The mesh daemon prints lower-transport segmentation events.
# Filter the journal output for SAR-related log lines:
sudo journalctl -u bluetooth-meshd -f | grep -iE "seg|sar|frag|reassem"
# Typical lines you may see:
# mesh/transport.c: Received segment 0 of 3
# mesh/transport.c: Received segment 2 of 3
# mesh/transport.c: Sending SAR ACK, BlockAck=0x00000007
# mesh/transport.c: Reassembly complete, len=36
Next Up: Bluetooth Mesh Network Layer
Learn how Network PDUs are encrypted, obfuscated, and relayed across mesh nodes — and how the IV Index prevents replay
