Introduction: NeoWeather: Ambient Weather Indicator

About: Developer at Electric Imp

We can't all have the desk next to the window. In my case, my coworker at the desk across from mine has the window seat. When it comes time to make the trek down the street to get some lunch, I'm never quite sure what I'm going to find in the great out-of-doors. Do I need my jacket? An umbrella?

I've had a theory for a little while that just a dozen (or two) pixels and an internet connection are all you need for a seriously vast number or applications. This seemed as good a place as any to start, so I connected an Adafruit Neopixel Ring to the internet with Electric Imp, cooked up some animations, and set it up to grab the forecast from Weather Underground. It turns out to be a snap to build, and not expensive!

My theory was that the geeks I work with would be unable to resist writing other applications for the shiny new toy, and it turned out I was right. By the end of the day that I built this project, it could already also be used to track packages or tell time. I'm really looking forward to seeing what else it can do.

The weather indicator is really handy - let's build one!

Step 1: Parts and Equipment

Here's what you'll need:

  • An Electric Imp card and an Electric Imp Breakout - About $37.50 total. Available from:
  • You'll need a Mini USB Cable, too - it's only for power. We'll be programming the imp entirely over WiFi.
  • Neopixel Ring! I used a 24-pixel one. You can use bigger or smaller, but I'll assume for the sake of mechanical design that you're using the 24-pixel variety, too. $25.
  • One general-purpose axial diode. We use this just to do a little hack to make the imp play nice with the Neopixels. You'll want one that can handle 1 A or more. Radioshack or Fry's keep these around, too. This will set you back a whopping $0.15 or so.
  • 4 4-40x1/2" Female/Male Standoffs. Hex or round is all up to you. $6.50 total.
  • 4 4-40 Nuts to secure the standoffs. A pack of 100 goes for $0.81.
  • 4 4-40x1/4" Phillips-Head Screws. $4.33 for a pack of 50.
  • Two 3"x3" squares of 1/8" Smoked Acrylic. I got mine from Tap Plastics in Mountain View; they gave me 9 of them for $8.00 (call it $1.00 a square). Home Depot and similar may also be able to help out with this piece.
  • I picked up a plastic picture hanger while I was at TAP for $0.45.

Approximate parts cost: $77.00. If you have spare machine screws lying around, you save $10. (Or more accurately, you've already spent your $10 and don't have to spend it again)

You'll need some tools, too. This is the full wish list; you can make do without having every last one:

  • A computer. You'll program the Imp from your web browser.
  • An iOS or Android device. You only need this for one step (connecting the Imp), so you can borrow one from a friend if you don't have one of your own.
  • Acrylic drill bit. This one's pretty important. Drilling through acrylic with a wood drillbit is a nightmare; you'll almost certainly break the acrylic. A 1/8" hole is the right size for the 4-40 hardware we've picked out.
  • Drill
  • Soldering Iron and some solder
  • Ruler (A T-square is super helpful, if you can get one)
  • Philips Screwdriver (#1 drive)
  • Wire cutters / Wire strippers
  • X-Acto Knife
  • Hammer and small Centerpunch
  • Pressure-sensitive adhesive. I used a roll meant for mounting glass in shower doors, which is clear and very, very sticky. Basically very serious double-sided tape.

Step 2: Wire It Up

Ok, time to start building. Check out the circuit diagram above to see how this goes together. A couple notes on the electrical design here. If you just want to build it as drawn and not worry about the details, skip the bullet points:

  1. The Neopixels are intended to be used with a 5V power rail. We're going to provide this by powering our gadget from a USB connector (VBUS is 5V). When the imp is running from the 5V on the USB connector, the "Vin" pad on the Imp breakout passes 5V through. If you're planning to power your board some other way, keep this requirement in mind.
  2. I put a diode between VIN on the Imp breakout and PWR_IN on the Neopixel ring - strange! This is a bit of a hack, and it has to do with the fact that the Imp doesn't run at 5V. It runs at 3.3V. The breakout board includes a power supply that provides the Imp with the necessary 3.3V rail. As such, when the imp sends signals to the Neopixels, logic high is 3.3V, not 5V.
    • On some power supplies, the Neopixels don't mind this so much and they listen and do the Right Thing. On other power supplies, though, the difference between logic high for the Neopixels and logic high for the Imp is too great, so the Neopixels don't respond. The diode "fixes" this by dropping Vin at the Neopixels by about 1V. The Neopixels still work fine at 4V, and the difference in logic levels is much smaller.
    • The downside to this method is that the diode dissipates quite a bit of power. If you want to drive a lot of Neopixels (and use a lot of power), you should solve this problem in your design *properly* - by adding a level translator. You can make one out of a PFET and two 1kΩ resistors!
  3. Neopixels use a special 1-wire protocol to communicate, and timing is critical. The Imp doesn't support this natively, so we use the Imp's SPI bus to emulate it. By setting the SPI clock rate correctly and connecting the Imp's MOSI ("Master-Out, Slave-In") line to the Neopixel's Data Input, we can emulate the signals the Neopixels expect. The Imp needs to use SPI257 to do this (SPI189 won't go fast enough), so the Data In line must be connected to Imp Pin7. For more information on Neopixel communication, take look at Adafruit's excellent guide. You can also take a look at the WS2812 (Neopixel) datasheet.

Ok, with that out of the way:

  1. Connect your diode to the Imp breakout board. Keep the leads short if you can, as exposed wire creates an opportunity for you to short your power supply. You want the diode's anode connected to the breakout board. The cathode is the other end of the diode, and it has a little stripe around it. This should be farther from the breakout board.
    Tape the breakout board down, trim the diode's anode lead, and put it through the VIN pad on the breakout board. You can bend the diode down so it's parallel with the breakout board and tape it to hold everything in place. Heat the part and the pad with the soldering iron and apply some solder. Remember: heat the parts, not the solder. The solder should flow into the joint when the parts are hot enough.
  2. Trim a piece of wire (red, if you have it) to about 2" to 3", and strip 1/8" to 1/4" off each end. Position one end of the wire so it's touching the diode's cathode, and tape the wire in place. Heat the cathode wire and your new wire together, then apply solder.
  3. The other end of your red wire goes to the PWR terminal on the Neopixel ring. Tape the ring down, then solder the wire to it the same way you did with the Imp breakout.
  4. Trim another piece of wire (black, preferably), and solder this between one of the breakout's GND pads and one of the Neopixel ring's GND pads.
  5. Trim a third and final piece of wire (any unique color, now), and solder this one between the breakout's Pin7 pad and the Data In pad on the ring.

That's that! Let's test it out.

Step 3: Test It Out

Ok, at this stage, it's best to make sure everything is wired together correctly before we make things harder to work on.

  1. Head to ide.electricimp.com and register for an account. It's free.
  2. Download the Electric Imp developer app on your iOS or Android device. Also free. Just search for "Electric Imp"
  3. Sign into the Electric Imp app with the credentials you used on ide.electricimp.com.
  4. Add a new network. This is the WiFi network the Imp will connect to. Make sure to type the SSID and password accurately, of course.

    Now we're going to connect the Imp to the Internet. We'll send the SSID and password to the device with something called "BlinkUp". BlinkUp flashes the screen to send the information to the device optically, which takes about 30 seconds. If you're sensitive to bright, flashing light, get someone to help you out with this. BlinkUp also plays chimes at the start and end of the process to let you know when it's ok to look.
  5. Put the Imp into the socket on the breakout board. It'll click into place.
  6. Power the Imp on by plugging in the USB cable and making sure the jumper is set to "USB"
  7. The Imp will start blinking to show you its current status. Press "Send BlinkUp" on the phone and hold the screen flat against the little window on the end of the Imp. The Imp's LED should go out until the process is complete to show that it is listening.
  8. The Imp will go through a series of LED patterns as it connects (see what they mean here), then blink green when it connects.
  9. The device will now appear in "Unassigned Devices" in the top-left corner of the IDE. Click the arrow to show all the unassigned devices. Click the gear next to your new device in the list to give your device a name and create a new model. (You may need to refresh the IDE to make your device appear).

    An Electric Imp model includes two pieces: an Agent firmware and a Device firmware. The Agent firmware runs on a VM in the Electric Imp Cloud, and is the device's partner. The Device firmware runs on the Imp card. The Agent and Device can talk to each other easily, and the Agent acts as the Device's gateway to the Internet.
  10. Name your new model, then pick up some test code from github. Copy "WS2812.device.nut", the device firmware, into the Device window in the IDE.
  11. Make sure NUMPIXELS is set to the number of pixels you have (24) in your Neopixel ring on line 103 of the Device firmware.
  12. Click "Build and Run" to send the code to the device. The ring should light up and a little purple trail should start to run back and forth around the ring.

If it isn't working, check your connections. Look for solder joints that aren't secure, make sure your diode is the correct way around, and make sure the Data In line on the Neopixel Ring is connected to Pin7 on the Imp breakout.

When you've got that working, let's finish building it!

Step 4: Finish Up the Mechanical Bits

The electrical pieces work - so it's time to make it pretty.

  1. Measure the acrylic and mark the drill locations. Use the ruler to measure 1/4" from each edge. Mark two spots per edge, each side. Then use the square to draw lines between the marks. The intersections are your drill points.
  2. Center-punch the drill locations carefully. Don't hit too hard or you'll break the corner off the acrylic square.
  3. Use the acrylic drill bit to drill the holes. If you don't have an acrylic drill bit, drill with a very small drill bit first, then slowly step up the size. Run the drill as slow as you can - remember that fast doesn't mean more torque. Acrylic doesn't really drill. It's more like peeling. The acrylic drill bit minimizes stress on the plastic to manage this.
  4. When you're done drilling, you can peel the backing paper off. Try not to get the plastic dirty or scratched! You need to drill four corner holes in each square.
  5. Use wire cutters to trim any protrusions on the back of the breakout board as much as you can.
  6. Place a few strips of adhesive across the back of the breakout board. It's best if you cover the raised areas with adhesive.
  7. Put one of the standoffs through one of the holes and secure it with a nut. This helps make sure you don't cover the hole with the Imp breakout board. Line the board up so the Imp is flush with the edge of the acrylic when it's secured in the breakout board.
  8. Peel the backing off the adhesive and press the Imp breakout down onto the acrylic. Hold it in place for 10 seconds to make sure it's secure.

    Now we need to find a way to hold the Neopixels against the front acrylic.
  9. Cut some short strips of adhesive with scissors and apply them over 3 or 4 of the Neopixels. You can see this in the images - the backing on the adhesive is sort of red/pink.
  10. Trim around the perimeter of the LEDs with the X-acto knife. Be careful not to damage the PCB with the blade.
  11. Carefully peel the backing off the adhesive with the X-acto knife or a pair of tweezers.
  12. Line the dial up with your second, drilled acrylic square. As you're doing this, you'll want to line the "0th" pixel up with the bottom of your display. I oriented my USB connector pointing downward, so the cord could go up into the display when it's mounted on the wall. The 0th pixel is the one just to the right of the "In" label on the Neopixel silkscreen. It has little "mouse bites" in the PCB right under it, where the PCB panel was broken apart. If you're not sure, run the test code and see which pixel it starts at. When you've got it lined up, press the ring onto the acrylic for 10 seconds to secure it.
  13. Add the other three standoffs and nuts.
  14. "Close" the two halves together and secure them together with the 4-40 screws. If you're having trouble lining the halves up, you can loosen the nuts on the back slightly to let the standoffs move relative to each other. When you've got all the screws in place, tighten everthing down, but not too tight, so you don't break the plastic.
  15. If you've got a picture hook, you can secure it to the back of the gadget with a bit more adhesive now.

    Good to go! Now let's get it to show something useful.

Step 5: Load the Application Code

Time to put the last piece in place: the software. The weather code is available on github.

You may want to create a new model for this, so that you can keep your test code around in the IDE. If so, just click the gear next to your device name and type a new model name again. If the model doesn't already exist, it will be created.

Paste neoweather.device.nut into the device window, and neoweather.agent.nut into the agent window. You need to make sure two lines are set correctly to get this working:

At the very bottom of the device code, make sure NUMPIXELS is set to the number of pixels in your Neopixel ring:

// The number of pixels in your chain
const NUMPIXELS = 24;

At the very top of the agent code, make sure you supply your Weather Underground API Key. Click the link to get one; they're free. It'll take a minute to register. Be sure to keep your key safe:

// Add your own wunderground API Key here. <br>// Register for free at http://api.wunderground.com/weather/api/<a href="http://api.wunderground.com/weather/api/" rel="nofollow">
</a>
const WUNDERGROUND_KEY = "YOUR KEY HERE";
local WUNDERGROUND_URL = "http://api.wunderground.com/api/";

Click "Build and Run". The logs will show the device booting up, and the agent starting. As soon as the device checks in with the agent, it will trigger the agent to fetch the current forecast for its default location (Mountain View, CA). Since it'll definitely be sunny and 75º, the display will light up a nice warm orange/red.

When you're ready to point the device at a different locale, click on the Agent URL in the Agent window. This will open a request to that URL in another browser tab. This Electric Imp agent is programmed to act as a tiny webserver - it responds to blank requests by serving up a small web page. This web page provides you with a way to put in a new zip code or location string, and it serves up the 5-day forecast for the currently-set location, courtesy of forecast.io.

That's it, all done! If you like to take a look inside the code, check out the next two steps.

Step 6: What's in the Device Code?

The device code is long, but most of it is similar, repeated code for the specific weather effects included. Here's the basic parts:

The Neopixel base class, included by "requiring" the NeoPixels library. This class creates a blob (a data structure, somewhat like an array but capable of being read and written like a file stream) which is used as a frame buffer. The frame buffer can be modified at any time and then written out to the display. This class has three main functions:

  1. clearFrame: Clear the frame buffer (sets the values for all three channels of each pixel back to zero)
  2. writePixel: Write the value of a single pixel in the frame buffer
  3. writeFrame: Send the current frame buffer to the display

The NeoWeather Extension Class. This class extends the base class, using its functions as if they were its own, but also adding some new ones. The NeoWeather class includes new methods for each distinct animation. Some of these methods take a parameter - factor. This number is an integer from 0 to 9 that controls the "intensity" of the animation. In the rain and snow effects, increasing the factor increases the number of "drops" that appear on the display. Take a look at the rain method if you'd like to delve a little deeper into this class or add new animations of your own.

Instantiating the NeoWeather class, clearing the buffer, and writing pixel 0 to full-red looks like this:

const NUMPIXELS = 24;
// configure the SPI peripheral and use it to instantiate the class
spi <- hardware.spi257;
display <- NeoWeather(spi, NUMPIXELS);
spi.configure(MSB_FIRST, 7500);

// clear the frame buffer and write [255,0,0] (full red) to pixel 0
display.clearFrame();
display.writePixel(0,[255,0,0]);
display.writeFrame();	

Lastly, there's an agent handler with a great big stack of conditional statements. When the agent sends a "seteffect" message to the device, it arrives here. This function checks for keywords in the weather forecast in order of priority. If it finds a match, it sets the appropriate effect:

if (cond.find("Ice") != null) {<br>    display.ice();
}

In some cases, the forecast comes with a modifier (e.g. "Heavy Rain"). The device looks for that, too, and uses it to set the intensity of the appropriate effect:

if (cond.find("Rain") != null) {<br>   if (cond.find("Light") != null) {
        display.rain(3);
    } else if (cond.find("Heavy") != null) {
        display.rain(5);
    } else {
        display.rain(4);
    }
}


Lastly, let's take a look at the agent code.

Step 7: What's in the Agent Code?

The agent has two jobs:

  1. Talk to Weather Underground to get the latest conditions
  2. Serve up the web UI when needed so the user can update the location or see the extended forecast.

Let's go ahead and skip looking at "prepWebpage" for a moment. About 150 lines down, take a look at getConditions.

The getConditions function's most important piece looks like this:

local url = format("%s/%s/conditions/q/%s.json", WUNDERGROUND_URL, WUNDERGROUND_KEY, safelocationstr);
local res = http.get(url, {}).sendsync();

This snippet builds a request URL from the Weather Underground API's base URL and your API key, then sends it and waits for a response, which is stored in res. If all goes well, res then contains all the data you need about the weather as a block of JSON, which we can easily parse:

local response = http.jsondecode(res.body);<br>local weather = response.current_observation;
LAT = weather.observation_location.latitude.tofloat();
LON = weather.observation_location.longitude.tofloat();
LOCATIONSTR = weather.observation_location.full;

Eventually, the agent puts together a forecast for the logs, then sends the key piece on to the device:

device.send("seteffect", {conditions = weather.weather, temperature = weather.temp_c});

Look familiar? That's a call against the "seteffect" handler we saw in the device, where the forecast is parsed and the effects are set. This is where it comes from. The second parameter here is the data the device will receive: a table with two objects, one for the conditions string and one for the current temperature.

The second job of the agent, serving the web UI, is handled right below getConditions, in http.onrequest. This is the general HTTP request handler, and you can write it to do just about anything you would like in response to many kinds of HTTP requests. This one's pretty simple. It checks the path to see if the request came in to somewhere more specific than just the agent URL. There are two options here that will trigger specific behavior. If the path is set to "getLocation", it's a request to see where the current forecast location is (probably from the web UI):

if (path == "/getlocation" || path == "/getlocation/") {<br>        if (LAT == null || LON == null) {
            getConditions();
        }
        resp.send(200, http.jsonencode( { "lat":LAT,"lon":LON,"name":LOCATIONSTR } ));
    }

Likewise, if the path is set to "setLocation", it's a request to change the forecast location.

else if (path == "/setlocation" || path == "/setlocation/") {<br>    LOCATIONSTR = req.body;
    getConditions();
    resp.send(200, http.jsonencode( { "lat":LAT,"lon":LON,"name":LOCATIONSTR } ));
    // keep the latest user-set location through agent restarts
    savedata.locationstr <- LOCATIONSTR; 
    server.save(savedata);
}

If no path is supplied at all, the agent assumes it's a browser request, and hands back a great big multiline string - that happens to be a web page! This lets the agent act like a tiny web server:

else {<br>    resp.send(200, WEBPAGE);
}

That's all there is to it - and congrats on your new weather gadget. Enjoy!