What are Foundation Models?
When you build a Bluetooth Mesh network, every node (light bulb, sensor, switch) needs a way to tell the network who it is, what features it has, and how it should send messages. That is exactly what Foundation Models do — they are the core set of rules and data structures that make configuring and managing a mesh network possible.
Think of Foundation Models as the identity card + settings panel built into every mesh node. Without them, the provisioner (the device that adds nodes to the network) would have no idea what a new node can do or how to talk to it.
Keywords to Know
Before diving into the data structures, the spec sets two ground rules that everything else follows.
4.1.1 Endianness — Little Endian
All multi-byte numbers in the Foundation layer are stored little endian — the least significant byte comes first. For example, the value 0x1234 is stored as 0x34 0x12 on the wire.
| Byte Position | Byte 0 (first on wire) | Byte 1 |
|---|---|---|
| Value 0x1234 | 0x34 (LSB) | 0x12 (MSB) |
4.1.2 Log Field Transformation — Compressing 2 bytes into 1
The mesh spec often needs to store a value that could be up to 65535 (2 bytes) into a single byte field. It does this using a logarithmic trick: instead of storing the exact value, it stores a log field value n where 2^(n-1) is the largest power of 2 that fits in the real value.
Simple rule: Log field value = the number of bits needed to represent the value. Log 0x01 → value is exactly 1. Log 0x08 → value is somewhere between 128 and 255.
| Log Field (1 byte) | Actual 2-byte Range |
|---|---|
| 0x01 | 0x0001 (exactly 1) |
| 0x04 | 0x0008 – 0x000F (8 to 15) |
| 0x08 | 0x0080 – 0x00FF (128 to 255) |
| 0x10 | 0x8000 – 0xFFFF (32768 to 65535) |
💡 Why is this useful? Saving 1 byte per field matters a lot in BLE mesh where access payload size is limited. The tradeoff is that you lose precision — you only know the range, not the exact value.
Every node in a mesh network carries a Composition Data state. Think of it as a structured data packet that answers: “Who made me? What am I? What can I do? What models do I run?”
The data is split into pages. Page 0 is mandatory. Others are optional. The full state must not exceed the maximum BLE access payload size.
Composition Data Page 0 Fields
| Field | Size | What it means |
|---|---|---|
| CID | 2 bytes | Company ID — assigned by Bluetooth SIG (e.g. Nordic = 0x0059) |
| PID | 2 bytes | Product ID — vendor-defined (e.g. 0x0001 = Smart Bulb v1) |
| VID | 2 bytes | Version ID — firmware/hardware version, vendor-defined |
| CRPL | 2 bytes | Minimum replay protection list entries — prevents replay attacks |
| Features | 2 bytes | Bitmask of supported node features (Relay, Proxy, Friend, Low Power) |
| Elements | variable | One or more element descriptions (see below) |
Features Bitmask — What the node can do
| Bit | Feature | What it does |
|---|---|---|
| 0 | Relay | Re-broadcasts mesh messages to extend range |
| 1 | Proxy | Bridges GATT phone apps into the mesh network |
| 2 | Friend | Buffers messages for sleepy Low Power nodes |
| 3 | Low Power | Battery-saving mode; pairs with a Friend node |
| 4–15 | RFU | Reserved for future use |
💡 Example: A node with Features = 0x0003 means bits 0 and 1 are set — it supports both Relay and Proxy. A simple sensor with no relay capability would have Features = 0x0000.
Elements — The building blocks inside a node
A node contains one or more elements. Each element is like a separate logical device inside the node. A smart power strip, for example, might have one element per socket. Each element runs its own set of models.
| Field | Size | Meaning |
|---|---|---|
| Loc | 2 bytes | Location descriptor (e.g. “front”, “top”, “left”) from GATT Namespace |
| NumS | 1 byte | Number of SIG (standard) Model IDs that follow |
| NumV | 1 byte | Number of Vendor (custom) Model IDs that follow |
| SIG Models | variable | Array of NumS standard 2-byte model IDs |
| Vendor Models | variable | Array of NumV vendor 4-byte model IDs |
Visual: A Node with 2 Elements
| Composition Data Page 0 | |||||||
| CID 2B |
PID 2B |
VID 2B |
CRPL 2B |
Features 2B |
Elements → | ||
|
|||||||
BlueZ Example — Reading Composition Data via meshctl
In BlueZ’s meshctl tool, after provisioning a node you can retrieve its Composition Data Page 0:
# Launch meshctl and join the network
$ sudo meshctl
# Inside meshctl: provision a nearby unprovisioned device
[meshctl]# discover-unprovisioned on
[meshctl]# provision <UUID>
# After provisioning, send a Config Composition Data Get message
# This is opcode 0x8008, targeting page 0
[meshctl]# menu config
[meshctl - config]# target <unicast_address>
[meshctl - config]# composition-get 0
# The node responds with its Page 0 data.
# BlueZ prints: CID, PID, VID, CRPL, Features, and each element's models
If you are reading raw HCI data, the Composition Data response comes back as a little-endian byte stream. Here is how you would parse the Features field in C:
#include <stdio.h>
#include <stdint.h>
typedef struct {
uint16_t cid;
uint16_t pid;
uint16_t vid;
uint16_t crpl;
uint16_t features;
} comp_data_page0_header_t;
void print_features(uint16_t features) {
printf("Node Features:\n");
printf(" Relay: %s\n", (features & (1 << 0)) ? "Yes" : "No");
printf(" Proxy: %s\n", (features & (1 << 1)) ? "Yes" : "No");
printf(" Friend: %s\n", (features & (1 << 2)) ? "Yes" : "No");
printf(" Low Power: %s\n", (features & (1 << 3)) ? "Yes" : "No");
}
int main() {
/* Example: features = 0x0003 means Relay + Proxy supported */
uint16_t features = 0x0003;
print_features(features);
return 0;
}
Once a node is provisioned and its composition is known, the next question is: how does a model know where to send messages, how often, and with what settings? That is controlled by the Model Publication state.
Each model in each element has its own independent Model Publication state instance. The state holds:
| Component | Role |
|---|---|
| Publish Address | Where to send the message (group address, unicast, or Label UUID) |
| Publish Period | How often to publish (encoded as steps × resolution) |
| Publish AppKey Index | Which application key to use for encrypting messages |
| Friendship Credential Flag | Whether to use friendship security credentials |
| Publish TTL | Time-To-Live — how many hops the message can take |
| Retransmission Count | How many times to re-send the same message |
| Retransmit Interval Steps | Delay between retransmissions |
4.2.2.1 Publish Address
This is the destination address for messages the model publishes. It can be:
- Unassigned address (0x0000) — model is inactive, sends nothing unsolicited
- Unicast address — messages go to one specific node
- Group address — messages go to all subscribers in a group (e.g. all lights in a room)
- Label UUID — a virtual address derived from a 128-bit UUID
💡 If a model’s publish address is set to 0x0000 (unassigned), it will only reply to acknowledged messages — it never sends anything on its own.
4.2.2.2 Publish Period
The Publish Period is a 1-byte value packed with two fields using a simple bit layout:
| Bits 5..0 — Number of Steps (6 bits) | Bits 7..6 — Step Resolution (2 bits) | ||||||
|---|---|---|---|---|---|---|---|
| b5 | b4 | b3 | b2 | b1 | b0 | b7 | b6 |
The Step Resolution (2 bits) sets the unit of time:
| Value (bits 7:6) | Step Unit | Max Period (63 steps) |
|---|---|---|
| 0b00 | 100 ms | 6.3 seconds |
| 0b01 | 1 second | 63 seconds |
| 0b10 | 10 seconds | 630 seconds (~10.5 min) |
| 0b11 | 10 minutes | 630 minutes (~10.5 hours) |
Actual Period = Number of Steps × Step Unit
💡 Example: Byte = 0x26 → binary 0010 0110 → Step Resolution = 00 (100 ms), Number of Steps = 100110 = 38. Publish Period = 38 × 100 ms = 3800 ms = 3.8 seconds.
BlueZ Example — Setting Model Publication via meshctl
# Inside meshctl config menu, target the provisioned node
[meshctl - config]# target 0x0005
# Set publication for a Generic OnOff Server model (SIG Model ID 0x1000)
# Syntax: pub-set <element_address> <pub_address> <appkey_index> <ttl> <period> <retransmit> <model_id>
#
# pub_address : 0xC000 (a group address — all lights in the room)
# appkey_index : 0 (use AppKey 0)
# ttl : 5 (message lives for 5 hops)
# period : 0x26 (38 steps × 100ms = 3.8 seconds)
# retransmit : 0x05 (5 retransmissions, default interval)
# model_id : 0x1000 (Generic OnOff Server)
[meshctl - config]# pub-set 0x0005 0xC000 0 5 0x26 0x05 0x1000
# A successful Config Model Publication Status response confirms the change.
Here is how you would build the Publish Period byte in C before passing it to your mesh stack:
#include <stdint.h>
#include <stdio.h>
/* Step Resolution values */
#define STEP_RES_100MS 0x00
#define STEP_RES_1S 0x01
#define STEP_RES_10S 0x02
#define STEP_RES_10MIN 0x03
uint8_t build_publish_period(uint8_t steps, uint8_t resolution) {
/* steps : 0-63 (6 bits)
resolution : 0-3 (2 bits, goes into bits 7:6) */
return (uint8_t)((resolution << 6) | (steps & 0x3F));
}
int main() {
/* Publish every 1 second: 1 step × 1s resolution */
uint8_t period = build_publish_period(1, STEP_RES_1S);
printf("Publish Period byte for 1s: 0x%02X\n", period); /* 0x41 */
/* Publish every 10 minutes */
uint8_t period2 = build_publish_period(1, STEP_RES_10MIN);
printf("Publish Period byte for 10min: 0x%02X\n", period2); /* 0xC1 */
return 0;
}
| Topic | One-line Summary |
|---|---|
| Foundation Models | Core building blocks that let nodes announce identity and get configured |
| Little Endian | All multi-byte values put the LSB first on the wire |
| Log Field | Compresses 2-byte range into 1 byte using a power-of-2 index |
| Composition Data Page 0 | Node’s identity card: company, product, version, features, and elements |
| Features Bitmask | 4 bits tell you if a node can do Relay, Proxy, Friend, or Low Power |
| Elements | Logical sub-devices inside a node, each running its own model set |
| Model Publication | Per-model state that controls destination, frequency, and TTL of messages |
| Publish Period | 1-byte value: 6-bit step count × 2-bit resolution (100ms to 10min per step) |
Keep Learning Bluetooth Mesh
This post is part of the EmbeddedPathashala Bluetooth Classic & Mesh series.
