BLE MESH TUTORIAL Data Encoding, Mesh Features & Bearer Layer

BLE MESH TUTORIAL Data Encoding, Mesh Features & Bearer Layer

Part 1 of 3 — Data Encoding, Mesh Features & Bearer Layer

Spec
BT Mesh v1.0
Level
Intermediate
Stack
BlueZ / Linux
Layer
Bearer

Why Study Mesh Internals?

Most BLE tutorials stop at GATT services. Bluetooth Mesh is an entirely different architecture. It builds a publish-subscribe network on top of BLE advertising — devices do not pair with each other; they broadcast encrypted PDUs that any node in range can relay, decrypt, and act on.

To write solid mesh firmware or debug a BlueZ-based mesh daemon you need to know exactly how bytes are packed, how they travel over the air, and how the network layer routes incoming PDUs. This three-part series works through the Bluetooth Mesh Profile specification section by section, grounding every concept in real BlueZ code.

This part covers Section 3.1 (field ordering and byte order), Section 3.2 (the four optional features), and Section 3.3 (advertising bearer and GATT bearer).

Topics in this post

Big Endian Little Endian Field Ordering Relay Feature Proxy Feature Friend Feature Low Power Node Advertising Bearer GATT Bearer Mesh Message AD Type Mesh Proxy Service BlueZ HCI ADV_NONCONN_IND CCCD

1. Data Encoding: Fields, Bit Positions and Byte Order

1.1 How Multi-Field Values Are Assembled

The Mesh Profile specification defines protocol fields in tables. Each row is a named field with a fixed bit width. Before transmission, all fields in a table are merged into a single binary number, and that number is serialised onto the wire. Two independent choices govern the result:

  • Field ordering: which end of the combined number a field occupies — decided by whether the layer uses big-endian or little-endian field ordering.
  • Byte order (endianness): which octet of the combined number is placed on the wire first.

These two choices interact. Get them backwards and every multi-byte value decodes to the wrong number. The subsections below show both variants with worked examples and C code.

1.2 Big-Endian Field Ordering

When a layer uses big-endian ordering, the first field in the table is placed in the most significant bit positions of the combined number. The combined number is then transmitted most-significant-octet first.

Worked example — three fields packed big endian:

bit 31    28 27               16 15                       0
Field 0
4 bits = 0x6
Field 1
12 bits = 0x987
Field 2
16 bits = 0x1234

▲ Field 0 fills the MSB region; Field 2 fills the LSB region.

Combined 32-bit value: 0x69871234

Octet 0 (first on wire) Octet 1 Octet 2 Octet 3 (last)
0x69 0x87 0x12 0x34
MSB first LSB last

Transmitted wire bytes: 0x69, 0x87, 0x12, 0x34

1.3 Little-Endian Field Ordering

When a layer uses little-endian ordering, the first field in the table occupies the least significant bit positions of the combined number. The number is then transmitted least-significant-octet first. The field assignment is therefore reversed compared to big endian.

Same three fields, little-endian ordering:

Building the combined number from LSB to MSB:

  • Bits [3:0] ← Field 0 (0x6)
  • Bits [15:4] ← Field 1 (0x987)
  • Bits [31:16] ← Field 2 (0x1234)

bit 31                       16 15               4 3    0
Field 2
16 bits = 0x1234
Field 1
12 bits = 0x987
Field 0
4 bits = 0x6

▲ Field 0 fills the LSB region; Field 2 fills the MSB region.

Combined 32-bit value: 0x12349876

Octet 0 (first on wire) Octet 1 Octet 2 Octet 3 (last)
0x76 0x98 0x34 0x12
LSB first MSB last

Transmitted wire bytes: 0x76, 0x98, 0x34, 0x12

Common mistake: Developers sometimes confuse field ordering (which bit position a field lands on) with byte order (which octet goes first on the wire). They are separate decisions. A field that ends up in the MSB position is not the same as big-endian byte transmission — the two must both be applied to get the correct wire bytes.

1.4 Implementing Field Packing in C — BlueZ Style

BlueZ uses inline helpers for endian-safe reads and writes throughout its mesh code (mesh/net.c, mesh/crypto.c). The pattern below demonstrates both little-endian field packing and big-endian field packing from scratch, matching the spec examples exactly.

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

/* ---------------------------------------------------------------
 * Little-endian field packing
 * field0 : 4-bit value
 * field1 : 12-bit value
 * field2 : 16-bit value
 * Returns the 32-bit combined number before wire serialisation.
 * --------------------------------------------------------------- */
static uint32_t pack_le(uint8_t f0, uint16_t f1, uint16_t f2)
{
    uint32_t n = 0;
    n |= (uint32_t)(f0 & 0x0F);               /* bits [3:0]  */
    n |= (uint32_t)(f1 & 0x0FFF) << 4;        /* bits [15:4] */
    n |= (uint32_t)(f2 & 0xFFFF) << 16;       /* bits [31:16]*/
    return n;
}

/* ---------------------------------------------------------------
 * Big-endian field packing
 * field0 : 4-bit value  (placed at MSB)
 * field1 : 12-bit value
 * field2 : 16-bit value (placed at LSB)
 * --------------------------------------------------------------- */
static uint32_t pack_be(uint8_t f0, uint16_t f1, uint16_t f2)
{
    uint32_t n = 0;
    n |= (uint32_t)(f0 & 0x0F) << 28;        /* bits [31:28] */
    n |= (uint32_t)(f1 & 0x0FFF) << 16;      /* bits [27:16] */
    n |= (uint32_t)(f2 & 0xFFFF);             /* bits [15:0]  */
    return n;
}

/* Serialise a 32-bit value in little-endian byte order */
static void write_le32(uint32_t v, uint8_t *out)
{
    out[0] = (uint8_t)(v);
    out[1] = (uint8_t)(v >>  8);
    out[2] = (uint8_t)(v >> 16);
    out[3] = (uint8_t)(v >> 24);
}

/* Serialise a 32-bit value in big-endian byte order */
static void write_be32(uint32_t v, uint8_t *out)
{
    out[0] = (uint8_t)(v >> 24);
    out[1] = (uint8_t)(v >> 16);
    out[2] = (uint8_t)(v >>  8);
    out[3] = (uint8_t)(v);
}

int main(void)
{
    uint8_t wire[4];

    /* --- Little-endian example --- */
    uint32_t le_num = pack_le(0x6, 0x987, 0x1234);
    printf("LE combined: 0x%08X\n", le_num);   /* 0x12349876 */
    write_le32(le_num, wire);
    printf("LE wire: %02X %02X %02X %02X\n",
           wire[0], wire[1], wire[2], wire[3]); /* 76 98 34 12 */

    /* --- Big-endian example --- */
    uint32_t be_num = pack_be(0x6, 0x987, 0x1234);
    printf("BE combined: 0x%08X\n", be_num);   /* 0x69871234 */
    write_be32(be_num, wire);
    printf("BE wire: %02X %02X %02X %02X\n",
           wire[0], wire[1], wire[2], wire[3]); /* 69 87 12 34 */

    return 0;
}

Compile with: gcc -o endian_demo endian_demo.c && ./endian_demo

Both outputs match the spec examples exactly. This kind of sanity-check program is worth writing before touching any mesh PDU parser.

2. The Four Optional Mesh Node Features

Not every mesh node needs the same capabilities. The spec defines four optional features that a node may implement depending on its role in the network. Each feature is independently enabled or disabled and each has resource implications. Understanding them upfront makes provisioning and configuration decisions much clearer.

Feature What it does Typical hardware Power cost
Relay Re-broadcasts mesh PDUs to extend network range Mains-powered hubs, smart bulbs, routers High (always scanning)
Proxy Bridges GATT clients into the advertising-based mesh Gateway nodes, provisioner devices Medium
Friend Buffers messages on behalf of a sleeping Low Power node Lighting controllers, smart hubs Medium–High
Low Power Wakes periodically to poll its Friend for queued messages Coin-cell sensors, door/window detectors Very low
2.1 Relay Feature — Extending Coverage Hop by Hop

The core relay mechanism is simple: a Relay node receives a valid mesh PDU over the advertising bearer, checks that the TTL (Time To Live) field is greater than 1, decrements TTL by one, and retransmits the PDU. This turns a mesh of individually short-range nodes into a network that can span entire buildings without any infrastructure changes.

The spec permits a Relay node to retransmit the same PDU multiple times within a short window to compensate for BLE advertising channel collisions. The retransmission count and inter-retransmission interval are stored as configuration state and are managed in BlueZ mesh via the org.bluez.mesh.Management1 D-Bus interface.

/* Simplified relay decision — conceptual replica of
 * the logic in BlueZ mesh/net.c net_relay_to_subnet()
 */

#define MESH_TTL_MASK   0x7F   /* lower 7 bits of CTL|TTL byte */
#define MESH_CTL_MASK   0x80   /* bit 7 = CTL flag             */

/* Returns 1 if the PDU should be relayed, 0 otherwise.
 * pdu[0] = IVI | NID
 * pdu[1] = CTL | TTL
 */
int mesh_should_relay(const uint8_t *pdu)
{
    uint8_t ttl = pdu[1] & MESH_TTL_MASK;
    /* TTL 0 = exhausted; TTL 1 = not to be relayed further */
    return ttl > 1;
}

/* Decrement TTL and copy into relay buffer */
void mesh_prepare_relay_pdu(const uint8_t *pdu, uint8_t *relay_buf,
                             uint16_t len)
{
    memcpy(relay_buf, pdu, len);
    uint8_t ctl = relay_buf[1] & MESH_CTL_MASK;
    uint8_t ttl = (relay_buf[1] & MESH_TTL_MASK) - 1;
    relay_buf[1] = ctl | ttl;
}
2.2 Friend & Low Power Node — The Sleep-and-Poll Contract

These two features are designed as a pair. A Low Power Node (LPN) cannot afford to keep its radio receiver running continuously — it would drain a coin cell in days. Instead it negotiates a friendship with a nearby, well-powered Friend Node using a four-message handshake.

After the friendship is established, the Friend stores incoming messages destined for the LPN in an internal message queue. The LPN wakes at its configured Poll Interval, transmits a Friend Poll PDU, receives any queued messages bundled into Friend Update responses, then goes back to sleep immediately.

Low Power Node (LPN) direction Friend Node
Friend Request
Friend Offer
Friend Poll
Friend Update (subscription confirmed)
sleeps for Poll Interval (T ms) queues incoming messages for LPN
Friend Poll
Friend Update + queued message(s)
sleeps again clears delivered messages from queue

In BlueZ mesh, the LPN state machine lives in mesh/friend.c. The Poll Interval and other friendship parameters are negotiated during the Friend Request / Friend Offer exchange and stored in the mesh configuration JSON.

3. Mesh Bearers — Moving PDUs Over the Air

A bearer is the lowest-level transport that carries mesh PDUs. The spec defines two: an advertising bearer (the primary path used by native mesh devices) and a GATT bearer (a compatibility path for devices that cannot participate in raw advertising-mode mesh). Every node must support at least one of the two.

3.1 Advertising Bearer — Architecture and Rules

The advertising bearer wraps each Network PDU inside a BLE advertisement packet. The AD Type used is 0x2A (Mesh Message) as assigned by the Bluetooth SIG. The payload of that AD structure is the complete Network PDU.

Three mandatory rules govern advertising-bearer advertisements:

  1. The event type must be ADV_NONCONN_IND (non-connectable, non-scannable, undirected). Any mesh message arriving inside a connectable or scannable advertisement must be silently discarded by receivers.
  2. The Flags AD Type (normally required in connectable advertisements) is intentionally omitted. Skipping it frees two octets that are added directly to the Network PDU payload — a deliberate bandwidth optimisation in the spec.
  3. The inter-packet gap within a single advertising event should be randomised to reduce the probability of collisions across all three BLE advertising channels (37, 38, 39).

Advertising packet structure:

Length
1 octet
AD Type
0x2A = Mesh Message
1 octet
Network PDU
up to 29 octets (max BLE adv payload minus 2)

The Length byte carries the count of the AD Type byte plus the Network PDU bytes. The receiver reads Length, extracts the AD Type, and hands the remaining bytes to the mesh network layer.

Scanning requirement: a device that relies solely on the advertising bearer should perform passive scanning with a duty cycle as close to 100 % as its hardware allows. Both the GAP Observer role (scanning) and GAP Broadcaster role (advertising) are mandatory for all mesh devices.

3.2 Advertising Bearer — BlueZ HCI C Implementation

Below is a minimal C function that configures a non-connectable undirected BLE advertisement carrying a mesh Network PDU. It uses BlueZ’s hci_lib to send raw HCI commands directly to the controller. In production you would use bluetooth-meshd and its D-Bus API; this low-level approach is useful for writing protocol test tools or for studying exactly what goes on the wire.

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/hci_lib.h>

#define MESH_MSG_AD_TYPE   0x2A   /* «Mesh Message» AD Type */

/*
 * Configure and enable a non-connectable, non-scannable undirected
 * advertisement carrying a Bluetooth Mesh Message AD Type.
 *
 * hci_dev_id : HCI device index (0 for hci0)
 * net_pdu    : Network PDU bytes
 * net_len    : Length of net_pdu (max 29 bytes given Flags omitted)
 *
 * Returns 0 on success, -1 on error.
 */
int mesh_start_adv_bearer(int hci_dev_id,
                           const uint8_t *net_pdu,
                           uint8_t net_len)
{
    int dd;
    struct hci_request rq;
    uint8_t status;

    le_set_advertising_parameters_cp ap;
    le_set_advertising_data_cp ad;
    le_set_advertise_enable_cp ae;

    if (net_len > 29) {
        fprintf(stderr, "Network PDU too long (%u > 29)\n", net_len);
        return -1;
    }

    dd = hci_open_dev(hci_dev_id);
    if (dd < 0) {
        perror("hci_open_dev");
        return -1;
    }

    /* ---- Step 1: Set advertising parameters ---- */
    memset(&ap, 0, sizeof(ap));
    ap.min_interval = htobs(0x00A0);   /* 100 ms */
    ap.max_interval = htobs(0x00A0);
    ap.advtype      = 0x03;            /* ADV_NONCONN_IND */
    ap.own_bdaddr_type = 0x00;         /* public address  */
    ap.chan_map     = 0x07;            /* channels 37, 38, 39 */

    memset(&rq, 0, sizeof(rq));
    rq.ogf    = OGF_LE_CTL;
    rq.ocf    = OCF_LE_SET_ADVERTISING_PARAMETERS;
    rq.cparam = &ap;
    rq.clen   = LE_SET_ADVERTISING_PARAMETERS_CP_SIZE;
    rq.rparam = &status;
    rq.rlen   = 1;
    if (hci_send_req(dd, &rq, 1000) < 0 || status) {
        perror("LE_SET_ADVERTISING_PARAMETERS");
        goto out;
    }

    /* ---- Step 2: Build and set advertising data ---- */
    /*
     * AD structure layout in the 31-byte payload:
     *   [0]          : length = 1 (AD type byte) + net_len
     *   [1]          : 0x2A  = Mesh Message AD Type
     *   [2 .. 2+n-1] : Network PDU bytes
     */
    memset(&ad, 0, sizeof(ad));
    ad.length   = 2 + net_len;         /* total used bytes in data[] */
    ad.data[0]  = 1 + net_len;         /* per-structure length field */
    ad.data[1]  = MESH_MSG_AD_TYPE;
    memcpy(&ad.data[2], net_pdu, net_len);

    memset(&rq, 0, sizeof(rq));
    rq.ogf    = OGF_LE_CTL;
    rq.ocf    = OCF_LE_SET_ADVERTISING_DATA;
    rq.cparam = &ad;
    rq.clen   = LE_SET_ADVERTISING_DATA_CP_SIZE;
    rq.rparam = &status;
    rq.rlen   = 1;
    if (hci_send_req(dd, &rq, 1000) < 0 || status) {
        perror("LE_SET_ADVERTISING_DATA");
        goto out;
    }

    /* ---- Step 3: Enable advertising ---- */
    memset(&ae, 0, sizeof(ae));
    ae.enable = 0x01;

    memset(&rq, 0, sizeof(rq));
    rq.ogf    = OGF_LE_CTL;
    rq.ocf    = OCF_LE_SET_ADVERTISE_ENABLE;
    rq.cparam = &ae;
    rq.clen   = LE_SET_ADVERTISE_ENABLE_CP_SIZE;
    rq.rparam = &status;
    rq.rlen   = 1;
    if (hci_send_req(dd, &rq, 1000) < 0 || status) {
        perror("LE_SET_ADVERTISE_ENABLE");
        goto out;
    }

    printf("Mesh advertising bearer started (ADV_NONCONN_IND, "
           "AD Type 0x2A, %u-byte Network PDU)\n", net_len);
    hci_close_dev(dd);
    return 0;

out:
    hci_close_dev(dd);
    return -1;
}

/* Example usage */
int main(void)
{
    /* Dummy 10-byte Network PDU (replace with real encrypted PDU) */
    uint8_t dummy_net_pdu[] = {
        0x68, 0xCA, 0xBC, 0x0A, 0xE0, 0x34, 0xD5, 0x47, 0x30, 0x01
    };
    return mesh_start_adv_bearer(0, dummy_net_pdu, sizeof(dummy_net_pdu));
}

Compile: gcc -o mesh_adv mesh_adv.c -lbluetooth && sudo ./mesh_adv

The call requires root or CAP_NET_RAW because it opens a raw HCI socket. Use hcidump -X in a second terminal to verify the ADV_NONCONN_IND packet and the 0x2A AD Type on the air.

3.3 GATT Bearer — Architecture

The GATT bearer exists because some devices — a smartphone running a mesh provisioner or configurator app, for example — cannot inject PDUs directly into the BLE advertising stream. The GATT bearer creates a standard GATT connection between the phone (GATT Bearer Client) and a Proxy-capable mesh node (GATT Bearer Server). The server then acts as a gateway, translating between the GATT interface and the advertising mesh.

GATT Bearer Client
e.g. smartphone / tablet
GATT Client role
Write Without Response →
Mesh Proxy Data In
← Notifications
Mesh Proxy Data Out
GATT connection (Proxy Protocol)
GATT Bearer Server
Proxy-capable mesh node
GATT Server role
The GATT Bearer Server injects the Proxy PDU into the advertising mesh on behalf of the client device

The GATT Bearer Server must host exactly one Mesh Proxy Service, containing two mandatory characteristics:

  • Mesh Proxy Data In (UUID 0x2ADD) — write-only, Write Without Response. The client sends Proxy PDUs here.
  • Mesh Proxy Data Out (UUID 0x2ADE) — notify-capable. The server pushes Proxy PDUs to the client. Each notification contains exactly one Proxy PDU.
3.4 GATT Bearer — Service Discovery Sequence

Before the client can exchange Proxy PDUs it must discover the Mesh Proxy Service and enable notifications on Data Out. The GATT specification requires this as a sequential four-step process:

# GATT Sub-Procedure Purpose
1 Discover All Primary Services
or Discover Primary Services by Service UUID
Locate Mesh Proxy Service (UUID 0x1828) handle range
2 Discover All Characteristics of a Service
or Discover Characteristics by UUID
Find Data In (0x2ADD) and Data Out (0x2ADE) characteristic handles
3 Discover All Characteristic Descriptors Find the CCCD (UUID 0x2902) of the Data Out characteristic
4 Write CCCD (value 0x0001) Enable notifications on Mesh Proxy Data Out — data flow can now begin

The client must be tolerant of additional optional characteristics in the service record. The spec explicitly states this because vendors may extend the Mesh Proxy Service with proprietary characteristics for firmware updates or diagnostics.

3.5 GATT Bearer — Python Client Using BlueZ via bluepy

The script below performs the full service discovery sequence and then exchanges a Proxy PDU. It uses bluepy, which wraps BlueZ’s GATT stack and exposes a clean Python API. This is the quickest way to test a Proxy-capable mesh node from a Linux laptop.

#!/usr/bin/env python3
"""
Minimal GATT Bearer Client for Bluetooth Mesh Proxy Service.
Performs full service discovery per Section 3.3.2 of the Mesh Profile spec.

Requirements: pip install bluepy
Usage:        python3 mesh_gatt_client.py AA:BB:CC:DD:EE:FF
"""

import sys, struct, time
from bluepy.btle import Peripheral, UUID, DefaultDelegate, BTLEException

# Mesh Proxy Service and Characteristic UUIDs (assigned by Bluetooth SIG)
UUID_MESH_PROXY_SVC  = UUID(0x1828)
UUID_MESH_DATA_IN    = UUID(0x2ADD)   # Write Without Response
UUID_MESH_DATA_OUT   = UUID(0x2ADE)   # Notify
UUID_CCCD            = UUID(0x2902)   # Client Characteristic Configuration

# Proxy PDU message type values (SAR field = 00, type in lower nibble)
PROXY_TYPE_NETWORK   = 0x00
PROXY_TYPE_BEACON    = 0x01
PROXY_TYPE_CONFIG    = 0x02
PROXY_TYPE_PROV      = 0x03


class MeshProxyDelegate(DefaultDelegate):
    """Handles incoming Proxy PDU notifications from the mesh node."""

    def __init__(self, data_out_handle):
        super().__init__()
        self._handle = data_out_handle

    def handleNotification(self, cHandle, data):
        if cHandle == self._handle:
            msg_type = data[0] & 0x3F   # lower 6 bits
            print(f"  [Proxy IN] type={msg_type:#04x}  "
                  f"payload={data[1:].hex()}")


class MeshGattClient:
    def __init__(self, mac: str):
        self._mac = mac
        self._periph = None
        self._data_in  = None
        self._data_out = None

    def connect(self):
        print(f"Connecting to {self._mac} ...")
        self._periph = Peripheral(self._mac)

        # Step 1 — discover Mesh Proxy Service
        svc = self._periph.getServiceByUUID(UUID_MESH_PROXY_SVC)
        if svc is None:
            raise RuntimeError("Mesh Proxy Service (0x1828) not found")
        print("  Mesh Proxy Service found")

        # Step 2 — discover characteristics
        chars = {str(c.uuid): c for c in svc.getCharacteristics()}
        self._data_in  = chars.get(str(UUID_MESH_DATA_IN))
        self._data_out = chars.get(str(UUID_MESH_DATA_OUT))
        if not self._data_in or not self._data_out:
            raise RuntimeError("Data In or Data Out characteristic missing")

        print(f"  Data In  handle : {self._data_in.getHandle():#06x}")
        print(f"  Data Out handle : {self._data_out.getHandle():#06x}")

        # Step 3 + 4 — find CCCD and enable notifications on Data Out
        # The CCCD descriptor sits at handle = characteristic handle + 1
        cccd_handle = self._data_out.getHandle() + 1
        self._periph.writeCharacteristic(cccd_handle,
                                         struct.pack("<H", 0x0001),
                                         withResponse=True)
        print("  Notifications enabled on Mesh Proxy Data Out")

        # Register delegate for incoming notifications
        self._periph.setDelegate(
            MeshProxyDelegate(self._data_out.getHandle())
        )

    def send_proxy_pdu(self, msg_type: int, payload: bytes):
        """
        Send a Proxy PDU to the mesh node via Mesh Proxy Data In.
        Proxy PDU = 1 header byte + payload bytes.
          header bits [7:6] = SAR (00 = complete message)
          header bits [5:0] = Message Type
        """
        if self._data_in is None:
            raise RuntimeError("Not connected")
        header = (0x00 << 6) | (msg_type & 0x3F)
        pdu = bytes([header]) + payload
        # Write Without Response (withResponse=False)
        self._data_in.write(pdu, withResponse=False)
        print(f"  [Proxy OUT] type={msg_type:#04x}  payload={payload.hex()}")

    def listen(self, seconds: float = 5.0):
        print(f"Listening for notifications ({seconds}s)...")
        deadline = time.time() + seconds
        while time.time() < deadline:
            if not self._periph.waitForNotifications(1.0):
                print("  (no notification)")

    def disconnect(self):
        if self._periph:
            self._periph.disconnect()
            print("Disconnected")


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print(f"Usage: {sys.argv[0]} <MAC address>")
        sys.exit(1)

    client = MeshGattClient(sys.argv[1])
    try:
        client.connect()
        # Send a dummy Network PDU type Proxy PDU (real PDU would be encrypted)
        dummy_payload = bytes.fromhex("68cabca0e034d5473001")
        client.send_proxy_pdu(PROXY_TYPE_NETWORK, dummy_payload)
        client.listen(seconds=8.0)
    except BTLEException as e:
        print(f"BTLE error: {e}")
    finally:
        client.disconnect()

Install: pip3 install bluepy  |  Run: sudo python3 mesh_gatt_client.py AA:BB:CC:DD:EE:FF

The BlueZ mesh daemon handles all of this internally when you use the org.bluez.mesh.Node1 D-Bus API. The code above is valuable for debugging: capture the GATT traffic with btmon while the script runs to see every ATT PDU on the wire.

Continue to Part 2

Part 2 covers the Network Layer in depth: the full Network PDU format, why big-endian byte order is mandatory here, and how the four address types (unassigned, unicast, virtual, group) work at the bit level — all with working BlueZ C code.

Leave a Reply

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