Bluetooth Mesh with four EBYTE E73-TBB Development Boards based on Nordic nRF52832 Microcontroller

Bluetooth Mesh with nRF52 and Apache Mynewt

Suppose we have four light bulbs in our home, each with its own on/off switch…

Let’s make them smart… With Wireless Remote Control!

Flipping the on/off switch of the first bulb should switch the other bulbs on and off. So the first on/off switch acts like a Master On/Off Switch for our entire home.

How shall we implement this?

We could do this with WiFi… Assuming that we have good WiFi coverage at every corner of our home. There’s a simpler solution… Bluetooth Mesh!

With Bluetooth Mesh there’s no need for a shared network like WiFi. Each node in the mesh may relay messages to other nodes in the mesh. So our bulbs could relay on/off messages to one another all around the house… Maybe outside the house too! (Think Christmas lights)

That’s exactly what I have created here… A Master On/Off Switch for four nRF52 wireless nodes, connected via Bluetooth Mesh…

Master On/Off Switch for four nRF52 wireless nodes, connected via Bluetooth Mesh. The nRF52 boards are powered by a USB charger connected to the Micro USB ports.

Now Bluetooth Mesh is incredibly complicated and difficult to grasp… So I’ll explain it in small pieces (while I’m learning it myself!)

In this tutorial we’ll set up the above nRF52 mesh network, step by step, without any coding! During the setup we’ll encounter mesh concepts (like Models, Elements, Groups, …) We’ll learn them only when we meet them. (The advanced concepts shall be deferred to the next tutorial.)

The complete source code may be found in the mesh branch of this repository…

EBYTE E73-TBB Development Board (based on nRF52) provisioned into a Bluetooth Mesh by running meshctl on Raspberry Pi 4

Hardware Needed

Get ready the following hardware to build our Bluetooth Mesh…

This is the same setup that I have explained in my previous article Coding nRF52 with Rust and Apache Mynewt on Visual Studio Code

Install Raspberry Pi

To set up the Bluetooth Mesh network, we need to use a Raspberry Pi to talk to the nRF52 boards wirelessly and add them one by one to the mesh… This is called Mesh Provisioning.

We’ll be using a tool called meshctl to perform the provisioning on our Pi. (Why Pi? Because meshctl doesn’t run on Windows and macOS.) meshctl is part of the open-source BlueZ suite of Bluetooth utilities.

Follow the instructions here to install meshctl on your Raspberry Pi. (Yes BlueZ is preinstalled on Pi but it lacks the meshctl utility.)

Unfortunately we need to rebuild the Pi kernel (as explained in the instructions)… Because meshctl needs AEAD-AES_CCM encryption, which runs as a secure service in the kernel. This may take an hour or more to complete.

Install nRF52 Boards

Follow the instructions here to flash the bootloader and sample application (NimBLE blemesh_models_example_2) to your nRF52 boards. If the boards have been previously provisioned, click Terminal → Run Task → Erase Flash to erase the previous provisioning info (stored in Flash ROM), then re-flash the bootloader and application.

Connect the first nRF52 board to your computer via ST-Link. Start a debug session, click Continue for all breakpoints. Observe the Console Log.

Keep your nRF52 Board close to your Raspberry Pi. Remember we’ll be doing the Mesh Provisioning wirelessly, over the crowded 2.4 GHz airwaves, so the provisioning works better when the devices are close together.

Provision First nRF52 Board

On our Raspberry Pi, enter these commands…

cd ~
cp bluez-5.50/mesh/local_node.json bluez-5.50/mesh/prov_db.json .

The cp command is only needed for provisioning the first nRF52 board. What’s prov_db.json?

As we provision the mesh, meshctl will remember the details of our mesh… security keys, node addresses, services in each nodes. These details will be saved into prov_db.json when each nRF52 node has been provisioned.

When we provision the second, third, fourth, … nRF52 nodes, meshctl will read the mesh details from prov_db.json so that it can add the nodes to be existing mesh.

Now enter…


Press Enter to see the [meshctl]# prompt.

Our first nRF52 board is powered on and ready to be provisioned… But meshctl has no clue how to contact the board. Let’s give meshctl some guidance. Enter…

discover-unprovisioned on

This enables meshctl to discover any nodes that have not been provisioned… Because unprovisioned nodes will broadcast their unprovisioned status to the airwaves (like a beacon).

In a while we’ll see this…

SetDiscoveryFilter success
Discovery started
Adapter property changed
[CHG] Controller DC:A6:32:2C:70:F1 Discovering: yes
Mesh Provisioning Service (00001827-0000-1000-8000-00805f9b34fb)
Device UUID: dddd0000000000000000000000000000
OOB: 0000
[NEW] Device 09:10:C7:7C:DC:8F nimble-mesh-node

Our nRF52 board has been found! (If you don’t see this, move the nRF52 board closer to your Raspberry Pi)

Take note of the discovered Device UUID dddd0000000000000000000000000000

Now we tell meshctl to provision this new node into our mesh. Enter…

provision dddd0000000000000000000000000000

If the provisioning goes well we should see this prompt…

Request ASCII key (max characters 6)
[mesh] Enter key (ascii string):

(Move the devices closer if you don’t see this)

This is a security check, to be sure that we are authorised to provision the node into the mesh. How do we find the secret key?

Switch over to the Visual Studio Code Debugger. Check the Console Log of our nRF52 board. We should see this message


Go back to meshctl and enter exactly what you see next to OOB String (assuming that you see RWLDWY)…


Our provisioning is almost done!

Unicast Address

In seconds we should see the provisioning completed for our first mesh node…

Provision success. Assigned Primary Unicast 0100

On the Console Log of our nRF52 board we should see a matching message…

Local node provisioned, primary address 0x0100

What is this Primary Unicast Address 0100?

Each node in our mesh will be assigned a permanent address (in hexadecimal): 0100, 0102, 0104, 0106, … Since this is the first node, it’s assigned address 0100.

(What happened to 0101, 0103, …? I’ll explain in the next tutorial)

This is called a Unicast Address, the direct address for the node… “Got something to say to this node? Call this direct number!”

Unicast Addresses for our Mesh Nodes

(Later we’ll see a different kind of address, a Group Address, that we may use to contact multiple nodes.)

Unicast Addresses are stored in prov_db.json of our Raspberry Pi, also in the Flash ROM of the nRF52 board (so the board won’t forget its assigned address).

If we ever need to re-provision this node, we will have to run the Terminal → Run Task → Erase Flash command in Visual Studio Code to erase the Unicast Address stored in the Flash ROM.

Configure the nRF52 Node

Now that we have provisioned the node, let’s configure it. Enter into meshctl

menu config

meshctl shall now dial up the new node at 0100 to give it a makeover. Enter…

target 0100

Let’s talk secrets. We can’t let anyone manipulate our nodes, so we need to lock our nodes with a common secret Application Key or AppKey. Enter into meshctl

appkey-add 1

Where 1 refers to the AppKey with index 1 from prov_db.json


Generic On/Off Model, Server and Client

Now that we have loaded the AppKey, we are ready to lock our new node, part by part. Recall that we have two parts in our nRF52 node, the Switch and the Light…

In Bluetooth Mesh lingo this setup is called a Generic On/Off Model… We only remember the on/off state of the Switch and the Light.

The Switch controls the Light. So we call the Switch the Server, and we call the Light the Client.

Let’s lock the Switch, a.k.a. On/Off Server. Enter the into meshctl

bind 0 1 1000

This locks Element #0 using AppKey #1 (remember our key?) for Model #1000.

Today we won’t be going combing the entire Periodic Table… We’ll limit ourselves to a single Element: Element #0. (More about Elements next time)

Model #1000… Wow have we got thousands of Models squeezed into our tiny workspace? (Like Project Runway?)

No, each Model Server/Client is designated a fixed number by the creators of Bluetooth Mesh… Model #1000 is the official designation for Generic On/Off Server a.k.a. BT_MESH_MODEL_ID_GEN_ONOFF_SRV a.k.a. The Switch.

So this command locks the Switch with our AppKey. Once it’s locked, others may access the Switch securely, as long as they got the right AppKey.

In case you’re curious about the other Model numbers:

For completeness, let’s lock and expose our Light as well. Enter into meshctl

bind 0 1 1001

This locks (and exposes) Element #0 (only one element today) with AppKey #1 (yes the same) for Model #1001.

If we ask the creators of Bluetooth Mesh, they’ll say that Model #1001 is the Generic On/Off Client alias BT_MESH_MODEL_ID_GEN_ONOFF_CLI alias The Light. So our Light has been locked for secure access.

Generic Level Model, Server and Client

Our Switch and Light are ready to be used! But let’s strut some more fabulous Models down our tiny runway. Enter into meshctl

bind 0 1 1002
bind 0 1 1003

Same thing as before, but Model #1002 is a Generic Level Server (BT_MESH_MODEL_ID_GEN_LEVEL_SRV) and Model #1003 is a Generic Level Client (BT_MESH_MODEL_ID_GEN_LEVEL_CLI).

What’s a Generic Level? Why not just use Generic On/Off?

Sure… if we’re happy with binary states like On and Off, Black and White. But if we’re into Fifty Shades Of Grey (think dimmable lights), then we need a Generic Level to express levels of brightness (“50% brightness for this light”)

So many Models… Will this runway ever end?

Yes… eventually. Our sample application defines a number of Bluetooth Mesh Models. But today we’ll learn only the simplest On/Off Model. The Models are defined here:

Publish and Subscribe

Recall from the video that we had a Central Switch controlling the other Lights wirelessly. How did the on/off state of our Central Switch propagate through the airwaves to the other Lights?

With a little Bluetooth Mesh magic called Publish and Subscribe!

Our Central Switch publishes its on/off state to the mesh. Then each Light subscribes to this on/off state to receive updates.

Let’s understand this better by doing a simple Publish and Subscribe. Enter into meshctl

sub-add 0100 c000 1000

This creates a new Subscription that will monitor updates to Unicast Address 0100 (our first node). The Subscription will listen to Group Address c000 for updates to Model #1000 (our Switch, the Generic On/Off Server).

What is this Group Address c000?

In a while we’ll see that c000 is the Group Address that our Switch will use to publish on/off updates.

Yes we have only one node, but it’s perfectly OK for the node to subscribe to itself. This enables the Light inside our node to subscribe to updates from the Switch that belongs to the same node. Flip the Switch, and the Light changes!

Let’s create another Subscription to reinforce the Publish/Subscribe concept. Enter into meshctl

sub-add 0100 c000 1002

This creates a new Subscription that will monitor updates to Unicast Address 0100 (same node). The Subscription will listen to Group Address c000 (same address).

But this time we’re listening for updates to Model #1002 (our Generic Level Server). Why would we do this? So that the first node can propagate its Brightness Level to other nodes and achieve all-round Fifty Shades of Grey (actually 65,536 shades).

Publish On/Off Status and Brightness Level

To complete the node configuration let’s publish the On/Off Status. Enter into meshctl

pub-set 0100 c000 1 0 5 1001

This creates a new Publication that will notify its subscribers of updates to Unicast Address 0100 (the first node).

The updates shall be published to Group Address c000 (remember this?). We may pick any Group Address from c000 to ffff.

For security, we shall lock the Publication with AppKey #1. The numbers 0 and 5 refer to the Publish Period and Publish Retransmit Count (which we won’t cover today, but retransmits are really helpful).

Updates to the On/Off Status shall be channelled to the Light, which is Model #1001 (our Generic On/Off Client).

Lastly we publish the Brightness Level by entering…

pub-set 0100 c000 1 0 5 1003

Everything is the same, except for Model #1003, the Generic Level Client that can receive updates for 65,536 levels of brightness. (No more shady business today I promise!)

Here’s an excellent application of Generic Level Model

Test Publish and Subscribe

Enough provisioning and configuring for our first node… Let’s test it! In meshctl enter…

menu onoff
target 0100

Remember that Generic On/Off is a standard Model in Bluetooth Mesh. So meshctl will let us send On/Off commands easily (via the onoff menu) to test our node.

As for the target… Which node shall we be testing? 0100 of course. Enter into meshctl

onoff 0

This sets the On/Off Client Status to Off. The nRF52 application has been programmed to show the On/Off Client Status on the LED, so this flips the LED Light off. (Check the LED!)

Verify the On/Off Client Status by entering…


You should see value 0…

On Off Model Message received (1) opcode 8204

The nRF52 Console Log should show the LED Light flipping off…

power-> 0, color-> 0

Now watch what happens when you enter this…

onoff 1

Yes we are now flipping our nRF52 LED Light on and off wirelessly via our Raspberry Pi!

Try the same thing you see in the video at the top of this article… Press the buttons on the nRF52 board one at a time. Pressing the first button should flip the Light on, pressing the second button should flip the Light off.

The nRF52 application has been programmed such that pressing the buttons will set the on/off state of the On/Off Server (the Master Switch).

Remember that the LED Light is an On/Off Client that subscribes to the server… Hence the LED Light will also flip on and off.

This is Publish and Subscribe in action!

When we have finished testing, exit meshctl by entering…


Always exit meshctl before powering down any mesh node. Otherwise meshctl may hang while attempting to exit. (And you would need to reboot your Raspberry Pi to release the locked Bluetooth driver)

To connect to the mesh network in future, just launch meshctl and enter connect. All the provisioning details are stored in prov_db.json, so meshctl knows exactly how to connect to the mesh. You may power up the nRF52 board by disconnecting it from ST-Link and connecting a USB Charger to the Micro USB port. Here’s a video demo…

Connecting to Bluetooth Mesh with meshctl. The nRF52 board is powered by a USB charger connected to the Micro USB port.

Provision, Configure and Test Node #1

Here’s a recap of the steps to provision, configure and test our first mesh node…

Here are my meshctl Log and Console Log for provisioning the first node.

Let’s complete the setup for the remaining three nodes in our mesh…

Our Bluetooth Mesh with four nodes

Provision, Configure and Test Nodes #2, 3 and 4

The mesh demo application works with two or more nodes. Here are the steps for setting up the second, third and fourth nodes.

Follow the instructions in this article (“Flash The Firmware”) to flash our application to the second, third and fourth nRF52 boards.

Before setting up the second mesh node, stop the Visual Studio Code Debugger and disconnect the first node from ST-Link.

Connect the second node to ST-Link and start the debugger. Press Continue at all breakpoints. Don’t worry, the mesh will run fine without the first node, because meshctl has all the mesh details in prov_db.json

Enter these commands to provision, configure and test Node #2…

Here are my meshctl Log and Console Log for provisioning the second node.

Now this looks odd… Can you spot the problem? (Remember that 0102 is the Unicast Address for Node #2)

sub-add 0102 c000 1000 — Subscribe to On/Off Updates at Group Address c000
pub-set 0102 c000 1 0 5 1001
— Publish On/Off Updates at Group Address c000

Within Node #2 it makes sense to publish and subscribe On/Off updates locally, so that pressing the buttons on Node #2 will flip the Node #2 LED Light on and off.

But Group Address c000 is already used by Node #1 to publish On/Off updates for the buttons on Node #1!

Hence when we press the buttons on Node #2, the updates will be propagated to Node #1… The buttons on Node #2 also work as the Master Switch!

This fix for this is… left as an exercise for the reader. For now we’re using a simple setup that has the unintended effect of making all nodes the Master Switch. Now for Node #3…

Stop the debugger, disconnect Node #2 from ST-Link, connect Node #3 to ST-Link and start the debugger. Press Continue at all breakpoints.

Enter these commands to provision, configure and test Node #3…

Here are my meshctl Log and Console Log for provisioning the third node. Finally for Node #4…

Stop the debugger, disconnect Node #3 from ST-Link, connect Node #4 to ST-Link and start the debugger. Press Continue at all breakpoints.

Enter these commands to provision, configure and test Node #4…

Here are my meshctl Log and Console Log for provisioning the fourth node.

Here’s my prov_db.json after provisioning all four nodes. I strongly recommend that you backup your copy of prov_db.json located at the pi home directory of your Raspberry Pi… Because the microSD Card can get corrupted so easily. (Happened a few times while I was writing this)

If we need to re-provision any of the nodes, remember to erase the Flash ROM with the Terminal → Run Task → Erase Flash command in Visual Studio Code.

And we’re done! Disconnect the fourth node from ST-Link. Power up all four nodes by connecting a USB Charger to the Micro USB port. Press the buttons on Node #1 like in this video… And the LED Light on other nodes will magically flip on and off!

Master Light Switch accomplished with Bluetooth Mesh!

What’s Next?

This is the second article in my nRF52 series of articles. The first article is here: Coding nRF52 with Rust and Apache Mynewt on Visual Studio Code

Bluetooth Mesh looks really useful for the upcoming PineTime Smart Watch (also based on nRF52). So you can discover which of your friends and family members are in close range (assuming they are wearing the watch too).

Can’t wait to install Bluetooth Mesh on PineTime! Here’s my review…

You may have noticed that the nRF52 application includes the Embedded Rust runtime. I’ll be using Rust Macros to eliminate the repetitive C code used for building Bluetooth Mesh.

Bluetooth Mesh programming doesn’t need to be so complicated… Stay tuned for the simpler, safer Rust version!


Our sample application comes from the open-source Apache NimBLE project, which is described here…

Here’s an excellent tutorial on Bluetooth Mesh based on Zephyr embedded OS…

Another tutorial based on nRF52 and Nordic SoftDevice…

Bluetooth SIG has a series of high-level articles about Bluetooth Mesh…

The official Bluetooth Mesh specifications are here…