Bluetooth Mesh tutorial : Segmentation & Reassembly (SAR)

Bluetooth Mesh tutorial : Segmentation & Reassembly (SAR)

How the Lower Transport Layer splits large messages into small packets — and puts them back together

⏱️
15 min read
📶
BT Mesh · SAR
🔵
Intermediate

Topics Covered
Segmentation & Reassembly Lower Transport Layer BlockAck Bitmap SeqZero SeqAuth SegO / SegN Segment Acknowledgment Segmented Control Message BlueZ Mesh OBO (Friend/LPN)

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.

🧩 Chapter 1: The Big Picture — What Happens Step by Step

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
Segment 0
Header | octets 0–11 (12B)
Segment 1
Header | octets 12–23 (12B)

⬇   Each segment goes to Network Layer → gets its own SEQ number, encryption, NetMIC   ⬇

Network Layer
NID | IVI | SEQn | SRC | DST | Seg0 | NetMIC NID | IVI | SEQn+1 | SRC | DST | Seg1 | NetMIC
Key insight: Each segment burns one SEQ number from the node’s sequence counter. A 24-octet message produces 2 segments → consumes 2 SEQ numbers. The Network PDU looks totally different for each segment (different SEQ → different encryption output), so an eavesdropper cannot correlate them from the outside.

📏 Chapter 2: Segment Sizes — Access vs Control PDUs

The maximum size of each segment depends on the PDU type being segmented:

PDU Type Segment Size Max Segments Max PDU Size
Upper Transport Access PDU 12 octets 32 (SegN 0–31) 384 octets
Upper Transport Control PDU 8 octets 32 (SegN 0–31) 256 octets

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)

Example — Control PDU of 42 octets (8-octet segments): Produces 6 segments: Seg 0 (0–7), Seg 1 (8–15), Seg 2 (16–23), Seg 3 (24–31), Seg 4 (32–39), Seg 5 (40–41, 2 octets, shorter).

🔑 Chapter 3: SeqAuth and SeqZero — The Glue Holding Segments Together

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
Why only 13 bits? Sending all 56 bits of SeqAuth in every segment wastes header space. The lower 13 bits (SeqZero) are enough for the receiver to match segments, because the full SeqAuth can be reconstructed using the IV Index and the SEQ numbers visible in the Network PDU headers.

📦 Chapter 4: Segmented Access Message — Packet Format

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

Field Bits What it does
SEG 1 Always 1 for a segmented message
SeqZero 13 Lower 13 bits of SeqAuth — ties all segments of this PDU together
SegO 5 Segment Offset — position of this segment (0 = first)
SegN 5 Position of the last segment. Total count = SegN + 1
Segment m ≤ 12B The actual payload chunk for this segment
Rule: Every segment of the same Upper Transport PDU must have the same SeqZero and SegN. If the receiver sees a segment with a different SegN for the same SeqZero, it discards it.

🎛️ Chapter 5: Segmented Control Message — Packet Format

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
The key difference from the Access format is the Opcode field (replacing AKF+AID+SZMic). There is no TransMIC in Control PDUs — authentication is handled differently at the transport layer.

✅ Chapter 6: Segment Acknowledgment — The BlockAck Mechanism

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

OBO Field (On Behalf Of):
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.

📋 Chapter 7: SAR Transmission Rules
Rule Details
One PDU in flight at a time Only one segmented Upper Transport PDU may be outstanding to the same destination at any time. Wait for full ACK (or cancel) before sending the next one.
Selective retransmit BlockAck tells you exactly which bits are 0. Retransmit only those segments — not the whole message.
Use segmented for single-segment too A PDU that fits in one packet can still be sent as a single Segmented message to gain lower-transport ACKing. Useful when losing the ACK from an access-layer reply would require re-sending an expensive multi-segment message.
TTL=0 ACK rule If the original segments were sent with TTL=0, the Segment Acknowledgment should also use TTL=0.
Ignore zero bits beyond SegN Bits in BlockAck that are above SegN are always 0 and must be ignored by the receiver.

💻 Chapter 8: Observing SAR with BlueZ Tools

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

📝 Summary
Upper Transport PDUs > 15 octets trigger SAR Access PDU: 12 octets per segment Control PDU: 8 octets per segment Max 32 segments (SegN 0–31) SegO = this segment’s position (0-based) SegN = last segment position SeqZero = lower 13 bits of SeqAuth SeqAuth = IV Index + SEQ of first segment BlockAck = 32-bit bitmap, bit n = Seg n received OBO = 1 when Friend ACKs for LPN One segmented PDU per destination at a time Retransmit only missing segments

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

Leave a Reply

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