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
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
Table of Contents
- Legacy Device Support via GATT Proxy
- Low Power Support and the Friendship Concept
- States — How Mesh Devices Represent Data
- Messages — Opcodes, Payload, and SAR
- Elements — Addressable Units Inside a Node
- Mesh Addressing — Unicast, Virtual, and Group
- Models — Defining Node Functionality
- Quick Reference Summary Table
1. Legacy Device Support via GATT Proxy
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
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 |
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
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.
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 |
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
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.
2 bytes
4 bytes
SN:0/1
(more segs)
SN:1/1
4 bytes
Acknowledged vs Unacknowledged Messages
|
✅ Acknowledged Message
GENERIC_ONOFF_SET (0x8202)
→ GENERIC_ONOFF_STATUS (0x8204) |
⚡ Unacknowledged Message
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
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 |
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.
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
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.
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
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
Generic OnOff Server in a lamp
|
🔵 Client Model
Generic OnOff Client in a switch
|
🟣 Control Model
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.
