Bluetooth Mesh programming in c : Lower Transport Layer

Bluetooth Mesh programming in c : Lower Transport Layer
Segmentation, Reassembly, PDU Formats and Network Message Cache — with BlueZ Code Examples
4
PDU Formats
§3.5
Mesh Spec Section
BlueZ
Code Examples
12B
Per Segment

What is the Lower Transport Layer?

In a Bluetooth Mesh network, messages pass through several protocol layers before reaching their destination. The Lower Transport Layer sits between the Network Layer (below) and the Upper Transport Layer (above). It has two core responsibilities:

  • Outgoing messages (TX): Take a payload from the Upper Transport Layer and, if it is too large for a single Network PDU, break it into numbered 12-byte chunks before handing them to the network layer one by one.
  • Incoming messages (RX): Collect those chunks as they arrive from the network, track which ones are still missing, send acknowledgments back to the sender, and pass the fully reassembled payload up to the Upper Transport Layer.

If the payload is small enough to fit in one Network PDU, no splitting happens at all — it travels as an unsegmented message and the acknowledgment mechanism is skipped entirely, keeping things lightweight.

This tutorial walks through every PDU format defined in the Bluetooth Mesh Profile specification (§3.4.6.5 and §3.5), with BlueZ-style C code showing how each concept maps to a real implementation.

Key Terms You Will Encounter

Lower Transport PDU Network Message Cache SEG bit CTL bit AKF / AID SZMIC SeqZero SegO / SegN Segment m BlockAck bitmask Segment Acknowledgment TransMIC Unsegmented Access Segmented Access Transport Control OBO flag

1 — Network Message Cache: Stopping Duplicate Processing
Duplicate Detection LRU Eviction Relay Optimization SRC + SEQ Tracking

Why duplicates happen

Bluetooth Mesh uses flooding to deliver messages: relay nodes forward packets they receive, and those relayed packets can arrive at the same destination node multiple times via different paths. Without any protection, a node would process and relay the same Network PDU dozens of times — wasting CPU time, battery, and air time.

How the cache works

Every mesh node maintains a Network Message Cache containing a record of recently seen Network PDUs. When a PDU arrives:

  • If the PDU is already in the cache → discard it immediately without any further security checks or relaying.
  • If the PDU is new → run security validation, and if it passes, store it in the cache and continue processing.

The specification does not require storing the entire PDU. Caching only the source address (SRC) paired with the sequence number (SEQ) is sufficient to detect re-arrivals, because each element in the mesh increments its sequence number for every new Network PDU it originates. Some implementations also cache the NetMIC value for an extra layer of uniqueness.

Cache size and eviction

The minimum cache size is two entries, though in a dense network you want significantly more. When the cache is full and a new entry needs to be stored, the oldest entry is replaced — a classic circular-buffer (FIFO) strategy. The exact size is left to the implementer because it depends on how many relay nodes and how much traffic the device expects to see.

BlueZ implementation

The BlueZ mesh daemon (mesh/net.c) checks incoming Network PDUs against a message cache before doing any security processing. The snippet below shows the core logic in a simplified form:

/*
 * mesh/net.c — Network Message Cache (simplified)
 *
 * The mesh network layer calls net_pdu_received() for every
 * incoming PDU. Duplicates are dropped before any security
 * processing is attempted.
 */

#include <stdint.h>
#include <stdbool.h>
#include <string.h>

#define MSG_CACHE_SIZE  32   /* Adjust based on network density */

struct msg_cache_entry {
    uint32_t seq;    /* Sequence number from Network PDU header */
    uint16_t src;    /* Source unicast address                  */
    bool     valid;  /* Slot is occupied                        */
};

static struct msg_cache_entry msg_cache[MSG_CACHE_SIZE];
static int cache_head = 0;   /* Points to next slot to overwrite */

/* Returns true if (src, seq) is already in the cache */
static bool is_duplicate(uint16_t src, uint32_t seq)
{
    int i;

    for (i = 0; i < MSG_CACHE_SIZE; i++) {
        if (msg_cache[i].valid        &&
            msg_cache[i].src == src   &&
            msg_cache[i].seq == seq) {
            return true;   /* Already seen — discard */
        }
    }
    return false;
}

/* Store a new (src, seq) pair; overwrites the oldest entry */
static void cache_add(uint16_t src, uint32_t seq)
{
    msg_cache[cache_head].src   = src;
    msg_cache[cache_head].seq   = seq;
    msg_cache[cache_head].valid = true;

    cache_head = (cache_head + 1) % MSG_CACHE_SIZE;
}

/*
 * Entry point called by the network layer for every incoming PDU.
 * Spec §3.4.6.5: a PDU already in the cache shall not be processed.
 */
void net_pdu_received(uint16_t src, uint32_t seq,
                      bool ctl, uint8_t *pdu, size_t len)
{
    if (is_duplicate(src, seq)) {
        /* Drop silently — no relaying, no security check */
        return;
    }

    /* New PDU: add to cache, then hand off to lower transport */
    cache_add(src, seq);
    lower_transport_receive(src, seq, ctl, pdu, len);
}

The key design point: only src and seq go into the cache — the actual PDU bytes are not stored. This keeps memory usage low while still satisfying the specification requirement of not processing the same PDU more than once.

2 — Lower Transport PDU: Four Formats, One SEG Bit
SEG bit (MSB) CTL from Network PDU Big-Endian byte order Format dispatch

The two-bit type selector

Every Lower Transport PDU starts with the SEG bit in the most significant position of its first byte (bit 7). This single bit tells the receiver whether the payload is a complete message or one chunk of a segmented message.

The CTL bit comes from the enclosing Network PDU header — it is not part of the Lower Transport PDU itself. CTL distinguishes access traffic (application data) from control traffic (mesh management messages).

Combining CTL and SEG gives four distinct Lower Transport PDU formats:

CTL SEG Format Typical use
0 0 Unsegmented Access Small app messages that fit in one PDU
0 1 Segmented Access Large app messages split across multiple PDUs
1 0 Unsegmented Control Segment ACK or transport control opcode
1 1 Segmented Control Large control messages split across PDUs

All multi-byte numeric values inside the Lower Transport Layer are transmitted in big-endian (most significant byte first) order, consistent with the rest of the Bluetooth Mesh specification.

/*
 * mesh/transport.c — Classify a Lower Transport PDU
 *
 * ctl   : CTL bit read from the enclosing Network PDU header
 * data  : pointer to first byte of the Lower Transport PDU
 */

#include <stdint.h>
#include <stdbool.h>

/* SEG is always the MSB of the first Lower Transport PDU byte */
#define SEG_BIT  0x80

typedef enum {
    MSG_UNSEG_ACCESS  = 0,   /* CTL=0, SEG=0 */
    MSG_SEG_ACCESS    = 1,   /* CTL=0, SEG=1 */
    MSG_UNSEG_CONTROL = 2,   /* CTL=1, SEG=0 */
    MSG_SEG_CONTROL   = 3,   /* CTL=1, SEG=1 */
} lt_msg_type_t;

static lt_msg_type_t classify(bool ctl, uint8_t first_byte)
{
    bool seg = (first_byte & SEG_BIT) != 0;

    if (!ctl && !seg) return MSG_UNSEG_ACCESS;
    if (!ctl &&  seg) return MSG_SEG_ACCESS;
    if ( ctl && !seg) return MSG_UNSEG_CONTROL;
    return MSG_SEG_CONTROL;
}

void lower_transport_receive(uint16_t src, uint32_t seq,
                              bool ctl, uint8_t *data, size_t len)
{
    switch (classify(ctl, data[0])) {
    case MSG_UNSEG_ACCESS:
        handle_unseg_access(src, data, len);
        break;
    case MSG_SEG_ACCESS:
        handle_seg_access(src, seq, data, len);
        break;
    case MSG_UNSEG_CONTROL:
        handle_unseg_control(src, data, len);
        break;
    case MSG_SEG_CONTROL:
        handle_seg_control(src, seq, data, len);
        break;
    }
}

3 — Unsegmented Access Message: Small Payloads, No Splitting
SEG = 0 AKF flag AID (6 bits) 32-bit TransMIC Single PDU delivery

When is this format used?

If the encrypted Upper Transport Access PDU (including its 32-bit TransMIC) fits inside a single Network PDU — which allows up to 15 bytes of Lower Transport payload — the message is sent as an Unsegmented Access Message. No sequence tracking, no acknowledgment, no reassembly needed on the receiving end.

Field breakdown

The first byte of this PDU packs three fields together:

  • SEG (bit 7) = 0 — Signals to the receiver that this is a complete message, not a segment.
  • AKF (bit 6) — Application Key Flag. Set to 1 when an Application Key encrypted the payload; set to 0 when a Device Key was used (for configuration messages).
  • AID (bits 5–0) — Application Key Identifier. A 6-bit value derived from the Application Key, used to select the correct key for decryption without transmitting the key itself.

The remaining bytes are the encrypted Upper Transport Access PDU. Because this format carries no SZMIC field, the TransMIC is always treated as a 32-bit (4-byte) value — exactly as if SZMIC were 0.

/*
 * mesh/transport.c — Unsegmented Access Message handler
 *
 * Byte 0 layout:  [SEG=0 | AKF | AID(6 bits)]
 * Bytes 1..n:     Encrypted Upper Transport Access PDU + 32-bit TransMIC
 */

#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>

#define AKF_MASK  0x40   /* Bit 6 of byte 0 */
#define AID_MASK  0x3F   /* Bits 5-0 of byte 0 */

void handle_unseg_access(uint16_t src, uint8_t *data, size_t len)
{
    bool    akf = (data[0] & AKF_MASK) != 0;
    uint8_t aid =  data[0] & AID_MASK;

    /*
     * data[1..len-1] = encrypted Upper Transport PDU
     * The last 4 bytes of that range are the 32-bit TransMIC.
     * No SZMIC field exists here — TransMIC is always 32 bits.
     */
    uint8_t *upper_pdu = &data[1];
    size_t   upper_len = len - 1;

    if (akf) {
        /*
         * Payload was encrypted with an Application Key.
         * Use 'aid' to look up the matching AppKey from the
         * device's key database, then decrypt.
         */
        int err = mesh_decrypt_appkey(aid, src, upper_pdu, upper_len);
        if (err < 0) {
            /* Wrong key or corrupted TransMIC — discard */
            return;
        }
    } else {
        /*
         * Payload was encrypted with the Device Key.
         * 'aid' is ignored in this case.
         */
        int err = mesh_decrypt_devkey(src, upper_pdu, upper_len);
        if (err < 0) {
            return;
        }
    }

    /* Hand decrypted application payload to the upper transport layer */
    upper_transport_receive(src, upper_pdu, upper_len);
}

4 — Segmented Access Message: Breaking Large Payloads Apart
SEG = 1 SZMIC SeqZero (13 bits) SegO / SegN 12-byte chunks Reassembly buffer

Why segmentation is necessary

A single Bluetooth Mesh Network PDU can carry at most 15 bytes of Lower Transport payload. But an upper-layer model message — say, a firmware update chunk or a long vendor model command — can be far larger. The lower transport layer handles this by slicing the Upper Transport PDU into 12-byte chunks and sending each one in a separate Network PDU.

Header fields in a Segmented Access PDU

Each segment PDU carries a 4-byte header followed by the data chunk:

  • SEG (bit 7 of byte 0) = 1 — Marks this as a segment, not a complete message.
  • AKF + AID — Same meaning as in the unsegmented format: identifies which key was used.
  • SZMIC (1 bit) — Selects the TransMIC size of the final reassembled message: 0 → 32-bit TransMIC, 1 → 64-bit TransMIC. Individual segment PDUs do not carry a TransMIC themselves.
  • SeqZero (13 bits) — The 13 least-significant bits of the sequence number assigned to the first segment. Every segment of the same message carries the same SeqZero, so the receiver can group them together.
  • SegO (5 bits) — Segment Offset, zero-based. SegO = 0 is the first chunk, SegO = 1 is the second, and so on.
  • SegN (5 bits) — The index of the final segment (zero-based). A value of 3 means there are 4 segments total (0, 1, 2, 3).
  • Segment m (up to 12 bytes) — The actual data slice. All segments except the last carry exactly 12 bytes; the last segment carries the remaining bytes.

All segments of the same Upper Transport PDU share the same AKF, AID, SZMIC, SeqZero, and SegN values. Only SegO and the payload differ between segments.

/*
 * mesh/transport.c — Segmented Access Message: parsing and reassembly
 *
 * Byte 0:  [SEG=1 | AKF | AID(6)]
 * Byte 1:  [SZMIC | SeqZero(12:6)]
 * Byte 2:  [SeqZero(5:0) | SegO(4:3)]
 * Byte 3:  [SegO(2:0) | SegN(4:0)]
 * Bytes 4+: Segment m data (up to 12 bytes)
 */

#include <stdint.h>
#include <stdbool.h>
#include <string.h>

#define MAX_SEG_SESSIONS   4
#define MAX_SEGMENTS       32
#define SEGMENT_SIZE       12   /* Each segment carries 12 bytes */

struct seg_rx_sess {
    uint16_t src;
    uint16_t seq_zero;
    uint8_t  seg_n;               /* Index of last segment            */
    uint32_t block_ack;           /* Bitmask: bit N set = seg N rcvd  */
    uint8_t  buf[MAX_SEGMENTS * SEGMENT_SIZE];
    bool     active;
};

static struct seg_rx_sess rx_sessions[MAX_SEG_SESSIONS];

/* Locate an active session or claim a free slot */
static struct seg_rx_sess *get_session(uint16_t src, uint16_t seq_zero)
{
    int i, free_slot = -1;

    for (i = 0; i < MAX_SEG_SESSIONS; i++) {
        if (rx_sessions[i].active &&
            rx_sessions[i].src      == src      &&
            rx_sessions[i].seq_zero == seq_zero) {
            return &rx_sessions[i];   /* Existing session */
        }
        if (!rx_sessions[i].active && free_slot < 0)
            free_slot = i;
    }

    if (free_slot < 0)
        return NULL;   /* All slots in use */

    /* Initialise a new session */
    rx_sessions[free_slot].src       = src;
    rx_sessions[free_slot].seq_zero  = seq_zero;
    rx_sessions[free_slot].block_ack = 0;
    rx_sessions[free_slot].active    = true;
    return &rx_sessions[free_slot];
}

/* Extract the packed header fields from bytes 0-3 */
static void parse_seg_hdr(const uint8_t *data,
                           bool *akf, uint8_t *aid, bool *szmic,
                           uint16_t *seq_zero, uint8_t *seg_o, uint8_t *seg_n)
{
    *akf      = (data[0] & 0x40) != 0;
    *aid      =  data[0] & 0x3F;
    *szmic    = (data[1] & 0x80) != 0;
    *seq_zero = ((uint16_t)(data[1] & 0x7F) << 6) | (data[2] >> 2);
    *seg_o    = ((data[2] & 0x03) << 3) | (data[3] >> 5);
    *seg_n    =   data[3] & 0x1F;
}

void handle_seg_access(uint16_t src, uint32_t seq,
                        uint8_t *data, size_t len)
{
    bool    akf, szmic;
    uint8_t aid, seg_o, seg_n;
    uint16_t seq_zero;

    parse_seg_hdr(data, &akf, &aid, &szmic, &seq_zero, &seg_o, &seg_n);

    /* Segment payload starts at byte 4 */
    uint8_t *seg_data = &data[4];
    size_t   seg_len  = len - 4;   /* Up to 12 bytes for non-last segment */

    struct seg_rx_sess *sess = get_session(src, seq_zero);
    if (!sess) return;   /* Resource exhaustion */

    sess->seg_n = seg_n;

    /* Copy segment into the correct position in the reassembly buffer */
    memcpy(&sess->buf[seg_o * SEGMENT_SIZE], seg_data, seg_len);

    /* Record this segment as received */
    sess->block_ack |= (1u << seg_o);

    /* All segments received when every bit up to seg_n is set */
    uint32_t full_mask = (1u << (seg_n + 1)) - 1;
    if (sess->block_ack == full_mask) {
        /* Reassembly complete — hand up to the upper transport layer */
        size_t total = (size_t)seg_n * SEGMENT_SIZE + seg_len;
        upper_transport_receive(src, sess->buf, total);
        sess->active = false;
    }

    /* Always ACK after each received segment */
    send_seg_ack(src, seq_zero, sess->block_ack);
}

5 — Unsegmented Control Message and Segment Acknowledgment
Opcode 7 bits ACK opcode 0x00 OBO flag BlockAck 32-bit Selective retransmit

What is an Unsegmented Control Message?

An Unsegmented Control Message carries either a Segment Acknowledgment or a Transport Control message. The first byte holds SEG = 0 (in bit 7) followed by a 7-bit Opcode field that selects which of these it is:

  • Opcode 0x00 → Segment Acknowledgment
  • Opcodes 0x01 to 0x7F → Transport Control messages (heartbeat, friendship, etc.)

Segment Acknowledgment in detail

The Segment Acknowledgment is the mechanism that lets the sender know which segments have been received, so it can retransmit only the missing ones rather than all of them. Its 7-byte PDU structure is:

  • Byte 0: 0x00 — SEG=0 plus opcode 0x00.
  • OBO (1 bit): On Behalf Of flag. Set to 1 by a Friend node sending the ACK on behalf of a Low Power node. Normally 0.
  • SeqZero (13 bits): Identifies which segmented message this ACK belongs to — matches the SeqZero in the incoming Segmented Access PDUs.
  • RFU (2 bits): Reserved, always zero.
  • BlockAck (32 bits, big-endian): A bitmask where bit N being set means segment N was received successfully. The sender inspects this mask to find which bits are clear and retransmits those segments.
/*
 * mesh/transport.c — Build and send a Segment Acknowledgment
 *
 * PDU layout (7 bytes, CTL=1 so network layer knows it is control traffic):
 *   Byte 0:   [SEG=0 | Opcode = 0x00]  →  0x00
 *   Byte 1:   [OBO(1) | SeqZero(12:6)]
 *   Byte 2:   [SeqZero(5:0) | RFU(1:0) = 0]
 *   Bytes 3-6: BlockAck, big-endian
 */

#include <stdint.h>
#include <stdbool.h>
#include <string.h>

#define SEG_ACK_OPCODE  0x00
#define SEG_ACK_LEN     7

static void build_seg_ack(uint8_t *buf, bool obo,
                           uint16_t seq_zero, uint32_t block_ack)
{
    /* Byte 0: SEG=0, Opcode=0x00 */
    buf[0] = SEG_ACK_OPCODE;

    /* Bytes 1-2: OBO + SeqZero(13 bits) + 2 zero RFU bits */
    buf[1] = (obo ? 0x80 : 0x00) | ((seq_zero >> 6) & 0x7F);
    buf[2] = (uint8_t)((seq_zero & 0x3F) << 2);   /* RFU bits are 0 */

    /* Bytes 3-6: BlockAck in big-endian order */
    buf[3] = (block_ack >> 24) & 0xFF;
    buf[4] = (block_ack >> 16) & 0xFF;
    buf[5] = (block_ack >>  8) & 0xFF;
    buf[6] = (block_ack      ) & 0xFF;
}

/*
 * Called after each segment is received, or when a retransmission timer fires.
 * dst  : unicast address of the originating node that sent the segments.
 */
void send_seg_ack(uint16_t dst, uint16_t seq_zero, uint32_t block_ack)
{
    uint8_t pdu[SEG_ACK_LEN];

    build_seg_ack(pdu, /*obo=*/false, seq_zero, block_ack);

    /* Transmit via network layer; CTL=1 marks this as a control PDU */
    net_transmit(dst, /*ctl=*/true, pdu, SEG_ACK_LEN);
}

/* ------------------------------------------------------------------ */

/*
 * On the TX side: process a received Segment Acknowledgment and
 * retransmit any missing segments.
 */
void process_seg_ack(uint8_t *data, size_t len)
{
    if (len < SEG_ACK_LEN)
        return;

    uint16_t seq_zero = ((uint16_t)(data[1] & 0x7F) << 6) |
                         (data[2] >> 2);
    uint32_t block_ack = ((uint32_t)data[3] << 24) |
                         ((uint32_t)data[4] << 16) |
                         ((uint32_t)data[5] <<  8) |
                          (uint32_t)data[6];

    struct seg_tx_sess *sess = find_tx_session(seq_zero);
    if (!sess) return;

    /* Retransmit every segment whose bit is NOT yet set */
    for (int i = 0; i <= sess->seg_n; i++) {
        if (!(block_ack & (1u << i))) {
            retransmit_segment(sess, i);
        }
    }

    /* All segments confirmed — clean up TX session */
    uint32_t full_mask = (1u << (sess->seg_n + 1)) - 1;
    if (block_ack == full_mask) {
        sess->active = false;
    }
}

6 — End-to-End Data Flow: TX and RX Paths Together
TX segmentation path RX reassembly path BlockAck loop Retransmission

Sending a large message (TX path)

  1. The Upper Transport Layer passes a large PDU down to the Lower Transport Layer.
  2. The Lower Transport Layer determines how many 12-byte chunks are needed: SegN = ceil(payload / 12) - 1.
  3. It assigns a SeqZero value (taken from the current network sequence counter) and constructs a Segmented Access PDU for each chunk, filling in SegO = 0, 1, 2 … SegN.
  4. All segments are handed to the Network Layer for transmission.
  5. A retransmission timer starts. When a Segment Acknowledgment arrives, its BlockAck mask is inspected. Any segment whose bit is clear is retransmitted.
  6. Once the BlockAck shows all bits set, the TX session is closed.

Receiving segmented messages (RX path)

  1. Each arriving Network PDU is checked against the Network Message Cache — duplicates are silently dropped.
  2. The SEG and CTL bits are read. For a Segmented Access PDU, the receiver locates or creates a reassembly session keyed on (src, seq_zero).
  3. The segment data is copied into the reassembly buffer at offset SegO × 12, and bit SegO is set in the BlockAck bitmask.
  4. A Segment Acknowledgment carrying the current BlockAck is sent back to the originator after every received segment.
  5. When the BlockAck matches the full expected mask (1 << (SegN+1)) - 1, all chunks are present and the reassembled PDU is passed to the Upper Transport Layer.
/*
 * mesh/transport.c — TX path: segment an Upper Transport PDU
 *
 * pdu     : Upper Transport Access PDU to be segmented
 * pdu_len : Length of the PDU (must be > 15 bytes to require segmentation)
 * dst     : Destination unicast/group address
 * akf     : Application Key Flag
 * aid     : Application Key Identifier
 * szmic   : 0 = 32-bit TransMIC, 1 = 64-bit TransMIC
 */

#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <math.h>

#define SEGMENT_SIZE   12

void lower_transport_send_segmented(const uint8_t *pdu, size_t pdu_len,
                                     uint16_t dst, bool akf, uint8_t aid,
                                     bool szmic, uint16_t seq_zero)
{
    /* How many segments do we need? */
    uint8_t seg_n = (uint8_t)((pdu_len + SEGMENT_SIZE - 1) / SEGMENT_SIZE) - 1;

    for (uint8_t seg_o = 0; seg_o <= seg_n; seg_o++) {
        uint8_t  tx_buf[4 + SEGMENT_SIZE];
        size_t   offset   = seg_o * SEGMENT_SIZE;
        size_t   seg_len  = (offset + SEGMENT_SIZE <= pdu_len)
                              ? SEGMENT_SIZE
                              : pdu_len - offset;

        /* Byte 0: SEG=1, AKF, AID */
        tx_buf[0] = 0x80 | (akf ? 0x40 : 0x00) | (aid & 0x3F);

        /* Byte 1: SZMIC + SeqZero[12:6] */
        tx_buf[1] = (szmic ? 0x80 : 0x00) | ((seq_zero >> 6) & 0x7F);

        /* Byte 2: SeqZero[5:0] | SegO[4:3] */
        tx_buf[2] = (uint8_t)((seq_zero & 0x3F) << 2) | (seg_o >> 3);

        /* Byte 3: SegO[2:0] | SegN[4:0] */
        tx_buf[3] = (uint8_t)((seg_o & 0x07) << 5) | (seg_n & 0x1F);

        /* Bytes 4+: segment payload */
        memcpy(&tx_buf[4], &pdu[offset], seg_len);

        /* CTL=0 because this is access traffic */
        net_transmit(dst, /*ctl=*/false, tx_buf, 4 + seg_len);
    }
}

Quick reference: field sizes

Field Size (bits) Present in Purpose
SEG 1 All formats 0 = complete, 1 = segment
AKF 1 Access formats Which key type was used
AID 6 Access formats Application key index
SZMIC 1 Segmented Access 32-bit or 64-bit TransMIC
SeqZero 13 Segmented formats + ACK Groups segments of one message
SegO 5 Segmented formats This segment’s position (0-based)
SegN 5 Segmented formats Last segment’s index
BlockAck 32 Segment ACK Bitmask of received segments

Ready to Go Deeper?

This tutorial covered the Lower Transport Layer of Bluetooth Mesh — the Network Message Cache, the four PDU formats, unsegmented and segmented access messages, and how the Segment Acknowledgment BlockAck mechanism drives selective retransmission. The natural next step is the Upper Transport Layer, which encrypts and authenticates the payloads that the lower transport layer carries.

Leave a Reply

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