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
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.
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.
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.
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.
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.
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
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, and instead install and run
bluetooth-meshd, which can speak native advertising-bearer Bluetooth Mesh. Instead of
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
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
Further commands happen from the
config menu, which you enter by typing
target 00c4 (or the address assigned for your node), so that subsequent commands
are sent, using the device's key, to the device.
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
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.
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
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
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
[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
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.
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".
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
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
Bluetooth Mesh model messages, such as
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.