What's more Christmas-y than the entire office decorated in multi-colored LEDs? After my last attempt at creating a wireless network I realized that a simple point-to-point network does not scale to the level of our entire office. So for the next iteration i chose to jump on the protocol of choice for indoor connected nodes: Thread. 


I started out with zero knowledge about this and ended up with 18 Thread-connected nodes all polling the main server for a color. The nodes are implemented on Thunderboard Sense and glows in different colors dictated by the server. The server can either provide a fixed color or do a mode where it cycles through the colors along the edges of the RGB-gamut. Here's a couple of pictures from the installation. Detailed instructions below the pics.

Thread - Server and Node

The commissioning Thread server and one node.


Thread - Multiple nodes

Multiple Thread nodes all glowing in the same color.


Thread - Around the Office

Thread nodes all glowing around the office.


Thread - In the Corner

Thread nodes in every corner of the office.



Detailed description


Step 1: Get started

The first thing I did was get the client/server example up running on two WSTKs. This example is using CoAP for communication between a client and a server, very much like communication between a webclient and a webserver using HTTP. You can use POST commands to push data to the server and GET commands to retrieve data back. For the initial phase I just followed QSG113: Getting Started with Silicon Labs Thread.


It's best to remove bootloader, you can do that in the .isc file by configuring "HAL->Bootloader->None" and unchecking the box "Plugins->Common->TFTP Bootload Target", and then commenting out the non-compiling bits in client-implementation.c and server-implementation.c. If you remove the bootloader you can just press the debug-button in Simplicity Studio to run code on the device.


Step 2: Get it running on a Thunderboard Sense

The next thing I had to do was to get our Thread stack running on a Thunderboard Sense. The first thing I did was to create a new client-project under a different name, I used client_tbs. I then removed the bootloader as outlined above and changed the target part to EFR32MG1P132F256M48 and hit 'Generate'. After removing the boorloader parts from client-implementation.c this should run on the Thunderboard Sense, reporting back the temperature.


To get it properly working with the peripherals on the actual board I did two things: 1) Update the buttons/LEDs and 2) include the BSP from the Bluetooth example.


Getting it to work with the correct buttons and LEDs I copied the folder EFR32MG1_BRD4151A into my project and renamed it to EFR32MG1_BRD4160A. I then updated the bspconfig.c with the correct locations of the LEDs and buttons:

#define BSP_GPIO_LED0_PORT      gpioPortD
#define BSP_GPIO_LED0_PIN       11
#define BSP_GPIO_LED1_PORT      gpioPortD
#define BSP_GPIO_LED1_PIN       12

#define BSP_GPIO_PB0_PORT       gpioPortD
#define BSP_GPIO_PB0_PIN        14
#define BSP_GPIO_PB1_PORT       gpioPortD
#define BSP_GPIO_PB1_PIN        15

If you then change the include path to this folder instead of the EFR32MG1_BRD4151A, the project should compile and you will get a green blinking LED.



To get the BSP running you first have to copy the neccessary contents from SimplicityStudio\v4\offline\examples\ble_2000\examples_thunderboard_sense\bsp into your project. For this project I'm just using the PWM LEDs and the Temp/Humidity sensor, so I copied: 


void emberAfMainCallback(MAIN_FUNCTION_PARAMETERS)
  BOARD_rgbledPowerEnable( 1 );
  BOARD_rgbledEnable( true, 0xf );



As the BOARD.c BSP relies on the systick-timer you have to add the following to the top of client-implementation.c  to route the systick interrupt to the correct location:



void halInternalSysTickIsr(void)



To initialize the BSP you add the following in the top of the same file:


void emberAfMainCallback(MAIN_FUNCTION_PARAMETERS)
  BOARD_rgbledPowerEnable( 1 );
  BOARD_rgbledEnable( true, 0xf );

The function emberAfMainCallback is run immediately on boot. A stub for this function is located in thread-callback-stubs.c by default, so remove it from here. The second thing you have to do is in main.c move the INTERRUPTS_ON(); above emAfMain(MAIN_FUNCTION_ARGUMENTS); as interrupts are needed for initializing the BSP:


  // Enable interrupts before main-init
  // Let the application and plugins do early initialization.  This function is
  // generated.




You should now be able to use all the BSP-functions of the Thunderboard Sense. For example, getting the actual temperature of the RH/temp sensor to report back to the server by replacing data = getTemp_mC(); with:


uint32_t rhData;
int32_t  tempData;
if( SI7021_measure( &rhData, &tempData ) != SI7021_OK )
  rhData   = 25000;
  tempData = 50000;
data = tempData;



Step 3: Use CoAP GET to get the colors from the server

The original example was only using POST, so the third task was to get the application to use GET instead.



On the server side you should add:


  {EMBER_AF_COAP_DISPATCH_METHOD_GET, "client/get", 10, clientGetHandler},

To the emberAfCoapDispatchTable[] in thread-coap-dispatch.c. This means that the server will run clientGetHandler every time it gets a GET request from a client. In server-implementation.c a corresponding handler will have to be added for this:

void clientGetHandler(const EmberCoapMessage *request)
  // Requests from clients are sent as CoAP GET requests to the "client/get"
  // URI.

  EmberCoapCode responseCode;

  if (state != ADVERTISE) {
  } else {
	if (!colorFixed)

	coapmessage[0] = colorTableSine[colorIndex][0];
	coapmessage[1] = colorTableSine[colorIndex][1];
	coapmessage[2] = colorTableSine[colorIndex][2];

	emberAfCorePrint("Sending %ld %ld %ld to client at ", coapmessage[0], coapmessage[1], coapmessage[2]);
    responseCode = EMBER_COAP_CODE_205_CONTENT;

  if (emberCoapIsSuccessResponse(responseCode)
      || request->localAddress.bytes[0] != 0xFF) { // not multicast
    emberCoapRespond(responseCode, coapmessage, 3); // Payload


The rest of the additions to server-implementation.c looks like this:

/* ********************* USER *************/
static uint32_t buttonpress = 0;
static uint8_t coapmessage[4] = {0};
static uint8_t colorIndex   = 0;
static uint8_t colorFixed   = 0;
static uint8_t colorStep    = 0;
static uint8_t colorStepped = 0;

static const uint8_t colorTableSine[48][3] =
  {0xFF, 0x0 , 0x0 },
  {0xE6, 0x19, 0x0 },
  {0xCE, 0x31, 0x0 },
  {0xB8, 0x47, 0x0 },
  {0xA5, 0x5A, 0x0 },
  {0x95, 0x6A, 0x0 },
  {0x8A, 0x75, 0x0 },
  {0x82, 0x7D, 0x0 },
  {0x80, 0x7F, 0x0 },
  {0x7D, 0x82, 0x0 },
  {0x75, 0x8A, 0x0 },
  {0x6A, 0x95, 0x0 },
  {0x5A, 0xA5, 0x0 },
  {0x47, 0xB8, 0x0 },
  {0x31, 0xCE, 0x0 },
  {0x19, 0xE6, 0x0 },
  {0x0,  0xFF, 0x0 },
  {0x0,  0xE6, 0x19},
  {0x0,  0xCE, 0x31},
  {0x0,  0xB8, 0x47},
  {0x0,  0xA5, 0x5A},
  {0x0,  0x95, 0x6A},
  {0x0,  0x8A, 0x75},
  {0x0,  0x82, 0x7D},
  {0x0,  0x80, 0x7F},
  {0x0,  0x7D, 0x82},
  {0x0,  0x75, 0x8A},
  {0x0,  0x6A, 0x95},
  {0x0,  0x5A, 0xA5},
  {0x0,  0x47, 0xB8},
  {0x0,  0x31, 0xCE},
  {0x0,  0x19, 0xE6},
  {0x0 , 0x0 , 0xFF},
  {0x19, 0x0,  0xE6},
  {0x31, 0x0,  0xCE},
  {0x47, 0x0,  0xB8},
  {0x5A, 0x0,  0xA5},
  {0x6A, 0x0,  0x95},
  {0x75, 0x0,  0x8A},
  {0x7D, 0x0,  0x82},
  {0x7F, 0x0,  0x80},
  {0x82, 0x0,  0x7D},
  {0x8A, 0x0,  0x75},
  {0x95, 0x0,  0x6A},
  {0xA5, 0x0,  0x5A},
  {0xB8, 0x0,  0x47},
  {0xCE, 0x0,  0x31},
  {0xE6, 0x0,  0x19}

static void incrementColorIndex(void)
  if (colorStep <= 1)
	if (colorStepped >= colorStep)
		colorStepped = 1;

  if (colorIndex >= 48)
    colorIndex = 0;

void halButtonIsr(uint8_t button, uint8_t state)
  // button: BUTTON0 BUTTON1
  if((button == BUTTON0) && (state == BUTTON_PRESSED))
	  colorStep = 0;
	  colorFixed = 0x1;
  else if((button == BUTTON1) && (state == BUTTON_PRESSED))
	colorFixed = 0x0;

As you can see the two buttons can change between a mode where the color is fixed across the network and a mode where we cycle through the RGB-gamut. I had to create a non-linear color table (colorTableSine) because our eyes will view all shades in the corners of the gamut as that color, and if you cycle the colors linearly it will look like it's racing past the intermediate colors (purple, teal and yellow).



In client-implementation.c you can then replace the contents of reportDataToServer with a function that will run a GET-command instead of the POST-command. My version of reportDataToServer looks like this:

static void reportDataToServer(void)
  // We peridocally get data from the server. 
  // The actual data is read and used in processServerDataGet()

  EmberStatus status;

  status = emberCoapGet(&server,

  if (status == EMBER_SUCCESS) {
  } else {
    emberAfCorePrintln("ERR: Reporting failed: 0x%x", status);

As you might guess you also have to add a processServerDataGet. This looks like this:

static void processServerDataGet(EmberCoapStatus status,
                                 EmberCoapMessage *coap,
                                 void *appData,
                                 uint16_t appDatalength)
  // We track the success or failure of reports so that we can determine when
  // we have lost the server.  A series of consecutive failures is the trigger
  // to detach from the current server and find a new one.  Any successfully-
  // transmitted report clears past failures.

	  failedReports = 0;
	} else {
	  emberAfCorePrintln("ERR: Report timed out - failure %u of %u",
	if (failedReports < REPORT_FAILURE_LIMIT) {
	} else {

  uint8_t coapmessage[4] = {0};
  // Getting the contents of the CoAP GET request
  MEMCOPY(coapmessage, (void*)coap->payload, coap->payloadLength);

  // Set the color of the LEDs accordingly
  BOARD_rgbledSetColor(coapmessage[0], coapmessage[1], coapmessage[2]);

  // Printing the response for debugging purposes
  emberAfCorePrint("Got %ld %ld %ld from ", coapmessage[0], coapmessage[1], coapmessage[2]);

That's it! Here's another picture of multiple Thread nodes all lighting in the same color:

Thread - Multiple colored nodes



Merry Christmas!




  • 32-bit MCUs
  • Projects
  • Wireless
  • Hello @Alf

    Very good and interesting project.

  • And the employees here find new ways of using the Thunderboard Sense:


    Thread - Connected Light

    Thread connected Christmas decoration.


    Thread - Connected Snowman

    And a Thread connected snowman.

  • Hi Alf!


    Is it possible to get access to the Silabs Thread/ZigBee stack without buying a Mighty Gecko WSTK?


    Kind regards


  • @michael_k: nope.


    Silicon Labs's Thread/Zigbee stacks are by far the most complete, stable and tested mesh stacks in the world, and this is our way of charging for that premium software Robot Happy


    Also, to develop networking applications like this, you really want the tools that the Wireless STKs provide, like Packet Trace interface and the ability to put nodes around the office and download real-time packet data just by connecting it an Ethernet drop. Our view is that if we gave you the Thread stack to run on a Thunderboard Sense you would probably get the initial examples up running, but would have a hard time moving beyond that.


    $499 might be a tough entry point for some, but you have to remember that this is a complete lab-setup for developing mesh-based wireless applications on the world's most used ZigBee/Thread stack.


    As an example, here's the view of the mesh networked TB-S:

    Thread - Node overview

    0000 is my server, 2C00 is the second WSTK in the network and 3000 is the most remote node (opposite end of the office from my server). What I tried to figure out is the strongest path of communication across the office.


    Edit: Added screenshot as attachment for quality.