Bluetooth Mesh is a brokerless system for devices to communicate within a local area. We've implemented a Bluetooth Mesh stack, in Rust, on top of Drogue-Device, our async framework for embedded development.

What's Bluetooth Mesh?

Bluetooth Mesh is a mesh topology using connectionless BLE advertising packets, to allow a variety of devices to communicate with each other, using a low-power means, without necessarily having knowledge about other members of the mesh.

The part that makes a mesh is the fact it uses "managed flooding" to relay packets possibly beyond the distance a single radio can transmit. Bluetooth, as we know, has a somewhat limited range. Each member of the Mesh then, can optionally extend the physical range of the entire mesh, by repeating or relaying messages onward.

The "managed flooding" aspect means that every node capable of relaying does attempt to relay any message it sees for the first time. Each message traversing the mesh starts with a TTL (time-to-live) that is decremented each time a node relays it onwards. In this way, hopefully every node sees every message at least once, while also not repeatedly relaying messages it's seen more than once.

Why Bluetooth Mesh and not Zigbee, Thread or something else?

We don't view the question of radio technology as an XOR condition. Instead, we are simply starting with Bluetooth Mesh because we already have familiarity with it, and the Bluetooth Consortium is pretty good about making its specifications openly available.

We did want a low-power method of connecting hundreds of battery-powered devices, so WiFi is excluded from the potential candidates.

The Moving Parts of a Mesh

Devices, Elements & Models.

Every participant in the mesh is a device (or node). A device is a physical thing, such as a lightswitch, an mains power outlet, or an internet-connected chicken.

Every device has one or more elements, which represent an individually-addressable portion of the device. A 2-gang outlet would have two elements: one element for the left socket, one element for the right socket.

Each element may have one or more models associated with it. A model in the Mesh world is a predefined idea of a thing that can control or be controlled. There are models defined for on-off switches and things that can be turned on and off. Each model is identified by either a specification-defined UUID or a vendor-defined UUID.

Models allow data to be smaller over the air, and for all participants to know how to react to messages, since they all share the same idea of what's being talked about. Bluetooth Mesh messages are not self-describing. By using pre-defined models, a 4-byte message can be transmitted, and all relevant devices know how to interpret those 4 bytes within the context of the known model structure.


Unicast addresses

Every element gets assigned a unique unicast address within the mesh. Given this, a single device with multiple elements ends up with multiple unicast addresses. Using the unicast address, messages can be targetting directly to an element on a device. Any node that receives messages for another's address will still relay those messages onwards, helping them to find their destination.

Virtual addresses

Bluetooth Mesh also defines a capability of having virtual addresses which are not assigned to any particular node. Instead, they act similar to IP mcast groups, which devices can send messages to, or monitor for messages being sent to it.

Virtual addresses are the part that allow devices to work in a publish/subscribe model without any central broker.


Device Key

Every device has its own cryptographic key used during provisioning to create a device key known by the provisioner (see below). For messages destined directly to the device for configuring it by the provisioner, this device key is used to communicate securely.

Network Keys

Every device brought into a mesh is given the network key used by the mesh. This allows multiple distinct meshes to operate within radio earshot of each other, and not intermingle.

Devices on a mesh can also be added to a subnet of the mesh, with its own unique key, to further restrict which devices can see which messages.

Application Keys

A mesh may have one or more application keys. Each element/model on a device can use an appropriate application key, to even further restrict which devices can see which messages.

Application keys provide a way to cryptographically keep your untrusted internet-connected chicken device from secretly unlocking your mesh-enabled doorlock.


A provisioner is a node that controls the membership and logical topology of the mesh. The mesh does not require a provisioner during normal operation. A provisioner is only required to bring a new device into the mesh and to configure the topology through setting up the various publish and subscribe rules for each device.

A stable mesh network can live happily without a provisioner, once established.

Bluetooth Mesh on Linux is... challenging.

Your run of the mill Linux distribution probably includes bluez, which has out-of-the-box support for Bluetooth Mesh. Sorta. Kinda.

Alas, the default bluez bluetoothd only supports Bluetooth Mesh over the GATT bearer, which really isn't the ideal way to go about it. The GATT bearer uses connection-oriented communications between the provisioner and the device being configured. It doesn't use the normal underlying advertising-based bearer used by the devices to communicate with each other.

First you have to stop bluetoothd on your linux node then install the bluez-mesh package on fedora, or bluez-meshd on debian/ubuntu. The bluetooth-meshd tool can speak native advertising-bearer Bluetooth Mesh.

You can run the ble mesh in a separate terminal: sudo /usr/libexec/bluetooth/bluetooth-meshd. Instead of using meshctl, you then use mesh-cfgclient to provision nodes.

Let's walk through an example.

Flash your device

Our example uses the current nRF52840-dk example that sets up a board with one active button and one active LED. Instructions for loading the code onto the board are included in the repository.

Discover unprovisioned devices

Once you have your linux stack working, you can listen for devices telling the world they are unprovisioned, or not a part of any mesh network. As it learns from unprovisioned devices, it'll display it on the console.

[mesh-cfgclient]# discover-unprovisioned on
Unprovisioned scan started
Scan result:
	rssi = -39
	UUID = 0EF817B94FA04859A4F7C80312CD724E
	OOB = A040

Provision it

Once you have the UUID of the device wanting to join the mesh, you can provision it.

[mesh-cfgclient]# provision 0EF817B94FA04859A4F7C80312CD724E
Provisioning started
Assign addresses for 1 elements
Provisioning done:
Mesh node:
	UUID = 0EF817B94FA04859A4F7C80312CD724E
	primary = 00c4

	elements (1):

When this is completed, the current state of affairs is:

  • The device has the network key
  • The provisioner assigned it a unicast address
  • The provisioner knows the device's key

Inspect it

Further commands happen from the config menu, which you enter by typing menu config.

Next, type target 00c4 (or the address assigned for your node), so that subsequent commands are sent, using the device's key, to the device.

The composition-get command will retrieve the device's composition of elements and models.

[config: Target = 00c4]# composition-get
Received DeviceCompositionStatus (len 21)
Received composion:
	Feature support:
		relay: yes
		proxy: no
		friend: no
		lpn: no
	 Element 0:
		location: 0100
		SIG defined models:
		  Model ID	0000 "Configuration Server"
		  Model ID	1001 "Generic OnOff Client"
		  Model ID	1000 "Generic OnOff Server"

In this case, we see that this particular device does indeed support relaying. It also has one element, which includes the Configuration Server model, which is the exact model that can reply to commands such as composition-get.

It also includes the Generic OnOff Client, which in our case the button1 on our board. Additionally it includes the Generic OnOff Server which is connected to the LED on this board.

Naming within the Bluetooth Mesh specification can sometimes be ambiguously confusing. A client tends to be a model that sends commands, while a server is a model that receives them. Generally speaking. Sometimes. Mostly. Kinda.

At this point, nothing happens on the board.

Distribute Application Key

Before we can make the button or LED do something useful, we need to create an application key.

We have to go back in mesh-cfgclient and create an application key for the primary network. We have to provide an index, which gives everyone on the mesh a short name to refer to that specific key.

Indexes do not have to be sequential. In this case, we have app-keys in indexes 0, 1 and 2, but we jump up and create an app-key in index 4.

[config: Target = 00c4]# back
[config: Target = 00c4]# appkey-create 0 4
[config: Target = 00c4]# keys
NetKey: 0 (0x000), phase: 0
	app_keys = 0 (0x000), 1 (0x001), 2 (0x002), 4 (0x004)

Currently, only the provisioner knows this key. Now we must give it to the device we want to have it using the appkey-add command.

[config: Target = 00c4]# appkey-add 4
No response for "AppKeyAdd" from 00c4
Received AppKeyStatus (len 4)
Node 00c4 AppKey status Success
NetKey	0 (0x000)
AppKey	4 (0x004)
Received AppKeyStatus (len 4)

You may notice that mesh-cfgclient first reported "No response" but the we get a response. That's life on a mesh for you. Since Bluetooth Mesh is connectionless, a lot of timeouts are involved. mesh-cfgclient decided the command had timed out, but then device sent a response a few milliseconds later and everything is fine.

At this point, the device has application key #4, but has no idea what to do with it.

Bind Application Key

Application keys can be bound to a model within an element. This tells the device that for messages to or from that model within the element, it should use a specific key for crypto.

Here we bind it to both the client and the server on the board.

[config: Target = 00c4]# bind 00c4 4 1001
Received ModelAppStatus (len 7)
Node 00c4: Model App status Success
Element Addr	00c4
Model ID	1001 "Generic OnOff Client"
AppIdx		4 (0x004)

[config: Target = 00c4]# bind 00c4 4 1000
Received ModelAppStatus (len 7)
Node 00c4: Model App status Success
Element Addr	00c4
Model ID	1000 "Generic OnOff Server"
AppIdx		4 (0x004)
 [config: Target = 00c4]#

Pub and Sub

Thus far, the client and the server know how to encrypt and decrypt messages related to their models using application key #4. But how do they know where to send messages or receive them?

They don't. Yet.

Virtual Address

Since we don't want to bind this particular button to that particular LED, we want to use a virtual address. This will allow us to have tons of buttons turning tons of LEDs on and off, if we wanted.

Imagine a 3-way lightswitch. You may a switch at the top of your stairs, and another at the bottom. Either switch can turn the several lights going up the stairwell on or off. Here, a virtual address might represent "control of the stairway lighting".

Within mesh-cfgclient we use virt-add which synthesizes a new virtual address for us and registers it within the provsioner.

[config: Target = 00c4]# virt-add
	Virtual addr: bda5, label: 1d3ffe292725f6cb64eaed53dba1d979

Still, nobody knows about it, beside the provisioner.


Now, we tell the button on the board to publish its messages to that virtual address:

[config: Target = 00c4]# pub-set 00c4 bda5 4 1 1 1001
No response for "ModelPubVirtualSet" from 00c4
Node 00c4 Publication status Success
Model ID	1001 "Generic OnOff Client"
	Element: 00c4
	Pub Addr: bda5
	Model: 1001
	App Key Idx: 4 (0x004)
	TTL: ff
Period		100 ms
Rexmit count	1
Rexmit steps	0

Now, when we push or release the button, it'll be shooting messages onto the Mesh addressed to bda5.

But if a message falls in a forest and nobody is around to hear it, does it turn a light on?



Next, we have to tell our LED to listen for messages sent to the same virtual address.

[config: Target = 00c4]# sub-add 00c4 bda5 1000
Received ModelSubStatus (len 7)

Node 00c4 Subscription status Success
Element Addr	00c4
Model ID	1000 "Generic OnOff Server"
Subscr Addr	bda5


Here, I've done the above process with two boards, so that we can see how a virtual address allows communication between several devices that have no knowledge of each other.


For the most part, the firmware only has to adapt Drogue-Device actor messages to/from domain-specific types, such as LEDMessage::On or ButtonMessage::Pressed to Bluetooth Mesh model messages, such as GenericOnOffMessage::Set(...).

The full example code is in our repository.

That seems complex...

Yes, we all agree the various commands and workflow to accomplish this through mesh-cfgclient is complex. Ultimately mesh-cfgclient is not a super-friendly application meant for end-users. Instead, it's the barest plumbing possible to accomplish all the steps required to provision and configure devices.

We assume in a real-world scenario, someone would create enough porcelain around it all to allow an end-user to think more semantically about lightswitches, stairways and internet-connected chickens.