Voice Controlled Christmas Tree Lights

What we’re building:

It’s a great project to get started with learning about IoT, and Alexa.

Hardware

Circuit Diagram

Fritzing file can be found here: https://github.com/dvas0004/SmartMote/blob/master/SmartMote_v1.fzz

Notes, tips and tricks about the Hardware

  • The mini relay uses 3V – so we connect this to the 3.3V output pin on the particle photon
  • The NeoPixels require 5V to power them – so we connect this to the VIN pin on the photon, which – despite it’s name – OUTPUTS about 5V when the photon is powered via a mini-USB cable
  • Regarding the relay, you need to cut ONE wire from the christmas tree’s lights. One end of that wire goes into “COM” while the other end goes into “NO”

Photon Code

// This #include statement was automatically added by the Particle IDE.
#include <neopixel.h>

// From particle documentation: enable OS system threading
SYSTEM_THREAD(ENABLED);

// This is where the NeoPixel Stick is plugged in
int led = D2; 

// This is where the mini-relay "set" and "unset" pins are connected respectively
int RELAY_SET = D3;
int RELAY_UNSET = D4;

// Even though my stick has 8 LEDS, the last two wouldn't work if unless I used 10
int NUMBER_OF_LEDS = 10;

// From Adafruit documentation : initialize the stick. Note the use of "SK6812RGBW"
// you may need to experiment with the last value - this is the one that worked for me
Adafruit_NeoPixel strip = Adafruit_NeoPixel(NUMBER_OF_LEDS, led, SK6812RGBW);

// Thread setup. "continue_thread" is used as a flag to control the thread
bool continue_thread = false;
Thread thread("ledThread", threadFunction);


// Next we go into the setup function.
void setup() {
    
    
    // First, declare all of our pins. This lets our device know which ones will be used for outputting voltage, and which ones will read incoming voltage.
    pinMode(led,OUTPUT); // Our LED pin is output (lighting up the NeoPixel)    
    pinMode(RELAY_SET, OUTPUT);
    pinMode(RELAY_UNSET, OUTPUT);
    
    // We are also going to declare a Particle.function so that we can turn the NeoPixel LEDs on and off from the cloud.
    Particle.function("led",ledToggle);
    // We do the same for the relay    
    Particle.function("relay",relayToggle);

    // Make sure the NeoPixel is blank when we start
    strip.begin();
    strip.clear();
    strip.show();

}


// Next is the loop function...

void loop() {

    // In this case we actually dont do anything here - all the work is done by the thread
    
}

void threadFunction(void *param) {
  strip.begin();
  int counter = 0;
	while(true) {
      // TODO: cleanup the code below - there's some repetition with two "switch" statements that can be removed
	    if (continue_thread) {
            // FADE IN the LEDs, and cycle between red, green and blue
            for( int a = 0; a < 150; a = a + 5 ) {
                for( int l = 0; l < NUMBER_OF_LEDS; l = l + 1 ) {
                    
                    int r = 0;
                    int g = 0;
                    int b = 0;
                    
                    switch (counter){
                        case(0): 
                            r = a;
                            break;
                        case(1):
                            g = a;
                            break;
                        case(2):
                            b = a;
                            break;
                    }
                    //GRBW
                    strip.setPixelColor((uint16_t)l, (uint8_t)g, (uint8_t)r, (uint8_t)b, (uint8_t)0);
                }
                strip.show();
                delay(100);
            }
            // FADE OUT the LEDs, and cycle between red, green and blue
            for( int a = 150; a >= 0; a = a - 5 ) {
                for( int l = 0; l < NUMBER_OF_LEDS; l = l + 1 ) {
                    
                    int r = 0;
                    int g = 0;
                    int b = 0;
                    
                    switch (counter){
                        case(0): 
                            r = a;
                            break;
                        case(1):
                            g = a;
                            break;
                        case(2):
                            b = a;
                            break;
                    }
                    //GRBW
                    strip.setPixelColor((uint16_t)l, (uint8_t)g, (uint8_t)r, (uint8_t)b, (uint8_t)0);
                }
                strip.show();
                delay(100);
            }
            counter = (counter + 1)%3;
        } else {
            // If the control variable "continue_thread" is set to False, - clear the neopixels
            for( int b = 0; b < NUMBER_OF_LEDS; b = b + 1 ) {
                strip.setPixelColor((uint16_t)b, (uint8_t)0, (uint8_t)0, (uint8_t)0, (uint8_t)0);
            }
            strip.show();
            delay(1000);
        }
	}
}

// Define our relayToggle function, which we can call through the particle cloud

int relayToggle(String command) {
    if (command=="on"){
        digitalWrite(RELAY_UNSET, LOW);
        digitalWrite(RELAY_SET, HIGH);
        return 1;
    }
    else if (command=="off"){
        digitalWrite(RELAY_SET, LOW);
        digitalWrite(RELAY_UNSET, HIGH);
        return 0;
    }
        
    
    return -1;
}


// Define our ledToggle function, which we can call through the particle cloud

int ledToggle(String command) {
    
    if (command=="on") {
        
        if (!continue_thread){
            continue_thread=true;
        }
        
        return 1;
    }
    else if (command=="off") {
        continue_thread=false;
        strip.begin();
            
        return 0;
    }
    else {
        return -1;
    }

}

Code can be found online: https://github.com/dvas0004/SmartMote/blob/master/SmartMote.ino

Notes, tips and tricks about the Photon Code

  • You must include the “neopixel” library using the photon IDE
  • For some reason, I needed to use a white lie and pass “10” instead of “8” as the number of LEDs on my NeoPixel stick in the “Adafruit_NeoPixel” constructor, otherwise the last two LEDs didn’t work
  • In the same constructor, make sure to set the correct model – in my case above “SK6812RGBW”. If your LEDs dont light up, light up in the wrong order, refuse to switch off, get the wrong colors and so on – this is the parameter to play around with (after checking for hardware issues….). It’s a good idea to read through the NeoPixel Uberguide for more details:
  • I used threading here ( hence the SYSTEM_THREAD(ENABLED) ), because I wanted to cycle the NeoPixels through red, green and blue as shown in the video below, while fading in and out. In the meantime I wanted the main thread (“loop”) to be free for other functions i’ll add later, and for retrieving instructions from the Particle Cloud API. It’s a good idea to review this post to get a good idea about threading:
why there’s so much code in that thread…

At this stage of the process, you should be able to login to your particle cloud console, and in the “my devices” section, click the relevant photon and you should see two functions: “led” and “relay“. If you have the neopixels, type in “on”/”off” in the “led” function argument and the LEDs should come on/off. Same for the relay – you should hear a little “click” when the relay changes state – and the featherwing onboard LED changes color

Adding Voice Control – Enter Amazon Alexa

This is actually pretty straightforward if you know one particular GOTCHA: you have to follow the below instructions while in the “US East (N. Virginia)” zone. No other zone will work, even if your Alexa is set to use the “English – UK” language (as opposed to “English – US”).

Use the “US East N. Virginia” zone – even if you’re developing for UK language

With that out of the way, developing the skill is actually quite straightforward, simply follow the tutorial written here:

https://github.com/alexa/alexa-smarthome/wiki/Build-a-Working-Smart-Home-Skill-in-15-Minutes

When it came to the actual lambda code, I changed the following:

  • The “SAMPLE_APPLIANCES” array was trimmed down to just one “appliance” – the particle photon. And because I wanted to say “Switch on the Christmas Tree”, the “friendlyName” also needed to be changed, resulting in something like this:
SAMPLE_APPLIANCES = [
    {
        "applianceId": "endpoint-001",
        "manufacturerName": "Sample Manufacturer",
        "modelName": "Smart Switch",
        "version": "1",
        "friendlyName": "Christmas Tree",
        "friendlyDescription": "Christmas Tree switch that can only be turned on/off",
        "isReachable": True,
        "actions": [
            "turnOn",
            "turnOff"
        ],
        "additionalApplianceDetails": {
            "detail1": "For simplicity, this is the only appliance",
            "detail2": "that has some values in the additionalApplianceDetails"
        }
    }
]

Integrating AWS to Particle Cloud

The last bit is allowing our lambda function to actually send commands to our particle photon – which we’ll do via the Particle Cloud API. Remember those “led” and “relay” functions? Those functions are very conveniently exposed via a REST API by Particle. Read up on the API here:

https://docs.particle.io/reference/device-cloud/api/

TL;DR: at least get your API token from the Particle Build web IDE on the ‘Settings’ page.

So we’d like our AWS Lambda function to call the ‘led’ and ‘relay’ functions from the Particle Cloud API with the “on” or “off” argument. We need to modify the lambda code as follows

  • First, import the python “requests” module at the top of the file:
from botocore.vendored import requests
  • Right after, define some constants we need to communicate with our particle:
#particle cloud
auth_token='<from the WEB IDE Settings page>'
header={'Authorization': 'Bearer ' + auth_token}
particleID='<from the My Devices page in Particle Cloud>'
  • Then, define a function which will call the “led” and “relay” functions on the Particle Cloud, for our particular photon:
def callParticleFunction(on_off):
    functionName='relay'    url='https://api.particle.io/v1/devices/{}/{}'.format(particleID, functionName)
    data={'arg':on_off}
    requests.post(url, headers=header, data=data)

    # same thing for LED function    
    functionName='led'    url='https://api.particle.io/v1/devices/{}/{}'.format(particleID, functionName)
    requests.post(url, headers=header, data=data)
  • Last, we need to call the above function with the “on” parameter whenever we get a “Turn On” request from Alexa, and a similar call for “off”. In our lambda, find the functions “handle_non_discovery(request)“, and call our function in the appropriate if clause, like so:
def handle_non_discovery(request):
    request_name = request["header"]["name"]

    if request_name == "TurnOnRequest":
        callParticleFunction('on')
        # ... rest of pre-existing code ....
    elif request_name == "TurnOffRequest":
        callParticleFunction('off')
        # ... rest of pre-existing code ....

We do the same thing for v3 alexa requests, in the “handle_non_discovery_v3(request)” function:

def handle_non_discovery_v3(request):
    request_namespace = request["directive"]["header"]["namespace"]
    request_name = request["directive"]["header"]["name"]

    if request_namespace == "Alexa.PowerController":
        if request_name == "TurnOn":
            value = "ON"
            callParticleFunction('on')
        else:
            callParticleFunction('off')
            value = "OFF"
           # ... rest of pre-existing code ....

Done!

Don’t forget to follow the Amazon instructions and “discover” your devices, and if you run into problems always have a look at the AWS CloudFront logs from the Lambda function to see what exactly is tripping up

Advertisements