Bluetooth Mesh Series · Part 5
Bluetooth Mesh tutorial — Access Layer in bluetooth mesh
How application messages are built, sent, received, and secured inside a BLE Mesh network — explained from scratch.
Keywords in This Post
What is the Access Layer?
Think of Bluetooth Mesh as a postal system spread across many smart devices — lights, sensors, switches. The access layer is like the department that writes, stamps, and reads the actual letters (application messages). It sits just above the transport layer and is where your app logic lives.
Every time a light switch sends “turn ON” to a bulb, or a sensor reports a temperature reading to a gateway, that message passes through the access layer. This tutorial walks you through exactly how that works — the message format, the addressing, the security, and the rules around sending and receiving.
1. Endianness — Which Byte Goes First?
All multi-byte numbers at the access layer are stored in little-endian format. That means the least significant byte (LSB) comes first in memory.
0x0136, little-endian stores it as 0x36 0x01 in the packet. The smaller byte goes first.This matters when you read opcode bytes or company identifiers from a raw packet capture.
2. Model Identifier — Naming Your Model
Every model (a logical unit of behavior — like “Generic OnOff Server”) must have a unique ID. There are two kinds:
SIG Model ID
16 bits (2 bytes)
Defined and standardized by the Bluetooth SIG. Examples: Generic OnOff, Light Lightness, Sensor.
Vendor Model ID
32 bits (4 bytes)
Your company’s custom model. Composed of a Bluetooth-assigned Company Identifier (2 bytes) + your own model number (2 bytes).
Vendor Model ID Byte Layout
| Byte 0–1 Company ID (LSB first) |
Byte 2–3 Vendor Model ID |
| e.g. 0x36 0x01 → Company 0x0136 | Any value your team assigns |
/* BlueZ: Registering a vendor model */
static struct bt_mesh_model_op vendor_ops[] = {
{ BT_MESH_MODEL_OP_3(0x23, 0x01, 0x36), 0, vendor_msg_handler },
BT_MESH_MODEL_OP_END,
};/* BT_MESH_MODEL_OP_3(opcode, company_lsb, company_msb)opcode = 0x23 (vendor-specific)
0x01,0x36 = company ID 0x0136 in little-endian */
3. Access Payload — What’s Inside a Mesh Message?
Every message your model sends has an access payload. It has exactly two fields:
| Opcode 1, 2, or 3 bytes |
Parameters 0 to 379 bytes |
| Operation Code (what to do) |
Application Data (how to do it) |
How Big Can a Message Be?
The mesh network segments large messages into chunks of 12 bytes each. You can have up to 32 segments. That gives you a raw 384 bytes maximum — but 4 bytes are reserved for TransMIC (the message integrity check), leaving 380 bytes for the actual payload.
| # Segments | Max Payload (32-bit TransMIC) | Max Payload (64-bit TransMIC) |
| 1 (unsegmented) | 11 bytes | n/a |
| 1 (segmented) | 8 bytes | 4 bytes |
| 2 | 20 bytes | 16 bytes |
| n (general) | (n×12) − 4 bytes | (n×12) − 8 bytes |
| 32 (max) | 380 bytes | 376 bytes |
4. Opcodes — The Command Codes
An opcode tells the receiving model what action to perform — like “GET state”, “SET state”, or “STATUS report”. The first byte of the opcode tells you how many bytes the opcode takes up total.
| First Byte Pattern | Opcode Size | Who Uses It | Count Available |
0xxxxxxx (except 0x7F) |
1 byte | Bluetooth SIG | 127 |
01111111 = 0x7F |
— | Reserved (RFU) | — |
10xxxxxx xxxxxxxx |
2 bytes | Bluetooth SIG | 16,384 |
11xxxxxx zzzzzzzz zzzzzzzz |
3 bytes | Vendors (custom) | 64 per company |
Worked Example: Vendor 3-Byte Opcode
Suppose your company ID is 0x0136 and your opcode value is 0x23:
Byte 00xE3 |
Byte 10x36 |
Byte 20x01 |
| 11100011 MSBs=11 → 3-byte opcode bits = 0x23 |
Company ID LSB 0x0136 → 0x36 |
Company ID MSB 0x0136 → 0x01 |
/* BlueZ: Defining a 3-byte vendor opcode handler */
/* BT_MESH_MODEL_OP_3(opcode_bits, company_lsb, company_msb) */static struct bt_mesh_model_op my_vendor_ops[] = {{
BT_MESH_MODEL_OP_3(0x23, 0x36, 0x01),
0, /* minimum message length */
my_vendor_msg_handler /* callback function */
},
BT_MESH_MODEL_OP_END,
};
/* Result: wire bytes will be 0xE3 0x36 0x01 */
5. Sending a Message — Step by Step
When a model sends a message, the access layer fills in several fields before handing it to the transport layer:
Response Delay Rules
To avoid message collisions when multiple nodes respond to the same message at the same time, the spec mandates a random delay before sending a response:
| Request Sent To | Response Delay Range | Why? |
| Unicast address | 20 – 50 ms | Only one node replies — short delay OK |
| Group / Virtual address | 20 – 500 ms | Many nodes reply — spread them out to avoid collisions |
Traffic Limit
/* BlueZ: Sending a Generic OnOff Set message */
struct bt_mesh_msg_ctx ctx = {
.net_idx = BT_MESH_KEY_PRIMARY,
.app_idx = my_app_idx, /* AppKey index */
.addr = target_addr, /* DST: unicast or group */
.send_ttl = BT_MESH_TTL_DEFAULT,
};BT_MESH_MODEL_BUF_DEFINE(buf, BT_MESH_MODEL_OP_GEN_ONOFF_SET, 2);bt_mesh_model_msg_init(&buf, BT_MESH_MODEL_OP_GEN_ONOFF_SET);
net_buf_simple_add_u8(&buf, onoff_state); /* 1 = ON, 0 = OFF */
net_buf_simple_add_u8(&buf, tid++); /* transaction ID */
bt_mesh_model_send(model, &ctx, &buf, NULL, NULL);
6. Receiving a Message — When Does the Model Accept It?
When a message arrives, the access layer checks three things before delivering it to a model:
| Check # | Condition | Detail |
| 1 | Opcode Match | The opcode must be one this model handles |
| 2 | Address Match | DST must be the element’s unicast address, a subscribed group/virtual address, or a fixed group address |
| 3 | Key Binding | The model must be bound to the AppKey or DevKey that secured this message |
If any of these checks fail, the message is silently dropped. No error is sent back to the sender.
/* BlueZ: Message receive callback for Generic OnOff Server */
static void gen_onoff_get(struct bt_mesh_model *model,
struct bt_mesh_msg_ctx *ctx,
struct net_buf_simple *buf)
{
/* ctx->addr = source address of sender */
/* ctx->recv_dst = destination address */
/* ctx->app_idx = which AppKey was used */BT_MESH_MODEL_BUF_DEFINE(resp_buf,BT_MESH_MODEL_OP_GEN_ONOFF_STATUS, 1);
bt_mesh_model_msg_init(&resp_buf,
BT_MESH_MODEL_OP_GEN_ONOFF_STATUS);
net_buf_simple_add_u8(&resp_buf, current_onoff);
/* Reply uses same AppKey as the request (ctx) */bt_mesh_model_send(model, ctx, &resp_buf, NULL, NULL);
}
7. Security — Every Message is Encrypted
Unlike classic Bluetooth where you might encrypt the link itself, Bluetooth Mesh encrypts each message individually at the upper transport layer using one of two keys:
AppKey (Application Key)
Shared between a group of models that belong to the same application. E.g., all lights and switches in a building share one AppKey.
Most messages use this.
DevKey (Device Key)
Unique per device. Used only by the provisioner (the one who set up the mesh network) to configure individual nodes.
Config messages use this.
8. Message Errors — What Happens With Bad Messages?
A message is considered “not understood” if any of the following is true:
- The opcode is unknown to the receiving element
- The message size does not match what the opcode expects
- The parameters contain values marked as “Prohibited” in the spec
There’s also an interesting edge case — a message can accidentally pass the MIC integrity check using wrong keys. When decrypted, the result is garbage that the model doesn’t recognize. Again, silently ignored. The probability is very low but non-zero.
9. Acknowledged vs Unacknowledged Messages
At the access layer, every opcode is defined as one or the other:
| Unacknowledged | Acknowledged |
| Sender → Receiver (no reply expected) |
Sender → Receiver → Status reply (receiver must respond) |
Example: GENERIC_ONOFF_SET_UNACKLower traffic, no confirmation |
Example: GENERIC_ONOFF_SETSender retransmits until STATUS arrives |
/* BlueZ: Unacknowledged vs Acknowledged Set */
/* Unacknowledged — no reply expected */
BT_MESH_MODEL_OP_GEN_ONOFF_SET_UNACK
/* Acknowledged — peer must reply with GENERIC_ONOFF_STATUS */
BT_MESH_MODEL_OP_GEN_ONOFF_SET
/* The model receiving SET must send back a STATUS message.
The model receiving SET_UNACK does nothing in response. */
Quick Summary
| Topic | Key Point |
| Endianness | Multi-byte values are little-endian |
| Model ID | SIG = 16-bit, Vendor = 32-bit (Company ID + model number) |
| Access Payload | Opcode (1–3 bytes) + Parameters (0–379 bytes) |
| Max payload | 380 bytes (32 segments × 12 bytes − 4-byte TransMIC) |
| Opcodes | MSBs decide size: 0x→1B SIG, 10x→2B SIG, 11x→3B Vendor |
| No delivery guarantee | Model handles retransmit; use acknowledged messages for confirmation |
| Security | AppKey for app messages, DevKey for provisioner config; reply uses same key |
| Bad messages | Silently dropped — no error response |
Next Up in the Bluetooth Mesh Series
Now that you understand how the access layer works, the next post dives into Mesh Nodes, Elements, and Models — how devices are organized and how models bind to application keys.
