Creating coordinated Christmas light displays with mesh networking

Originally published at:

Now that Particle’s third generation of devices have shipped, developers all over the world are unboxing, claiming and playing with their new Argons, Borons, and Xenons. And while there are thousands of things one can build with these new devices (like robot friends, or dog food monitoring systems), I’ve been working on a few projects designed to test and show off the mesh networking capabilities unique to the third generation hardware. In this post, I’ll introduce a new API for low-latency mesh network messaging and show how it can be used to create coordinated holiday light animations.

Introducing Mesh Pub/Sub for local network messaging

All of new mesh-ready devices have the ability to participate in a local mesh network with other devices. Each device is assigned an IPv6 address local to the network, and can communicate with other devices on the local mesh network via UDP.

And while it’s possible to implement UDP messaging yourself, Particle provides the ability to send multicast messages between devices on a local network via the Mesh.publish and Mesh.subscribe methods in the Particle Device OS firmware. These methods are similar to Particle.publish and Particle.subscribe, but instead of communicating through the Particle Device Cloud, Mesh Pub/Sub is for local network messaging only. Local-only messaging is a powerful capability that works regardless of the network’s connection to the outside world.

It’s also very fast. I wanted to see just how fast, so I decided to create a test project using Mesh pub/sub to play coordinated light animations across several devices. And, since it’s mid-December, it’s only fitting that those lights would be wrapped around mini-Christmas trees and set to holiday music!

Setting up the Meshmas Tree hardware

My hardware setup is simple, with five total third generation devices, though you could accomplish the same result with as few as three. My specific setup includes:

  • (1x) Particle Mesh network with an Argon as a gateway.
  • (1x) Particle Xenon connected to the PartiBadge (more on that below). This is solely so I can use the built-in piezo buzzer and menu user-interface (UI). You could breadboard a piezo and Xenon as well.
  • (3x) Particle Xenons connected to Neopixel strips.
  • (3x) 12-inch artificial Christmas trees I purchased online.

All five of the Particle devices are on the same mesh network. This is critical for Mesh pub/sub to operate correctly.

Writing the firmware

On the firmware side, there are two key pieces to this demo, the music player/message publisher, and the message subscriber/light controller.

For the music player, I modified the source code for the Particle PartiBadge that we provided to Spectra attendees earlier this year. The PartiBadge includes a piezo and menu UI for exploring device features, so it was easy to add new commands for playing festive holiday songs.

For the holiday songs themselves, I borrowed code from Particle’s 2017 Christmas Tree project. Specifically, I pulled the xmassongs.h file into the PartiBadge source and modified each song to perform a Mesh.publish each time a note is played. Here’s what it looks like for one of the songs:

void playWeWishYouAMerryXmas()
  for (int i = 0; i < noteCount * 3; i += 3)
    if (changeSong)
    Mesh.publish("tone", String(notes_weWishYouAMerryXmas[i]));
    tone(BUZZER_PIN, notes_weWishYouAMerryXmas[i], notes_weWishYouAMerryXmas[i + 1]);
    delay(notes_weWishYouAMerryXmas[i + 2]);
    Mesh.publish("no-tone", NULL);

Before a note is played on the Piezo, the code sends a publish message called tone that all of the devices on the local mesh network receive. Because the network is so fast, there is almost no perceptible delay between the audible tone and the blinking. Then, after the delay between notes, the code publishes another message, this time with a no-tone string. This is all that’s needed to mesh-enable my music player!

Next up, I need to subscribe to the messages from the player and light up my neopixel strips. I have the exact same firmware running on all three of my Xenons, and you can view the complete sketch here. The critical piece is in the setup() function, where I’m using Mesh.subscribe to set-up listeners for the “tone” and “no-tone” messages:

void setup()
  Mesh.subscribe("tone", playTone);
  Mesh.subscribe("no-tone", stopTone);

The playTone handler sets a lightUp boolean to true and the red, green and blue values to a random integer between 1 and 255. The stopTone handler flips the lightUp boolean back to false.

void playTone(const char *event, const char *data)
  lightUp = true;
  redValue = random(1, 256);
  greenValue = random(1, 256);
  blueValue = random(1, 256);
void stopTone(const char *event, const char *data)
  lightUp = false;

Finally, in the loop() function, I check the lightUp boolean and, if false, set the red, green and blue values to 0 to turn them off, before calling the setAllPixels function, which sets each pixel on the neopixel strip to the current red, green or blue value.

void loop()
  if (!lightUp)
    redValue = 0;
    greenValue = 0;
    blueValue = 0;
void setAllPixels()
  for (int i = 0; i < strip.numPixels(); i++)
    strip.setPixelColor(i, redValue, greenValue, blueValue);

The end result is a pleasing, coordinated light show powered by a Particle Mesh network!

To the naked eye, it sure looks to be pretty well synchronized to me! And while the latency of mesh pub/sub will vary based on distance between devices on the network and the general RF conditions you’re operating that network in, it’s pretty cool to see this new messaging API operate so seamlessly out of the box!

Now it’s your turn! What can you do this holiday season to explore the power of Particle Mesh and your new devices? Be sure to let us know on the comments, or post your #ParticlePowered mesh-enabled holiday projects to Twitter, Facebook and Instagram and tag us!


You beat me to it! (by a mile)

A couple of years ago, we got a set of the Peanuts Gang Christmas Light Show figures for our son (he loves Charlie Brown). The figures communicate wirelessly and perform coordinated light shows:

The kids were playing with them this weekend, and it made me think of doing a similar set of coordinated lights via Particle Mesh. But now you’ve done the work for me! :wink:


Oooh that’s awesome @dougal, thanks for sharing! What I have so far is defintiely very basic broadcast messaging, but I’d love to add some coordination between devices to get that sequenced look as well. Very cool!

These things are from the Hallmark Store. I should open them up sometime and see if I can figure out what kind of radio they’re using. I’m guessing maybe some 433MHz transceivers? Hmm, maybe I could even sniff them with an SDR and see if I can figure out their protocol… (you know, in my Copious Free Time)


I posted on youtube before coming here. Almost complete newbie, I am hoping to make a sensor net on my 10-acre property, using xenon to capture the PIR and Boron to send out. I’ll figure out how to log later. I found only this regarding Particle and PIR:
PIR and Photon
It has only 4 lines of code (webooks-push.ino),

void setup() {
    pinMode(D0, INPUT);

void loop() {
    if (digitalRead(D0) == HIGH) {
        Particle.publish("office-motion", "OFFICE", 60, PRIVATE);
        while (digitalRead(D0) == HIGH); // hang tight here until motion stops

I connected the pins on the PIR to the D0, 3.3v and Grnd. When flashed to Xenon, it keeps sending it into safe mode. I have to flash different code to get it out of safe mode.

So I’m not sure where to go. If I can solve that, I’ve already played with simple Particle.publish functions in examples. The Mesh.publish() will be next.

Looking for other examples or troubleshooting advice.

@rick.roades, are you flashing your Xenon via the Web IDE? Did you select the latest rc27 as the firmware target? The Xenon will take a while to update if it is also updating its DeviceOS. This will make the Xenon appear like it is stuck in Safe mode but it will be going on and off as the firmware loads. Once it is completely finished, it should come back online.

@peekay123, Thanks for responding, MUCH appreciated. Yes, via web IDE, but I didn’t think to check the DeviceOS. I just looked, it’s set to rc27, and is on the device. I just tried to flash again, looks like that part is fine. I’ll retry the PIR, and a couple others. Having tried this several times last night, I can only say it’s the lack of experience with the devices, and I “panicked” too soon?

@rick.roades, usually going into safe mode means the DeviceOS version and the app you are trying to flash don’t match. More specifically, the app is compiled at a higher version. Which gateway are you using (Argon, Boron, Xenon/Ethernet)?

The code is from a photon post in 2015, and is really simple. But I follow the connection there, so I’ll keep an eye on that. I’m using an Argon gateway. I can ping the xenon from the console, but with the PIR connected, I don’t get any events logged.

@rick.roades, exactly which sensor are you using? If it is a 5v sensor, it may not be usable with the Mesh devices since the pins are not 5V tolerant like the Photon’s.

I honestly don’t know - I bought them 3-4 years ago when I first started playing with Arduino, but they aren’t marked. But trying to find them on Amazon, it comes up as HC-SR501, and says they are 3.3v. the “brand” is 2013Newestseller

@rick.roades, doing a quick search for that model indicates that they do put out 3.3v logic levels but need to be powered with a 5-20V source. So power the unit via the VUSB pin of the Xenon and see if it works. :wink:

That’s IT!! I’m getting events in the console. Now, how do I convince my wife that a 59 year old man acting like a giddy kid is not a danger to myself or others?? :smiley:

Thank you VERY MUCH! I’m off and running to my next obstacle. Whatever it may be!


@rick.roades, your talking to a 58 year old Elite so BE GIDDY!


Should future project questions go to a specific forum? I’m getting away from this topic. But my next challenge is power. First, I’ve moved code to my Boron, which is live and responding. I have the PIR on it, working on USB. In the field, I’ll need to power with battery, charging with solar. But using a Li-Po with on-board JST is 3.7, doesn’t power the PIR. I have 2 different device charger packs (anger, ravpower), which give me 5v USB but I guess the Boron/Xenon/Argon, do not draw enough amps because the chargers sleep after about 15-30 seconds. I’ll also need to add solar to charge anything I do use as power.

You could use a 3.3V to 5.0V step-up converter to power the sensor.

Try powering the PIR via the Boron’s LI+ pin, that might help. It’s worth a try.

[Edit] I just tried it and it works on a Boron LTE with a Li-Po.

Thanks @ScruffR - I don’t have the step-up, so will try @Rftop’s suggestion. But even before that, on USB, I have to figure out why the PIR keeps going HIGH non-stop without any activity, or even with the sensor in a small box blocking everything.

It will hold the output high for like 7 seconds after activity, but that’s adjustable.
This code works as expected:

  Pinout :
PIR     Boron
VCC     LI+
OUT     D3
Gnd     Gnd

void setup() {
pinMode(D3, INPUT);
pinMode(D7, OUTPUT);

void loop() {

if (digitalRead(D3) ==1) {
    digitalWrite(D7, HIGH); 
    digitalWrite(D7, LOW);

Thanks for the sample. My issue is that it just never quits triggering.

I borrowed from a couple examples, and for LED, one for PIR and one for functions (Particle.publish()). Maybe I’m getting ahead of myself.

I’ll test your config shortly. As is, here’s the sample code I was combining/testing, I’m sure it needs cleaning up.

int led1 = D0;
int led2 = D7;
// RLR my pin for PIR
int motion = D1;
int n = 0;
// RLR network.
    // Boron, Cellular = 01-01-00 (Network-Section-Zone) (Zone if needed)
        // Other units will be 01-01-01

String monitor_unit = "01-01-00";
// Last time, we only needed to declare pins in the setup function.
// This time, we are also going to register our Particle function

void setup()

   // Here's the pin configuration, same as last time
   pinMode(led1, OUTPUT);
   pinMode(led2, OUTPUT);
  // RLR my addition
   pinMode(motion, INPUT);

   // We are also going to declare a Particle.function so that we can turn the LED on and off from the cloud.
   // This is saying that when we ask the cloud for the function "led", it will employ the function ledToggle() from this app.

   // For good measure, let's also make sure both LEDs are off when we start:
   digitalWrite(led1, LOW);
   digitalWrite(led2, LOW);


// Last time, we wanted to continously blink the LED on and off
// Since we're waiting for input through the cloud this time,
// we don't actually need to put anything in the loop
// RLR - I'm adding Motion detection. So I want to turn the LED on while there is motion
//  or rather as long as the PIR is in the state of detection before resetting.

void loop()
    if (digitalRead(D1) == HIGH) {
        Particle.publish("Motion", monitor_unit);
        while (digitalRead(D1) == HIGH); // hang tight here until motion stops

// We're going to have a super cool function now that gets called when a matching API request is sent
// This is the ledToggle function we registered to the "led" Particle.function earlier.

int ledToggle(String command) {
    /* Particle.functions always take a string as an argument and return an integer.
    Since we can pass a string, it means that we can give the program commands on how the function should be used.
    In this case, telling the function "on" will turn the LED on and telling it "off" will turn the LED off.
    Then, the function returns a value to us to let us know what happened.
    In this case, it will return 1 for the LEDs turning on, 0 for the LEDs turning off,
    and -1 if we received a totally bogus command that didn't do anything to the LEDs.

    if (command=="on") {
        // RLR added
       n = 0;
        Particle.publish("LED Status = ","Full Power");
        return 1;
    else if (command=="off") {
        n = 0;
        Particle.publish("LED Status = ","Shutting Down");
        return 0;
    else if (command=="blink") {
        // RLR Added the blink section.
        n = 1;
        while (n == 1) {
            // To blink the LED, first we'll turn it on...
            digitalWrite(led1, HIGH);
            digitalWrite(led2, HIGH);
            // We'll leave it on for 1 second...
            Particle.publish("LED Staus = ","1");
            // Then we'll turn it off...
            digitalWrite(led1, LOW);
            digitalWrite(led2, LOW);
            // Wait 1 second...
            Particle.publish("LED Staus = ","0");
        return n;
    else {
        return -1;