This project is part of a bigger idea, which is to make my home a little smarter, so I am currently working on some voice control capabilities for my kitchen (another story that might make it into it's own hackster.io project). Whenever you choose voice as your user interface you have to provide some form of feedback system to your user e.g. popup an animation on a screen when the system is listening or play a sound when a command is recognized. In my use case neither of these options seems appropriate as I don't have a screen which is always turned on when I interact with the system and I don't want to hear the same annoying sounds over and over again. So I came up with the idea of integrating some notification effects into my room's lighting.
NeoPixels are a perfect fit for this project. Combined with a MediaTek LinkIt Smart 7688 Duo they can be controlled and integrated wirelessly with my existing home automation system.
Make sure to checkout the Developers Guide for the LinkIt Smart 7688 Duo. I won't describe the initial steps to setup the board and the WiFi connectivity as this is shown in detail in the Developers Guide.
HardwareObviously you will need a MediaTek LinkIt Smart 7688 Duo and a NeoPixel strip. Additionally, some wiring, resistors, a capacitator and a 5V power source are needed. As I am integrating this solution into my basic kitchen lighting I am using two 4m RGBW NeoPixel strips with 240 LEDs each. Although LEDs are very power efficient the power consumption adds up quickly when using a lot of them. So I went for a serious 40A/5V power supply. You should add a 470Ω resistor between the LinkIt Smart 7688 Duo and each Neopixel strip as well as a 1000µF capacitator. You can find a lot more details on how to calculate the power requirements and how the basic wiring is done for the NeoPixels in the Neopixel Uberguide.
The LinkIt Smart 7688 Duo can be powered by a 3,3V power source through the break out pins or by a 5V power source through a micro USB connector. To use the same power source for the board and the LEDs I used a standard micro USB cable, cut off the connector I didn't need and connected the red (+5V) and the black (GND) wires directly to the power supply.
So when everything is connected it looks like this:
You might be tempted to choose the GPIOs D0 and D1 to connect the Smart 7688 Duo to the NeoPixels, but these are used for the internal serial communication between the MPU and the MCU which we will need later on. So I chose the first available GPIOs D2 and D3.
SoftwareThe software consists of three components. A client application which controls the device written in C#, a Python script which handles the wireless connectivity through a UDP server and an Arduino sketch for the communication with the NeoPixels.
NeoPixels are easily controlled by an Arduino. As there are numerous tutorials online and the Uberguide is rather comprehensive I will not go too much into detail. That being said here are the steps to get started:
- Install the Adafruit NeoPixel Library and add it to your project.
#include <Adafruit_NeoPixel.h>
- Define the used GPIO(s).
#define PIN1 2
#define PIN2 3
- Set the number of pixels on your strip.
const int numPixels1 = 240;
const int numPixels2 = 240;
- Define the strip objects.
Adafruit_NeoPixel strip1;
Adafruit_NeoPixel strip2;
- Setup the strip objects according to hardware configuration. Mine are GRBW. I added a small delay after the initialization, because sometimes only the first strip I initialized responded to commands later on. After adding the delay I no longer experienced that issue.
void setup()
{
strip1 = Adafruit_NeoPixel(numPixels1, PIN1, NEO_GRBW + NEO_KHZ800);
delay(100);
strip2 = Adafruit_NeoPixel(numPixels2, PIN2, NEO_GRBW + NEO_KHZ800);
delay(100);
strip1.setBrightness(255);
strip1.begin();
strip1.show(); // Initialize all pixels to 'off'
strip2.setBrightness(255);
strip2.begin();
strip2.show(); // Initialize all pixels to 'off'
// start serial port and wait for port to open:
Serial.begin(115200);
Serial1.begin(115200);
}
That's it, you are ready to use the NeoPixels.
I added some helper methods to perform some simple effects. You can add as many different effects as you need.
void fadeIn()
{
isOn = true;
for(int j = 0; j<=51; j++)
{
for(int i = 0; i<numPixels1; i++)
{
strip1.setPixelColor(i, 0, 0, 0, 5*j);
strip2.setPixelColor(i, 0, 0, 0, 5*j);
}
strip1.show();
strip2.show();
delay(15);
}
}
void fadeOut()
{
isOn = false;
for(int j = 51; j>=0; j--)
{
for(int i = 0; i<numPixels1; i++)
{
strip1.setPixelColor(i, 0, 0, 0, 5*j);
strip2.setPixelColor(i, 0, 0, 0, 5*j);
}
strip1.show();
strip2.show();
delay(15);
}
}
void fire()
{
int r = 255;
int g = r-60;
int b = 20;
for(int x = 0; x <numPixels1; x++)
{
int flicker = random(0,150);
int r1 = r-flicker;
int g1 = g-flicker;
int b1 = b-flicker;
if(g1<0) g1=0;
if(r1<0) r1=0;
if(b1<0) b1=0;
strip1.setPixelColor(x,r1,g1, b1,0);
strip2.setPixelColor(x,r1,g1, b1,0);
}
strip1.show();
strip2.show();
delay(random(10,20));
}
void rainbowCycle(uint8_t wait) {
uint16_t i, j;
for(j=0; j<256*5; j++) { // 5 cycles of all colors on wheel
for(i=0; i< strip1.numPixels(); i++) {
strip1.setPixelColor(i, Wheel(((i * 256 / strip1.numPixels()) + j) & 255));
strip2.setPixelColor(i, Wheel(((i * 256 / strip2.numPixels()) + j) & 255));
}
strip1.show();
strip2.show();
delay(wait);
}
}
// Input a value 0 to 255 to get a color value.
// The colours are a transition r - g - b - back to r.
uint32_t Wheel(byte WheelPos) {
WheelPos = 255 - WheelPos;
if(WheelPos < 85) {
return strip1.Color(255 - WheelPos * 3, 0, WheelPos * 3);
}
if(WheelPos < 170) {
WheelPos -= 85;
return strip1.Color(0, WheelPos * 3, 255 - WheelPos * 3);
}
WheelPos -= 170;
return strip1.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
}
And finally here comes the plumbing to connect the NeoPixel code to the Linux part. The MPU and the MCU are connected through a serial connection (Serial1). During the main loop we read data from Serial1 and dispatch it according to one of three states:
1. Initial State
No known command is currently processed. The loop is waiting until the byte value 255 is received and discards every other sent data. Once 255 is received the state changes to Command State (isCommand = true).
2. Command State
During the Command State the next received byte determines which command is issued. Once the next byte is received the command is set and the state changes to Processing State.
3. Processing State
While in Processing State all data is sent to the handleCommand method. Depending on the currently processed command this method determines what to do with the data and when the command is completely processed. A command can be as simple as a single dummy byte to trigger a predefined effect or it can consist of a big data array containing multiple frames which are to be sent to the NeoPixels. After the command is finished the state is reset to Initial State.
void handleCommand(byte Command, byte Value)
{
switch(Command)
{
//Toggle
case 1:
isCommand = false;
command = 0;
if(isOn)
{
fadeOut();
}
else
{
fadeIn();
}
break;
//Turn on
case 2:
isCommand = false;
command = 0;
fadeIn();
break;
//Turn off
case 3:
isCommand = false;
command = 0;
fadeOut();
break;
}
}
void loop()
{
if (Serial1.available()>0)
{
int value = Serial1.read();
if(value == 255 && command == 0)
{
isCommand = true;
}
else if(isCommand && command == 0)
{
command = value;
}
else
{
handleCommand(command, value);
}
}
}
Flashing the sketch onto the LinkIt Smart 7688 Duo
As long as your development board is connected to your PC through an USB cable the firmware upload process just works straight forward through the Arduino IDE. Once you deploy the board to its intended location a more convenient way will be to update the firmware remotely. Luckily the OpenWRT distribution on your LinkIt Smart Duo comes preloaded with the necessary tools.
- First of all you have to compile the Firmware.
- Next you should transfer the compiled firmware to your LinkIt Smart Duo board. As I am no Linux-Guy I use WinSCP for that. It is a mighty tool which is easy to use and best of all it is free. To connect to your board you have to enter the IP address, select the SCP protocol, port 22 and your credentials.
- Now that you're logged in you can search the compiled firmware e.g. in File Explorer. You will find it in your project folder and it should be named like <project_name>.ino.with_bootloader.smart7688.hex. Transfer it to the root directory in WinSCP with drag and drop. (If you know what you're doing you can choose any appropriate tool for this task and maybe even choose another target directory).
- For the final step you will need another tool to login to your LinkIt Smart Duo via SSH. Probably the best tool for that on Windows is PuTTY and once again it is free.
- Choose the same IP and Port 22 like in WinSCP and choose SSH as Connection type. Click on Open and a console window asking for your credentials will appear.
- Now enter the following command to update the firmware using avrdude:
avrdude -p m32u4 -c linuxgpio -v -e -U flash:w:<your_script.ino>.with_bootloader.smart7688.hex -U lock:w:0x0f:m
That's it, now you have the Arduino part up and running.
Python ScriptThe next script will take care of the wireless connectivity for our service. The MPU part of the LinkIt Smart Duo is better suited for this task, as it has more memory and better processor performance. Therefore, this script is realized in Python and runs in the Linux environment.
The task is almost trivial, so the script isn't too complicated either. We just want to start a UDP Server and transfer all received data directly to the Arduino sketch. I chose UDP over TCP because simplicity and speed are more important than reliability in this scenario. When a sequence of frames is sent to the board it doesn't matter too much if a few frames get lost. Because of the high framerate the animation would stutter anyway, even if the lost data is sent again. If a simple command is dropped it can be sent again e.g. press the light switch once more. My WiFi connection is quite good, so even after several days of usage not a single command failed so far. So theoretically I could improve reliability by adding a handshake mechanism for the commands, but I am not sure if that will be necessary.
Ok, let's have a look at the code:
#!/usr/bin/python
import serial
import time
import socket
import sys
# Create a UDP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Bind the socket to the port
server_address = ('10.0.3.13', 8077)
print >>sys.stderr, 'starting up on %s port %s' % server_address
sock.bind(server_address)
s = None
def setup():
global s
# open serial COM port to /dev/ttyS0, which maps to UART0(D0/D1)
# the baudrate is set to 115200 and should be the same as the one
# specified in the Arduino sketch uploaded to ATMega32U4.
s = serial.Serial("/dev/ttyS0", 115200)
if __name__ == '__main__':
setup()
while True:
print >>sys.stderr, '\nwaiting to receive message'
data, address = sock.recvfrom(4096)
print >>sys.stderr, 'received %s bytes from %s' % (len(data), address)
print >>sys.stderr, data
for x in data:
s.write(x);
To open a UDP socket you just have to provide the IP of the network interface which is to be used and the desired port.
# Create a UDP socket
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# Bind the socket to the port
server_address = ('10.0.3.13', 8077)
print >>sys.stderr, 'starting up on %s port %s' % server_address
sock.bind(server_address)
Next the serial connection to the MCU is initialized.
s = serial.Serial("/dev/ttyS0", 115200)
And finally an endless loop is waiting for data on the UDP port and forwards it directly to the serial connection.
while True:
data, address = sock.recvfrom(4096)
for x in data:
s.write(x);
What's almost more complicated than the script itself is getting it on the LinkIt Smart Duo and automatically running it when the board boots. The first important thing for that is the first line in the Python script itself.
#!/usr/bin/python
I honestly don't know exactly what that line does, but it is somehow required for the automatic startup. Now you can transfer the script to the root directory using WinSCP. Next you have to create a service which hosts the Python script. This is done by creating a new file with the following content:
#!/bin/sh /etc/rc.common
SCRIPT_NAME="Neopixel Script"
SCRIPT_PATH="/root/<your_python_script>.py"
LOG_FILE="/root/neopixel.log"
START=99
STOP=99
start() {
echo "Starting $SCRIPT_NAME"
$SCRIPT_PATH >> $LOG_FILE 2>&1 &
}
stop() {
echo "Stopping $SCRIPT_NAME"
killall `basename $SCRIPT_PATH`
}
This script will create the service with the given SCRIPT_NAME. The service executes the Python script and writes its output into the LOG_FILE. The values for START and STOP determine the order in which the services are started/stopped. By choosing a high value I make sure that the service starts after some needed basic services like the network stack. The startup script has to be placed in the /etc/init.d directory (use WinSCP). Finally to make the file executable its permissions have to be adjusted. This can be done directly in WinSCP. Just right click the file and choose Properties.
Make sure to tick all the X (executable) boxes or just enter 0755 in the Octal field and hit OK. Now restart your board.
It should only take a few seconds until the board is back online. Now you can login through the web interface (http://<board_ip>) and switch to the OpenWrt administration.
In the OpenWrt administration go to System>Startup. You should find your service at the bottom of the list. Just click on Disabled and the service will be added to the startup sequence.
Whenever the Board boots it will launch the service. The work on the LinkIt Smart Duo is now completed. The NeoPixel interface is running constantly on the MCU and the wireless connectivity runs as a service on the MPU. The final piece of the puzzle is a client which actually triggers the action. For the sake of completeness I am providing an example written in C#, but you can use almost any language that fits your environment as long as it supports UDP.
C# ClientThe client application sends the defined byte sequences to trigger the available effects.
using System.Net.Sockets;
namespace phigax.Connector.NeoPixel
{
public class NeoPixelConnector
{
UdpClient _client;
public NeoPixelConnector()
{
_client = new UdpClient();
_client.DontFragment = true;
_client.Connect("10.0.3.13", 8077);
}
public void Toggle()
{
byte[] data = new byte[] { 255, 1, 0 };
_client.Send(data, 3);
}
public void TurnOn()
{
byte[] data = new byte[] { 255, 2, 0 };
_client.Send(data, 3);
}
public void TurnOff()
{
byte[] data = new byte[] { 255, 3, 0 };
_client.Send(data, 3);
}
}
}
First of all the UDP client is initialized. I am setting the DontFragment flag so the byte sequence of a single command is either received completely or not at all, to avoid any flunky state in the Arduino sketch. The IP and port have to match the values set in the Python script.
UdpClient _client;
public NeoPixelConnector()
{
_client = new UdpClient();
_client.DontFragment = true;
_client.Connect("10.0.3.13", 8077);
}
Then I added some methods to conveniently send the byte sequences.
public void Toggle()
{
byte[] data = new byte[] { 255, 1, 0 };
_client.Send(data, 3);
}
public void TurnOn()
{
byte[] data = new byte[] { 255, 2, 0 };
_client.Send(data, 3);
}
public void TurnOff()
{
byte[] data = new byte[] { 255, 3, 0 };
_client.Send(data, 3);
}
For each command/effect you defined in the Arduino sketch you can add another method in the client class. This class can now be integrated into any .Net application.
Congratulations! If you made it this far you should have a working solution that can be extended and adapted to your specific needs.
Further points of interestI just scratched the surface to keep it simple. There are definitely some rough edges and stuff that could be optimized. Theses are some things I have in mind to improve the solution.
- Add more effects
- Add a command to send the frames directly from the client instead of generating them in the Arduino sketch
- Add a Handshake mechanism to confirm that a command was received / executed
- Add a configuration so you can change the IP or number of LEDs without recompiling anything
Feel free to add your own ideas in the comments section. I would love to hear what you have in mind.
Comments