Bluetooth Mesh tutorial — Access Layer in bluetooth mesh

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.

3.7
Spec Section
380B
Max Payload
32
Max Segments
1–3
Opcode Bytes

Keywords in This Post

Access Layer Access Payload Opcode SIG Model ID Vendor Model ID TransMIC TTL AppKey DevKey Acknowledged Message Unicast Address Group Address

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.

Quick reminder: If the value is 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
Rule of thumb: For most sensor/actuator data, a single unsegmented message (11 bytes) is enough. Use multi-segment only for OTA firmware updates or bulk data.

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 0
0xE3
Byte 1
0x36
Byte 2
0x01
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:

1
Source (SRC) — Set to the unicast address of the element sending the message. Each element on a node has a unique address.
2
Destination (DST) — Set by the higher layer (application or publication config). Can be a unicast, group, or virtual address.
3
TTL (Time to Live) — Number of hops the message can travel. If you don’t set it, the Default TTL is used automatically.
4
Security keys — The message is encrypted with either an AppKey (application key, model-specific) or a DevKey (device key, provisioner use).
No delivery guarantee! The access layer is fire-and-forget. If you need confirmation, use an acknowledged message — the model must implement retransmission logic itself.

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

Keep traffic under control: A node must originate fewer than 100 Lower Transport PDUs per 10-second window. Mesh radio bandwidth is shared with all nodes and other BLE devices — don’t flood it.
/* 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.

Key rule: A response message must always use the same key as the request. If a GET came in secured with AppKey #3, the STATUS response must also use AppKey #3.

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
Silent drop: The spec says to simply ignore not-understood messages. No NACK, no error response. This also means: if you send an acknowledged message that the other side doesn’t understand, you will never get a reply.

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_UNACK
Lower traffic, no confirmation
Example: GENERIC_ONOFF_SET
Sender 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.

Leave a Reply

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