Bluetooth Mesh Architecture: States, Messages, Elements, Addresses & Models

EmbeddedPathashala · Bluetooth Mesh Protocol Series

Bluetooth Mesh Architecture: States, Messages, Elements, Addresses & Models

The complete guide to BLE Mesh building blocks — with BlueZ implementation examples from the meshd stack

7
Core Concepts
3
Address Types
384B
Max SAR Message
32,767
Unicast Addresses

Why This Tutorial Matters

Bluetooth Mesh enables thousands of BLE devices to communicate in a many-to-many topology — used in smart buildings, industrial sensor grids, and street lighting. But before writing a single line of BlueZ mesh code, you need to understand how data is modelled as states, exchanged as messages, organised inside elements, reached via three distinct address types, and encapsulated inside standardised models. These are not abstract concepts — every function call in bluetooth-meshd maps directly to them.

This post walks through all core Bluetooth Mesh architectural concepts defined in the Mesh Profile Specification (sections 2.2.4 – 2.3.6), with practical BlueZ source-level examples throughout.

Topics Covered

GATT Proxy Node Low Power Node (LPN) Friendship Feature Bluetooth Mesh States Composite State Bound States Message Opcodes SAR Segmentation Acknowledged Messages Mesh Elements Primary Element Secondary Element Unicast Address Virtual Address Label UUID Group Address Fixed Group Address Server Model Client Model Control Model BlueZ bluetooth-meshd

1. Legacy Device Support via GATT Proxy

GATT Proxy Node BLE Legacy Device Mesh Proxy Service

Billions of Bluetooth devices — smartphones, tablets, laptops — were designed before Bluetooth Mesh existed. These devices cannot natively send or receive mesh advertisement bearer PDUs. The specification solves this without requiring any OS or hardware update: a GATT Proxy Node sits at the edge of the mesh and bridges standard GATT connections into the mesh advertising bearer.

The proxy node exposes a Mesh Proxy GATT Service. A legacy device connects to this service via a standard BLE connection, writes mesh PDUs as GATT characteristics, and receives incoming mesh messages as GATT notifications. From the mesh network’s perspective, the proxy node forwards these as normal mesh advertisement packets.

GATT Proxy Architecture — Data Path

📱 Legacy BLE Device
Phone / Tablet / PC
Speaks GATT only
No Mesh ADV support
No OS update needed
GATT
BLE Connection
(Encrypted)
🔀 GATT Proxy Node
Mains-powered node
Exposes Mesh Proxy Service
Translates GATT ↔ Mesh PDU
Feature: MESH_FEATURE_PROXY
Mesh ADV
Bearer
(Encrypted)
🕸️ Mesh Network
All other mesh nodes
Relay nodes, sensors
Light controllers, etc.
ADV bearer natively

BlueZ: Enabling GATT Proxy on a Node

In BlueZ, the bluetooth-meshd daemon exposes the Mesh Proxy Service automatically when the node’s feature configuration enables MESH_FEATURE_PROXY. Below is how the proxy feature is checked at the code level.

/* BlueZ mesh/node.c — node feature flag helpers */

#include "mesh/mesh-defs.h"
#include "mesh/node.h"

/*
 * Feature bits (from mesh/mesh-defs.h):
 *   MESH_FEATURE_RELAY  = 0x0001  (bit 0)
 *   MESH_FEATURE_PROXY  = 0x0002  (bit 1)
 *   MESH_FEATURE_FRIEND = 0x0004  (bit 2)
 *   MESH_FEATURE_LPN    = 0x0008  (bit 3)
 */

/* Check whether this node is acting as a GATT Proxy */
bool node_proxy_mode_get(struct mesh_node *node)
{
    /*
     * Returns true when the PROXY feature bit is set.
     * The meshd daemon then advertises the Mesh Proxy
     * Service UUID (0x1828) over a connectable BLE ADV.
     * Legacy GATT clients connect to this node to gain
     * access to the full mesh network.
     */
    return node_get_feature(node, MESH_FEATURE_PROXY);
}
## meshctl — provisioning and connecting from Linux (acts as a GATT client to Proxy Node)

$ sudo bluetooth-meshd --nodetach --debug &

$ meshctl
[meshctl]# menu main

# Scan for unprovisioned mesh devices
[meshctl]# discover-unprovisioned on

[NEW] Device AA:BB:CC:11:22:33  Unprovisioned-Lamp
[NEW] Device AA:BB:CC:44:55:66  Unprovisioned-Switch

# Provision a device into the network
[meshctl]# provision AA:BB:CC:11:22:33

# After provisioning, connect to the local node via GATT
[meshctl]# connect 0100     # 0100 = unicast address of local node

# List all provisioned nodes in the network database
[meshctl]# list-nodes

2. Low Power Support and the Friendship Concept

Low Power Node (LPN) Friend Node Friendship Establishment Energy Harvesting

Battery-powered mesh nodes — door sensors, soil moisture probes, occupancy detectors — must survive for months or years on a coin cell. The mesh specification accommodates this through two mechanisms. First, the spec does not require nodes to coordinate transmissions, establish persistent connections, or restart security on every connection: a node can simply transmit a message and go back to sleep. Second, for devices that cannot even stay awake long enough to receive messages from the mesh bearer, the spec introduces Friendship.

How Friendship Works — Step by Step

Step LPN (Battery Device) Direction Friend Node (Always On) Purpose
1 Broadcasts Friend Request
Includes: RxDelay, RxWindow, criteria
Receives Friend Request;
evaluates if it can serve LPN
LPN announces it needs a Friend
2 Receives Friend Offers
from one or more candidates
Sends Friend Offer
Includes: queue size, RSSI, receive window
Candidate Friends advertise capability
3 Selects best Friend;
sends Friend Poll periodically
Stores messages for LPN;
delivers via Friend Update on Poll
Friendship active; LPN sleeps between polls
4 Wakes, sends data,
or polls for queued messages
Relays LPN messages to mesh;
delivers queued messages to LPN
Normal operation — LPN uses Friend as relay
Key Insight:

Relay nodes — those that retransmit mesh packets to extend range — are always listening and therefore consume significantly more power than a battery could sustain. A Low Power Node deliberately avoids acting as a relay. Instead, it delegates all listening and relaying responsibilities to its Friend Node, waking only to transmit or poll.

BlueZ: Configuring a Low Power Node

/*
 * BlueZ LPN configuration via node.json
 * Located at: ~/.config/meshd/<node-uuid>/node.json
 *
 * To designate a node as a Low Power Node, set "lpn": true
 * and disable relay, proxy, and friend features.
 * The friendPollTimeout is in 100ms units (1000 = 100 sec).
 */

/*  node.json (excerpt for an LPN sensor node):
{
  "cid": "0x05F1",
  "pid": "0x0010",
  "features": {
    "relay":  false,
    "proxy":  false,
    "friend": false,
    "lpn":    true
  },
  "friendPollTimeout":    300,
  "friendReceiveWindow":  255
}
*/

/* mesh/node.c — runtime check for LPN feature */
bool node_lpn_mode_get(struct mesh_node *node)
{
    return node_get_feature(node, MESH_FEATURE_LPN);
}
/* BlueZ mesh/friend.c — Friend Node queues messages for LPN */

/*
 * When the mesh network delivers a message addressed to the
 * LPN's unicast address or any group it subscribes to,
 * the Friend Node stores it instead of discarding it.
 * The LPN retrieves this via a Friend Poll cycle.
 */
static void friend_queue_add(struct l_queue *frnd_queue,
                             uint8_t *pdu, uint16_t len,
                             uint32_t seq, uint16_t src,
                             uint32_t dst)
{
    struct friend_msg *msg = l_new(struct friend_msg, 1);

    msg->pdu  = l_memdup(pdu, len);
    msg->len  = len;
    msg->seq  = seq;
    msg->src  = src;
    msg->dst  = dst;

    /* Push to tail — delivered FIFO order to LPN */
    l_queue_push_tail(frnd_queue, msg);
}

3. States — How Mesh Devices Represent Data

State Value Server Client Composite State Bound State

A state is a named value that describes the current condition of an element. It is the fundamental unit of data in Bluetooth Mesh — everything else (messages, models, publish-subscribe) exists to read or modify states. Understanding the server/client terminology is essential before working with BlueZ models.

Role Relationship to State Example Device BlueZ Model ID
Server Owns and exposes the state; holds the hardware Smart lamp exposing Generic OnOff 0x1000
Client Reads or changes the server’s state via messages Wall switch sending ON / OFF commands 0x1001
Composite State made of two or more sub-values Color lamp: Hue + Saturation + Lightness 0x1307 (HSL)

Bound States — Automatic State Propagation

When two states are bound, a change in one automatically triggers a corresponding change in the other — even across different models. The canonical example is the binding between Generic Level and Generic OnOff:

Generic Level State
Range: -32,768 to +32,767 (int16)
SET Level = 0 → triggers OnOff change
SET Level = 32767 → triggers OnOff change
BOUND STATE
Level = 0
→ OnOff = Off

Level ≠ 0
→ OnOff = On

Generic OnOff State
Values: 0x00 = Off, 0x01 = On
Off ← automatically when Level hits 0
On ← automatically when Level is non-zero

BlueZ: Implementing Bound State Propagation

/* BlueZ plugins/mesh/generic-level.c
 * Handling Level SET with bound OnOff state update       */

#include "mesh/mesh-defs.h"
#include "mesh/model.h"

#define OP_GENERIC_LEVEL_GET    0x8205
#define OP_GENERIC_LEVEL_SET    0x8206
#define OP_GENERIC_LEVEL_STATUS 0x8208

#define MESH_GENERIC_ONOFF_OFF  0x00
#define MESH_GENERIC_ONOFF_ON   0x01

struct level_state {
    struct mesh_node *node;
    uint8_t          ele_idx;
    int16_t          level;    /* current level value      */
    uint8_t          onoff;    /* bound OnOff state        */
};

static bool generic_level_set(uint16_t src, uint16_t dst,
                              uint16_t app_idx,
                              uint8_t *data, uint16_t len,
                              void *user_data)
{
    struct level_state *state = user_data;

    /* Parse the new level value (little-endian int16) */
    state->level = (int16_t) get_le16(data);

    /*
     * ── Bound State Propagation ──────────────────────
     * Spec §2.3.2: when Level changes, bound OnOff
     * must update automatically without any extra message.
     */
    if (state->level == 0)
        state->onoff = MESH_GENERIC_ONOFF_OFF;
    else
        state->onoff = MESH_GENERIC_ONOFF_ON;

    /*
     * Apply hardware change here (e.g., PWM dimmer value).
     * Then publish the new STATUS so all subscribers know.
     */
    generic_level_publish(state);

    return true;
}

4. Messages — Opcodes, Payload, and SAR

Opcode SAR Segmentation Transport Layer Acknowledged Unacknowledged

Every operation in a Bluetooth Mesh network is performed by sending a message. A message consists of an opcode that identifies its type, optional parameters, and a defined behavior that describes what happens when it is received. Opcodes are not arbitrary — each model defines exactly which opcodes it sends and receives, and the spec assigns numeric values to all standard opcodes.

Opcode Sizes and Available Parameter Space

Opcode Size Usage Non-SAR Params SAR Max Params Example
1 Octet Special — preserves max payload 10 bytes 379 bytes 0x01
2 Octets Standard messages — most common 9 bytes 378 bytes 0x8201
3 Octets Vendor-specific messages 8 bytes 377 bytes 0xC0 0x11 0x22

SAR — Segmentation and Reassembly

The transport layer delivers up to 11 octets in a single non-segmented PDU. If a message exceeds this, the transport layer automatically performs Segmentation and Reassembly (SAR): the message is split into up to 32 segments and reassembled at the receiver. The maximum SAR message is 384 octets. Crucially, SAR adds no extra overhead at the access layer per segment — a 10-byte message is one segment; a 20-byte message is two segments, transparently.

Message segmentation in practice:

▶ 10-byte message → 1 segment (no SAR, single PDU)
Opcode
2 bytes
Parameters — 8 bytes
TransMIC
4 bytes

▶ 20-byte message → 2 segments (SAR automatic, same throughput per byte)
SEG 0
SN:0/1
Opcode (2B) + first 9 bytes of payload
No MIC yet
(more segs)
SEG 1
SN:1/1
Remaining 9 bytes of payload
AppMIC
4 bytes

Acknowledged vs Unacknowledged Messages

✅ Acknowledged Message
  • Server must send a Status response
  • Client retransmits if no response arrives
  • Confirms the state was actually changed
  • Higher overhead; use when confirmation matters
GENERIC_ONOFF_SET (0x8202)
→ GENERIC_ONOFF_STATUS (0x8204)
⚡ Unacknowledged Message
  • No response required from the server
  • Fire-and-forget — lower network overhead
  • Useful for broadcast / group messages
  • Use when best-effort delivery is acceptable
GENERIC_ONOFF_SET_UNACK (0x8203)
→ (no response expected)

BlueZ: Sending Mesh Messages and Registering Opcode Handlers

/* BlueZ mesh/net.c — mesh_net_app_send(): send an access message
 *
 * The transport layer handles SAR transparently:
 *   total_len <= 11 bytes  → single unsegmented PDU
 *   total_len >  11 bytes  → SAR: split into N segments (max 32)
 *
 * Parameters:
 *   net       - mesh network context
 *   frnd_cred - use friendship security credentials
 *   src       - source unicast address (our element)
 *   dst       - destination address (unicast / group / virtual)
 *   app_idx   - application key index (for encryption)
 *   ttl       - time-to-live hop count (0x00-0x7F)
 *   data      - pointer to [opcode | parameters]
 *   len       - total byte length
 */
int mesh_net_app_send(struct mesh_net *net,
                      bool     frnd_cred,
                      uint16_t src,
                      uint32_t dst,
                      uint8_t  app_idx,
                      uint8_t  ttl,
                      uint8_t  *data,
                      uint16_t len)
{
    if (!net || !data || !len)
        return -EINVAL;

    return mesh_net_send_seg(net, frnd_cred, src, dst,
                             app_idx, ttl, data, len);
}
/* BlueZ mesh/model.c — opcode dispatch table */
/*
 * Each model registers a table of (opcode, min_len, handler).
 * When a message arrives, mesh_model_opcode_get() extracts the
 * opcode and the framework dispatches to the matching handler.
 * MESH_MODEL_OP_END terminates the table.
 */

/* Standard Generic OnOff opcodes */
#define OP_GENERIC_ONOFF_GET        0x8201
#define OP_GENERIC_ONOFF_SET        0x8202
#define OP_GENERIC_ONOFF_SET_UNACK  0x8203
#define OP_GENERIC_ONOFF_STATUS     0x8204

/* Opcode handler table for Generic OnOff Server */
static const struct mesh_model_op onoff_server_ops[] = {
    /* opcode                 min_param_len  handler           */
    { OP_GENERIC_ONOFF_GET,       0,         onoff_get        },
    { OP_GENERIC_ONOFF_SET,       2,         onoff_set        },
    { OP_GENERIC_ONOFF_SET_UNACK, 2,         onoff_set        },
    MESH_MODEL_OP_END   /* sentinel — marks end of table       */
};

5. Elements — Addressable Units Inside a Node

Primary Element Secondary Element Unicast Addressing Provisioning Model Overlap Rule

A node is a single physical device provisioned into the mesh. Within a node, there can be multiple independently addressable units called elements. An element is what actually holds states and contains models. The mesh network uses each element’s unique unicast address to precisely route messages to the correct functional unit inside a multi-function device.

The primary element is assigned the node’s base unicast address (given during provisioning). Each additional secondary element receives the next sequential unicast address. The count and structure of elements is fixed for the lifetime of the node — if a firmware update changes the element layout, the node must go through the Node Removal procedure and be reprovisioned.

Dual-Lamp Node — Element Layout Example

📦 Node: Dual-Lamp Ceiling Fixture — Base Unicast = 0x0005
Unicast 0x0005 (Primary) Unicast 0x0006 (Secondary)
Primary Element — Lamp 1
Configuration Server (0x0000)
Health Server (0x0002)
▸ Generic OnOff Server (0x1000)
▸ Light Lightness Server (0x1300)

Also holds node-level config states

Same node.
Different
unicast addresses.

Messages route
to correct lamp
via address.

Secondary Element — Lamp 2
▸ Generic OnOff Server (0x1000)
▸ Light Lightness Server (0x1300)

Independent ON/OFF control

The Model Overlap Rule — Why Multiple Elements Exist

A single element cannot contain two model instances that handle the same opcode in the same direction. If your node needs two independent Generic OnOff Servers (one per lamp), placing both in the same element creates an ambiguity: when an “ON” message arrives, which lamp should respond? The solution is simple — put each server in its own element, each with its own unicast address. The mesh framework routes the message to the correct server by matching the destination unicast address to the element address.

Another Real Example — Dual-Socket Smart Power Strip:

Each socket has an independent energy sensor. Both sensors expose a Sensor Data state. They cannot share an element (overlap rule), so the node gets two elements. Element 0 (primary, unicast = node address) holds Socket 1 sensor state plus node config. Element 1 (secondary, unicast = node address + 1) holds Socket 2 sensor state only. A cloud dashboard sends a GET message to unicast 0x000A to read Socket 1, and to 0x000B to read Socket 2.

BlueZ: Registering Multi-Element Nodes

/*
 * BlueZ: ~/.config/meshd/<node-uuid>/node.json
 * Element definitions for the dual-lamp fixture node.
 * Each element has an index, location, and list of models.
 * The primary element MUST include Configuration Server (0x0000).
 */

/*  node.json (elements section):
{
  "elements": [
    {
      "index": 0,
      "location": 1,
      "models": [
        { "modelId": "0x0000" },
        { "modelId": "0x0002" },
        { "modelId": "0x1000" },
        { "modelId": "0x1300" }
      ]
    },
    {
      "index": 1,
      "location": 2,
      "models": [
        { "modelId": "0x1000" },
        { "modelId": "0x1300" }
      ]
    }
  ]
}
*/

/* mesh/node.c — look up element by index */
struct mesh_element *node_get_element(struct mesh_node *node,
                                       uint8_t ele_idx)
{
    return l_queue_find(node->elements,
                        match_element_idx,
                        L_UINT_TO_PTR(ele_idx));
}

/*
 * Unicast address of a secondary element:
 *   element_unicast = primary_unicast + element_index
 * So for a node provisioned with base 0x0005:
 *   Element 0 → 0x0005,  Element 1 → 0x0006
 */
uint16_t node_get_element_addr(struct mesh_node *node,
                                uint8_t ele_idx)
{
    return node_get_primary(node) + ele_idx;
}

6. Mesh Addressing — Unicast, Virtual, and Group

Unicast Address Virtual Address Label UUID Group Address Fixed Group Addresses

Bluetooth Mesh uses a 16-bit address space partitioned into three address types that serve fundamentally different communication patterns. The address type is encoded in the high bits of the 16-bit value, so the stack can determine address type from the value alone — no additional context required.

Type Bit Pattern (MSBs) Range Count Scope
Unassigned 0x0000 0x0000 only 1 Not used in messages; placeholder
Unicast 0b0xxxxxxxxxxxxxxx 0x0001 – 0x7FFF 32,767 Exactly one element; assigned at provisioning
Virtual 0b10xxxxxxxxxxxxxx 0x8000 – 0xBFFF 16,384 hashes Multicast; 128-bit Label UUID — effectively unlimited
Group 0b11xxxxxxxxxxxxxx 0xC000 – 0xFFFF 16,384 Multicast; 256 fixed + 16,128 dynamic

Virtual Addresses — The Label UUID Mechanism

A virtual address is a 16-bit hash derived from a 128-bit Label UUID. When a message is sent to a virtual address, the full 128-bit Label UUID is embedded in the Message Integrity Check (MIC) computation. The receiver verifies the MIC against all its registered Label UUIDs. Only a node that knows the correct UUID can authenticate the message — giving publish-subscribe groups cryptographic identity without requiring central address management.

Why only 16,384 hash values but “millions” of virtual addresses?
The 14 usable bits in the virtual address range produce 16,384 possible hash values. But a hash value is just a shortcut — multiple different 128-bit Label UUIDs can produce the same 14-bit hash. On a match, the full UUID is verified via the MIC, resolving collisions cryptographically. The number of distinct Label UUIDs is 2128, making the number of possible virtual addresses practically unlimited.

Fixed Group Addresses

The top 256 group addresses (0xFF00 – 0xFFFF) are permanently reserved as fixed group addresses. These target all nodes with a specific feature enabled, regardless of their individual unicast addresses. The four most commonly used:

Address Name Targets All Nodes That Have…
0xFFFF All-Nodes Every primary element in the mesh network
0xFFFE All-Relays Relay feature enabled
0xFFFD All-Friends Friend feature enabled
0xFFFC All-Proxies GATT Proxy feature enabled

BlueZ: Address Type Helpers and Sending to Group Addresses

/* BlueZ mesh/mesh-defs.h — address type classification */

/* Fixed group addresses */
#define ALL_PROXIES_ADDRESS   0xFFFC
#define ALL_FRIENDS_ADDRESS   0xFFFD
#define ALL_RELAYS_ADDRESS    0xFFFE
#define ALL_NODES_ADDRESS     0xFFFF

/* Address type bit masks */
#define UNASSIGNED_ADDRESS    0x0000
#define UNICAST_ADDR_MARK     0x8000  /* bit15 = 0 → unicast   */
#define VIRTUAL_ADDR_MARK     0xC000  /* 0b10xx xxxx xxxx xxxx  */
#define GROUP_ADDR_MARK       0xC000  /* 0b11xx xxxx xxxx xxxx  */

static inline bool IS_UNASSIGNED(uint16_t addr)
{
    return addr == UNASSIGNED_ADDRESS;
}

static inline bool IS_UNICAST(uint16_t addr)
{
    /* bit15 = 0 and not 0x0000 */
    return (!IS_UNASSIGNED(addr)) && !(addr & UNICAST_ADDR_MARK);
}

static inline bool IS_VIRTUAL(uint16_t addr)
{
    /* bits [15:14] = 0b10 */
    return (addr & GROUP_ADDR_MARK) == VIRTUAL_ADDR_MARK;
}

static inline bool IS_GROUP(uint16_t addr)
{
    /* bits [15:14] = 0b11 */
    return (addr & GROUP_ADDR_MARK) == GROUP_ADDR_MARK;
}

/* Send a heartbeat to all relay nodes in the network */
void send_heartbeat_to_relays(struct mesh_net *net,
                               uint16_t src_addr)
{
    uint8_t hb_pdu[3];
    hb_pdu[0] = 0x3B;         /* Heartbeat opcode (1-byte)  */
    hb_pdu[1] = DEFAULT_TTL;
    hb_pdu[2] = MESH_FEATURE_RELAY;

    mesh_net_app_send(net, false, src_addr,
                      ALL_RELAYS_ADDRESS,  /* 0xFFFE */
                      APP_IDX_DEV,
                      DEFAULT_TTL,
                      hb_pdu, sizeof(hb_pdu));
}

7. Models — Defining Node Functionality

Server Model Client Model Control Model Publish-Subscribe Configuration Client

A model is a standardised software component that bundles together a set of states, the messages that operate on those states, and the behavioral rules governing those interactions. Models are the reusable building blocks of Bluetooth Mesh applications. The Bluetooth SIG defines models for lighting, sensors, time, scenes, and more. Vendors can also define proprietary models using 3-octet vendor opcodes.

Unlike a traditional Bluetooth profile where client and server roles are defined end-to-end, a Bluetooth Mesh application is configured by a Configuration Client — a provisioner tool (like meshctl) that sets up publication addresses, subscription lists, and AppKey bindings on each node independently. The application logic is then distributed across server, client, and control models.

Three Model Types — At a Glance

🟢 Server Model
  • Owns one or more states
  • Handles GET / SET messages
  • Sends STATUS responses
  • May publish unsolicited STATUS
  • Has state; does not initiate SET on others
Generic OnOff Server in a lamp
🔵 Client Model
  • No state of its own
  • Sends GET / SET to server models
  • Processes STATUS responses
  • Drives user interactions
  • Stateless; cannot receive SET
Generic OnOff Client in a switch
🟣 Control Model
  • Contains both client and server logic
  • Includes rule-based control logic
  • Coordinates other models
  • Middle-layer orchestrator
  • Rare; used for scene controllers, schedulers
Scene Controller coordinating multiple lamps

Client-to-Server via Publish-Subscribe — Message Flow

Device A — Wall Switch
Client Model
Generic OnOff Client
Publishes to:
Group 0xC001
ONOFF_SET
0x8202
ONOFF_STATUS
0x8204
Device C — Lamp
Server Model
Generic OnOff Server
STATE: OnOff = On/Off
Subscribed to: 0xC001
Same
ONOFF_SET
Device B — Another Lamp
Server Model
Generic OnOff Server
Also subscribed to: 0xC001

Device A’s Client Model publishes GENERIC_ONOFF_SET to group address 0xC001. Every Server Model subscribed to that group receives and processes the message, updating their respective OnOff states. Device C sends a GENERIC_ONOFF_STATUS response back to Device A. Device B also processes the SET (because it is subscribed) but does not need to respond. This is many-to-many mesh communication.

BlueZ: Full Generic OnOff Server Model Registration

/* BlueZ plugins/mesh/generic-onoff.c
 * Complete Generic OnOff Server model — states + handlers  */

#include "mesh/mesh-defs.h"
#include "mesh/model.h"
#include "mesh/node.h"

#define GENERIC_ONOFF_SERVER   0x1000
#define GENERIC_ONOFF_CLIENT   0x1001

#define OP_ONOFF_GET        0x8201
#define OP_ONOFF_SET        0x8202
#define OP_ONOFF_SET_UNACK  0x8203
#define OP_ONOFF_STATUS     0x8204

/* State struct: one instance per registered element */
struct onoff_state {
    struct mesh_node *node;
    uint8_t           ele_idx;
    uint8_t           onoff;      /* 0x00 = Off, 0x01 = On */
};

/* Opcode dispatch table */
static const struct mesh_model_op onoff_ops[] = {
    { OP_ONOFF_GET,       0, onoff_get  },
    { OP_ONOFF_SET,       2, onoff_set  },
    { OP_ONOFF_SET_UNACK, 2, onoff_set  },
    MESH_MODEL_OP_END
};

/* Handler: GET → reply with current state */
static bool onoff_get(uint16_t src, uint16_t dst,
                      uint16_t app_idx, uint8_t *data,
                      uint16_t len, void *user_data)
{
    struct onoff_state *s = user_data;
    uint8_t status = s->onoff;

    mesh_model_send(s->node, dst, src,
                    GENERIC_ONOFF_SERVER, app_idx,
                    DEFAULT_TTL, &status, 1);
    return true;
}

/* Handler: SET / SET_UNACK → update state, reply if ACK */
static bool onoff_set(uint16_t src, uint16_t dst,
                      uint16_t app_idx, uint8_t *data,
                      uint16_t len, void *user_data)
{
    struct onoff_state *s = user_data;
    uint32_t opcode;
    int n;

    if (data[0] > 1) return false;   /* invalid value */
    s->onoff = data[0];

    /* Drive hardware here: gpio_set_value(LED_PIN, s->onoff) */

    /* Reply only for acknowledged SET (not SET_UNACK) */
    n = mesh_model_opcode_get(data - 2, len + 2, &opcode, NULL);
    if (n > 0 && opcode == OP_ONOFF_SET)
        mesh_model_send(s->node, dst, src,
                        GENERIC_ONOFF_SERVER, app_idx,
                        DEFAULT_TTL, &s->onoff, 1);
    return true;
}

/* Register server on element ele_idx of node */
bool generic_onoff_server_init(struct mesh_node *node,
                                uint8_t ele_idx)
{
    struct onoff_state *s = l_new(struct onoff_state, 1);
    s->node    = node;
    s->ele_idx = ele_idx;
    s->onoff   = 0x00;   /* start Off */

    return mesh_model_register(node, ele_idx,
                               GENERIC_ONOFF_SERVER,
                               onoff_ops, s);
}
## meshctl — configure publication and subscription after provisioning

$ meshctl
[meshctl]# connect 0100

# Bind AppKey to the Generic OnOff Server on element 0
[meshctl]# appkey-add 0100 0 0

# Set this element's publication address to group 0xC001
[meshctl]# pub-set 0100 0 0xC001 0 0 10 0x1000

# Add group 0xC001 to the subscription list of the server
[meshctl]# sub-add 0100 0 0xC001 0x1000

# Now the lamp will respond to all SET messages sent to 0xC001
# and will publish STATUS updates to the same group

Quick Reference — Bluetooth Mesh Architecture Summary

Concept One-Line Definition Key Number / Detail BlueZ Symbol
State Named value representing an element condition e.g., OnOff = 0x01 onoff_state.onoff
Bound State Linked state — change one, the other auto-updates Level=0 → OnOff=Off generic_level_set()
Message Opcode + parameters exchanged over mesh bearer Max: 384B (SAR); 11B (non-SAR) mesh_net_app_send()
Element Addressable unit inside a node; contains models Primary + N secondaries; fixed at provision node_get_element()
Unicast Targets exactly one element 32,767 per network IS_UNICAST(addr)
Virtual Multicast via 128-bit Label UUID 16,384 hashes; unlimited UUIDs IS_VIRTUAL(addr)
Group Multicast to functional groups of nodes 256 fixed + 16,128 dynamic ALL_NODES_ADDRESS
Server Model Owns states; handles GET/SET; sends STATUS e.g., Generic OnOff Server: 0x1000 mesh_model_register()
Client Model No state; sends GET/SET; processes STATUS e.g., Generic OnOff Client: 0x1001 mesh_model_send()
Control Model Both client + server logic; rule-based orchestration Used in scene controllers, schedulers mesh_model_register()

Ready to Go Deeper into Bluetooth Mesh?

Now that you understand the core architecture, the next topics are Mesh Security (NetKey, AppKey, device key), the provisioning procedure, and relay behaviour — all implemented inside BlueZ’s bluetooth-meshd.

Browse Full Mesh Series All Tutorials

Leave a Reply

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