Overview

Weather Reactive Pixels

This project uses an Adafruit Feather HUZZAH with ESP8266 WiFi and NeoPixels to make an IoT weather device.

The Arduino sketch pulls weather forecast data from the Yahoo Weather API and displays animations on NeoPixel LEDs.

Prerequisite Guides

Walk through the following guides to get familiar with the hardware usd in this project.

Circuit Diagram

Reference Connections

Use the circuit diagram to reference for connecting the components together. The diagram does not depict exact wire lengths or size of components.

This circuit expects 5V power to the Adafruit Feather HUZZAH ESP8266 via microUSB

Wired Connections

  • Pin 14 from HUZZAH to Data Input on NeoPixel
  • GND from HUZZAH to GND on NeoPixel
  • 3V from HUZZAH to PWR +5V on NeoPixel
  • EN from HUZZAH to Slide Switch
  • GND from NeoPixel to Slide Switch

Power On/Off

The slide switch opens/closes a connection to the EN (enable) pin on the Adafruit Feather HUZZAH ESP8266. 

The EN pin is connected to the 3.3V regulator. It's pulled up, so when it's connect to ground, the 3.3V regulator is disabled.

Software

This code no longer works as query.yahooapis.com has changed. There are no plans to update the code as this guide is now deprecated.

Adafruit HUZZAH 8266 Arduino Libraries

Be sure to follow the guide below to install the board and libraries. When you've successfully install the ESP8266 Arduino libraries, come back here and continue the tutorial.

Arduino Sketch

The arduino sketch will connect your WiFI network and query the current weather conditions using the Yahoo Weather API. The Feather HUZZAH ESP8266 will then animate some NeoPixels to display the weather condition. In the sketch, you'll need to add your WiFi credentials, change the number of pixels you're using and your desired city.

The github repo contains this project. Below select Download: Project Zip to download both weatherPixels.ino and animation.cpp. Place both these files in an Arduino sketch folder named weatherPixels.

#include <ESP8266WiFi.h>
void animSetup(void);
void animConfig(uint16_t, uint8_t, uint8_t, uint8_t, uint8_t, uint8_t);
void waitForFrame(void);
void renderFrame(void);



const char* ssid     = "adafruit";
const char* password = "ffffffff";
int8_t utc_offset = -5; // hours off of UTC, e.g. EST is -5 
const char* location = "boston%2C%20ma";

const char* path_prefix = "/v1/public/yql?q=select%20item.condition.code%2C%20item.condition.text%20%20from%20weather.forecast%20where%20woeid%20in%20(select%20woeid%20from%20geo.places(1)%20where%20text%3D%22";
const char* path_postfix = "%22)&format=json&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys";
const char* host = "query.yahooapis.com";
const int httpPort = 80;


int16_t weathercode = -1;
int16_t createhour, createmin;

void setup() {
  Serial.begin(115200);
  delay(10);

  // We start by connecting to a WiFi network

  Serial.println();
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);
  
  WiFi.begin(ssid, password);
  
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.println("WiFi connected");  
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());

  animConfig(0, 0, 0, 0, 0, 0);
  animSetup();
}

uint32_t timekeep=0xFFFF;

void loop() {
  uint32_t currTime = millis();
  // every 30 seconds (or if there's a rollover/first time running, update the weather!
  if ((timekeep > currTime)  || (currTime > (timekeep + 30000))) {
    timekeep = currTime;
    updateWeather();
  }

  waitForFrame();
  renderFrame();

}


void updateWeather() {
  
  Serial.print("Connecting to "); Serial.println(host);
  
  // Use WiFiClient class to create TCP connections
  WiFiClient client;
  if (!client.connect(host, httpPort)) {
    Serial.println("Connection failed");
    return;
  }
  
  // We now create a URI for the request

  String url = String(path_prefix) + String(location) + String(path_postfix);
  
  Serial.print("Requesting URL: ");  Serial.println(url);
  
  // This will send the request to the server
  client.print(String("GET ") + url + " HTTP/1.1\r\n" +
               "Host: " + host + "\r\n" + 
               "Connection: close\r\n\r\n");
  delay(500);

  weathercode = -1;
  // Read all the lines of the reply from server and print them to Serial
  while(client.available()){
    String line = client.readStringUntil('\r');
    int i = line.indexOf(String("\"code\":"));
    if (i < 0) continue;
    Serial.println(line);
    weathercode = (line.substring(i+8)).toInt();

    // extract hour and minute
    i = line.indexOf(String("\"created\":"));
    if (i < 0) continue;
    createhour = (line.substring(i+22)).toInt();
    createmin = (line.substring(i+25)).toInt();
  }
  
  Serial.println("Closing connection");

  // convert from UTC to local
  createhour += 24;
  createhour += utc_offset;
  createhour %= 24;
  Serial.print("\nWeather code: "); Serial.print(weathercode);
  Serial.print(" @ "); Serial.print(createhour); Serial.print(":"); Serial.println(createmin);

  // Get the current time of day, between 0 and 65535
  uint16_t timeofday = map((createhour * 60) + createmin, 0, 1440, 0, 65535);

  Serial.print("Time of day = "); Serial.print(timeofday); Serial.println("/65535");
  
 /* void animConfig(
 uint16_t t,   // Time of day in fixed-point 16-bit units, where 0=midnight,
               // 32768=noon, 65536=midnight. THIS DOES NOT CORRESPOND TO
               // ANY SORT OF REAL-WORLD UNITS LIKE SECONDS, nor does it
               // handle things like seasons or Daylight Saving Time, it's
               // just an "ish" approximation to give the sky animation some
               // vague context. The time of day should be polled from the
               // same source that's providing the weather data, DO NOT use
               // millis() or micros() to attempt to follow real time, as
               // the NeoPixel library is known to mangle these interrupt-
               // based functions. TIME OF DAY IS "ISH!"
 uint8_t  c,   // Cloud cover, as a percentage (0-100).
 uint8_t  r,   // Rainfall as a "strength" value (0-255) that doesn't really
               // correspond to anything except "none" to "max."
 uint8_t  s,   // Snowfall, similar "strength" value (0-255).
 uint8_t  l,   // Lightning, ditto.
 uint8_t  w) { // Wind speed as a "strength" value (0-255) that also doesn't
               // correspond to anything real; this is the number of fixed-
               // point units that the clouds will move per frame. There are
               // 65536 units around the 'sky,' so a value of 255 will take
               // about 257 frames to make a full revolution of the LEDs,
               // which at 50 FPS would be a little over 5 seconds.
 **************************/

  // weathercode = 46; // hardcode weather animation test

  switch (weathercode) {
    case 0: // tornado!
      Serial.println("tornado");
      // lotsa cloud, no rain, and wind!
      animConfig(timeofday, 100, 0, 0, 0, 255);
      break;
    case 1: // tropical storm
      Serial.println("tropical storm");
      // no cloud, a lot of rain, no snow, no thunder and lotsa wind!
      animConfig(timeofday, 0, 255, 0, 0, 255);
      break;
    case 2: // hurricane
      Serial.println("hurricane");
      // some cloud, some rain, no snow, no thunder and lotsa wind!
      animConfig(timeofday, 50, 100, 0, 0, 255);
      break;

    case 3: // severe thunder
      Serial.println("severe thunder");
      // some cloud, no rain, no snow, mega lightning, some wind!
      animConfig(timeofday, 100, 0, 0, 255, 20);
      break;

    case 4: // thunder
      Serial.println("thunder");
      // some cloud, no rain, no snow, some lightning, some wind!
      animConfig(timeofday, 100, 0, 0, 100, 50);
      break;

    case 5: // mixed rain + snow
    case 6: // mixed rain and sleet
    case 7: // mixed snow and sleet
    case 18: // sleet
    case 35: // mixed rain/hail
      Serial.println("Rain/Snow/Sleet");
      // some cloud, some rain, some snow, no lightning, no wind!
      animConfig(timeofday, 10, 100, 100, 0, 0);
      break;

    case 8: // freezing drizzle
    case 9: // drizzle
      Serial.println("Drizzle");
      // some cloud, a little rain, no snow, no lightning, no wind!
      animConfig(timeofday, 30, 70, 0, 0, 0);
      break;

    case 10: // freezing rain
    case 11: // showers
    case 12: // showers
      Serial.println("Rain/Showers");
      // some cloud, lotsa rain, no snow, no lightning, no wind!
      animConfig(timeofday, 30, 250, 0, 0, 0);
      break;

    case 13: // snow flurries
    case 14: // light snow showers
      Serial.println("flurries");
      // some cloud, no rain, some snow, no lightning, no wind!
      animConfig(timeofday, 30, 0, 100, 0, 0);
      break;
      
    case 15: // blowing snow
      Serial.println("blowing snow");
      // some cloud, no rain, snow, no lightning, lotsa wind!
      animConfig(timeofday, 30, 0, 150, 0, 200);
      break;

    case 16: // snow
    case 17: // hail
    case 42: // scattered snow showers
      Serial.println("snow");
      // some cloud, no rain, snow, no lightning, no wind!
      animConfig(timeofday, 30, 0, 150, 0, 0);
      break;
      
    case 41: // heavy snow
    case 43: // heavy snow
      Serial.println("heavy snow");
      // some cloud, no rain, lotsa snow, no lightning, no wind!
      animConfig(timeofday, 30, 0, 255, 0, 0);
      break;

    case 31: // clear (night)
    case 32: // sunny
    case 33: // fair (night)
    case 34: // fair (day)
    case 25: // hot
    case 36: // cold
      Serial.println("Clear/fair");
      // no cloud, no rain, no snow, no lightning, no wind!
      animConfig(timeofday, 0, 0, 0, 0, 0);
      break;

    case 23: // blustery
    case 24: // windy
      Serial.println("Windy");
      // no cloud, no rain, no snow, no lightning, lots wind
      animConfig(timeofday, 0, 0, 0, 0, 200);
      break;

    case 26: // cloudy
    case 19: // dust
      Serial.println("Cloudy");
      // lotsa cloud, nothing else
      animConfig(timeofday, 255, 0, 0, 0, 0);
      break;

    case 27: // mostly cloudy
    case 28: // mostly cloudy
    case 20: // foggy
    case 22: // smoky
      Serial.println("mostly Cloudy");
      // lotsa cloud, nothing else
      animConfig(timeofday, 150, 0, 0, 0, 0);
      break;

    case 29: // partly cloudy
    case 30: // partly cloudy
    case 44: // partly cloudy
    case 21: // haze
      Serial.println("Partly Cloudy");
      // lotsa cloud, nothing else
      animConfig(timeofday, 150, 0, 0, 0, 0);
      break;

    case 37: // isolated thunderstorms
    case 47: // isolated thundershowers
      Serial.println("isolated thunderstorms");
      // some cloud, some rain, no snow, some lite, no wind
      animConfig(timeofday, 30, 150, 0, 30, 0);
      break;

    case 38: // scattered thunderstorms
    case 39: // scattered thundershowers
      Serial.println("scattered thundershowers");
      // some cloud, some rain, no snow, some lite, no wind
      animConfig(timeofday, 20, 150, 0, 60, 0);
      break;

    case 45: // thundershowers
      Serial.println("thundershowers");
      // some cloud, rain, no snow, lite, no wind
      animConfig(timeofday, 20, 250, 0, 100, 0);
      break;

    case 40: // scattered showers
      Serial.println("scattered showers");
      // some cloud, some rain, no snow, no lite, no wind
      animConfig(timeofday, 30, 50, 0, 0, 0);
      break;

    case 46: // snow showers
      Serial.println("snow showers");
      // some cloud, some rain, some snow, no lite, no wind
      animConfig(timeofday, 30, 100, 100, 0, 0);
      break;
      
    default:
      break;
  }
/*

25  cold
36  hot
3200  not available
*/
}
// Weather animation is rendered procedurally based on a few parameters
// (time of day, cloud cover, rainfall, etc.). Most of the inputs are NOT
// real-world units...see comments for explanation of what's needed.

// NeoPixel stuff --------------------------------------------------------

#include <Adafruit_NeoPixel.h>

#define NEOPIXEL_PIN 14 // NeoPixels are connected to this pin
#define NUM_LEDS    16 // Number of NeoPixels
#define FPS         50 // Animation frame rate (frames per second)

Adafruit_NeoPixel leds(NUM_LEDS, NEOPIXEL_PIN, NEO_GRB + NEO_KHZ800);

// Animation control stuff -------------------------------------------------

uint8_t renderBuf[NUM_LEDS][3], // Each frame of animation is assembled here
        alphaBuf[NUM_LEDS],     // Alpha mask for compositing each layer
        rainBuf[NUM_LEDS],      // Extra mask just for raindrop brightness
        rainCounter  = 1,       // Drop-to-drop countdown, in frames
        rainInterval = 0,       // Drop-to-drop interval, frames (0=no rain)
        windSpeed    = 0,       // Per-frame cloud motion (see comments)
        cloudCover   = 0;       // Percent cloud cover

uint16_t sunCenter   = 0,       // Position of 'sun' in 16-bit sky
         sunRadius   = 8192,    // Size of sun (same units)
         cloudOffset = 0,       // Position of cloud bitmap 'seam'
         timeOfDay   = 32768;   // Fixed-point day/night value (see notes)

uint8_t lightningBrightness = 0;
uint8_t lightningIntensity  = 0;
uint8_t snowIntensity       = 0;

uint32_t cloudBits   = 0;       // Bitmask of clouds
#if NUM_LEDS < 32
 #define NUM_CLOUD_BITS NUM_LEDS
#else
 #define NUM_CLOUD_BITS 32
#endif

#define N_STARS (3 + (NUM_LEDS / 7))
struct star {
  uint8_t pos;
  uint8_t brightness;
} star[N_STARS];

// Flake will "move," then "stop" when it hits the "ground," then fade.
// Kinda like raindrops, but moving first.
#define MAX_FLAKES (3 + (NUM_LEDS / 7))
struct flake {
  uint16_t pos;
  int16_t  speed;
  uint8_t  brightness;
  uint8_t  time;
} flake[MAX_FLAKES];
uint8_t nFlakes = 0;

void randomFlake(void) {
  flake[nFlakes].pos        = random(65536);
  uint8_t w = windSpeed;
  if(w < 20) w = 20;
  do {
    flake[nFlakes].speed    = random(w / -4, (w * 5) / 4);
  } while(!flake[nFlakes].speed);
  flake[nFlakes].brightness = random(128, 255);
  flake[nFlakes].time       = random(FPS, FPS * 2); // # frames until snowflake "touches ground"
  nFlakes++;
}


uint16_t lightningCounter = 0;

extern const uint8_t gamma8[]; // Big table at end of this code

// One-time initialization - clears NeoPixels & sets up some variables -----

void animSetup(void) {

  leds.begin();
  leds.setBrightness(200);
  leds.clear(); // All NeoPixels off ASAP
  leds.show();

  randomSeed(analogRead(A0));

  memset(rainBuf, 0, sizeof(rainBuf)); // Clear rain buffer
  for(uint8_t i=0; i<N_STARS; i++) {   // Initialize star positions
    star[i].pos = random(NUM_LEDS);    // TO DO: make stars not overlap
    star[i].brightness = random(15, 45);
  }
  memset(flake, 0, sizeof(flake));     // Clear snowflakes
}

// Utility functions -------------------------------------------------------

// Set up animation based on some weather attributes like cloud cover, etc.
void animConfig(
 uint16_t t,   // Time of day in fixed-point 16-bit units, where 0=midnight,
               // 32768=noon, 65536=midnight. THIS DOES NOT CORRESPOND TO
               // ANY SORT OF REAL-WORLD UNITS LIKE SECONDS, nor does it
               // handle things like seasons or Daylight Saving Time, it's
               // just an "ish" approximation to give the sky animation some
               // vague context. The time of day should be polled from the
               // same source that's providing the weather data, DO NOT use
               // millis() or micros() to attempt to follow real time, as
               // the NeoPixel library is known to mangle these interrupt-
               // based functions. TIME OF DAY IS "ISH!"
 uint8_t  c,   // Cloud cover, as a percentage (0-100).
 uint8_t  r,   // Rainfall as a "strength" value (0-255) that doesn't really
               // correspond to anything except "none" to "max."
 uint8_t  s,   // Snowfall, similar "strength" value (0-255).
 uint8_t  l,   // Lightning, ditto.
 uint8_t  w) { // Wind speed as a "strength" value (0-255) that also doesn't
               // correspond to anything real; this is the number of fixed-
               // point units that the clouds will move per frame. There are
               // 65536 units around the 'sky,' so a value of 255 will take
               // about 257 frames to make a full revolution of the LEDs,
               // which at 50 FPS would be a little over 5 seconds.

  timeOfDay          = t;
  cloudCover         = (c > 100) ? 100 : c;
  rainInterval       = r ? map(r, 1, 255, 64, 1) : 0;
  windSpeed          = w;
  lightningIntensity = l;
  snowIntensity      = s;

  // Randomize cloud bitmask based on cloud cover percentage:
  cloudBits = 0;
  for(uint8_t i=0; i<NUM_CLOUD_BITS; i++) {
    cloudBits <<= 1;
    if(cloudCover > random(150)) cloudBits |= 1;
  }

  nFlakes = 0;
  memset(flake, 0, sizeof(flake));
  if(s) {
    uint8_t n = 3 + (snowIntensity * (MAX_FLAKES - 2)) / 256;
    while(nFlakes < n) {
      randomFlake();
    }
  }
}

// Interpolate between two 'packed' (32-bit) RGB colors.
// Second argument is weighting (0-255) of second color.
uint32_t colorInterp(uint32_t color1, uint32_t color2, uint8_t w) {
  uint8_t  r1 = (color1 >> 16) & 0xFF,
           g1 = (color1 >>  8) & 0xFF,
           b1 =  color1        & 0xFF,
           r2 = (color2 >> 16) & 0xFF,
           g2 = (color2 >>  8) & 0xFF,
           b2 =  color2        & 0xFF;
  uint16_t w2 = (uint16_t)w + 1, // 1-256
           w1 = 257 - w2;        // 1-256
  r1 = (r1 * w1 + r2 * w2) >> 8;
  g1 = (g1 * w1 + g2 * w2) >> 8;
  b1 = (b1 * w1 + b2 * w2) >> 8;
  return (((uint32_t)r1 << 16) | ((uint32_t)g1 << 8) | b1);
}

// Using alphaBuf as a mask, fill an RGB color atop renderBuf
void overlay(uint8_t r, uint8_t g, uint8_t b) {
  uint16_t i, a1, a2;
  for(i=0; i<NUM_LEDS; i++) {
    a1 = alphaBuf[i] + 1; // 1-256
    a2 = 257 - a1;        // 1-256
    renderBuf[i][0] = (r * a1 + renderBuf[i][0] * a2) >> 8;
    renderBuf[i][1] = (g * a1 + renderBuf[i][1] * a2) >> 8;
    renderBuf[i][2] = (b * a1 + renderBuf[i][2] * a2) >> 8;
  }
}

// Same as above, for packed 32-bit RGB value
void overlay(uint32_t color) {
  overlay((color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF);
}

void waitForFrame(void) {
  static uint32_t timeOfLastFrame = 0L;
  uint32_t t;
  while(((t = millis()) - timeOfLastFrame) < (1000 / FPS)) yield();
  timeOfLastFrame = t;
}

#define NIGHTSKYCLEAR   0x0a1923
#define DAYSKYCLEAR     0x28648c
#define NIGHTSKYCLOUDBG 0x2c2425
#define DAYSKYCLOUDBG   0x5e6065
#define NIGHTSKYCLOUDFG 0x515159
#define DAYSKYCLOUDFG   0xc2c2c2
#define NIGHTSNOW       0xa6b1c0
#define DAYSNOW         0xffffff
#define SUNCLEAR        0xffff60
#define SUNCLOUDY       0x7a7a61

void renderFrame(void) {
  // Display *prior* frame of data at start of function --
  // this ensures uniform updates, as render time may vary.
  leds.show();

  // Then begin processing next frame...

  int i;

  // tod: 0-64K, where 0 = midnight, 32K = noon, 64K = midnight
  // this is an artistic approximation and doesn't take seasons,
  // etc into consideration. if you need that, can fudge it into
  // tod rather than here.
  // Sunrise and sunset are two 90-minute periods centered around
  // 6am and 6pm (again, not factoring in seasons, daylight savings
  // time, etc.). Sky and other effects will interpolate between
  // day and night states for these two things.
  long y = timeOfDay;
  uint8_t dayWeight;
  if(y > 32767) y = 65536 - y;
  y = y * 256L / 4096 - 896;
  dayWeight = (y > 255) ? 255 : ((y < 0) ? 0 : y); // 0-255 night/day

  // Determine sky and cloud color based on % of cloud cover
  uint32_t
    clearSkyColor  = colorInterp(NIGHTSKYCLEAR  , DAYSKYCLEAR  , dayWeight),
    cloudySkyColor = colorInterp(NIGHTSKYCLOUDBG, DAYSKYCLOUDBG, dayWeight),
    cloudColor     = colorInterp(NIGHTSKYCLOUDFG, DAYSKYCLOUDFG, dayWeight),
    skyColor       = colorInterp(clearSkyColor, cloudySkyColor, map(cloudCover, 30, 70, 0, 255));

  for(i=0; i<NUM_LEDS; i++) {
    renderBuf[i][0] = skyColor >> 16;
    renderBuf[i][1] = skyColor >>  8;
    renderBuf[i][2] = skyColor;
  }

  // Stars
  if(dayWeight < 128) { // Dark? Or getting there?
    uint16_t nightWeight = 257 - dayWeight;
    memset(alphaBuf, 0, sizeof(alphaBuf));
    for(i=0; i<N_STARS; i++) {
      alphaBuf[star[i].pos] = (nightWeight * random(star[i].brightness/2, star[i].brightness)) >> 8;
    }
    overlay(255, 255, 255);
  } else {
    sunRadius = map(dayWeight, 128, 255, 1, 8192);
    uint16_t x;
    int16_t px1, px2, sx1, sx2;

    // Clear alpha buffer, gonna render 'sun' there...
    memset(alphaBuf, 0, sizeof(alphaBuf));

    uint32_t
      sunColor = colorInterp(SUNCLEAR, SUNCLOUDY, map(cloudCover, 30, 70, 0, 255));

    // Figure overlap between sun and each pixel...
    //  uint16_t left, right, dist1, dist2;
    for(i=0; i<NUM_LEDS; i++) {
      // Pixel coord in fixed-point space
      x = (i * 65536L) / NUM_LEDS;
      int16_t foo = sunCenter - x; // sun center in pixel space
      sx1 = foo - sunRadius;
      sx2 = foo + sunRadius;
      px1 = 0;
      px2 = 65536 / NUM_LEDS;
      if((sx1 >= px2) || (sx2 < 0)) continue; // No overlap
      else if((sx1 <= 0) && (sx2 >= px2)) alphaBuf[i] = 255; // Fully encompassed
      else {
        if(sx1 > 0) {
          if(sx2 < px2) {
            alphaBuf[i] = 255L * (sx2 - sx1) / (px2 - px1);
          } else {
            alphaBuf[i] = 255L * (px2 - sx1) / (px2 - px1);
          }
        } else {
          alphaBuf[i] = 255L * (sx2 - px1) / (px2 - px1);
        }
      }
    }
    
    overlay(sunColor); // Composite sun atop sky
  }

  if(cloudBits) {
    // Clear alpha buffer, gonna render clouds there...
    memset(alphaBuf, 0, sizeof(alphaBuf));
    uint16_t x, minor;
    uint8_t  major, l, r;
    for(i=0; i<NUM_LEDS; i++) {
      x           = (i * 65536L) / NUM_LEDS - cloudOffset;  // Pixel coord in fixed-point space (0-65535) relative to clouds
      x           = (x * (NUM_CLOUD_BITS * 256UL)) / 65536; // Scale to cloud pixel space
      major       = x >> 8;                                 // Left bit number (0 to NUM_CLOUD_BITS-1)
      minor       = x & 0xFF;                               // Weight (0-255) of next bit over
      l           = (cloudBits & (1 << major)) ? 220 : 0;   // Left bit opacity
      if(++major >= NUM_CLOUD_BITS) major = 0;              // Next bit over
      r           = (cloudBits & (1 << major)) ? 220 : 0;   // Right bit opacity
      alphaBuf[i] = ((l * (257 - minor)) + (r * (minor + 1))) >> 8; // Blend
    }

    uint32_t c = colorInterp(NIGHTSKYCLOUDFG, DAYSKYCLOUDFG, dayWeight);
    overlay(c); // Composite clouds atop sky
  }

  if(rainInterval) {
    memset(alphaBuf, 0, sizeof(alphaBuf));
    for(i=0; i<NUM_LEDS; i++) {
      rainBuf[i] = (rainBuf[i] * (uint16_t)245) >> 8;
    }
    // Periodically, randomly, add a drop to rainBuf[]
    if(!--rainCounter) {
      i = random(NUM_LEDS); // Which spot?
      int16_t foo = rainBuf[i] + 255;
      if(foo > 255) foo = 255;
      rainBuf[i] = foo;
      uint8_t r4 = rainInterval / 4;
      if(r4 < 1) r4 = 1;
      rainCounter = random(r4, rainInterval);
    }
    memcpy(alphaBuf, rainBuf, sizeof(rainBuf));
    overlay(130, 130, 150);
  }

  if(nFlakes) {
    uint16_t x, minor;
    uint8_t  major, l, r;
    memset(alphaBuf, 0, sizeof(alphaBuf));
    for(i=0; i<nFlakes; i++) {
      // Render flake here
      x     = (flake[i].pos * (NUM_LEDS * 256UL)) / 65536;
      major = x >> 8;   // Left pixel number (0 to NUM_LEDS-1)
      minor = x & 0xFF; // Weight (0-255) of next pixel over
      alphaBuf[major] = (alphaBuf[major] * (1   + minor)) + (flake[i].brightness * (257 - minor)) >> 8;
      if(++major >= NUM_LEDS) major = 0;
      alphaBuf[major] = (alphaBuf[major] * (257 - minor)) + (flake[i].brightness * (1   + minor)) >> 8;
      flake[i].pos += flake[i].speed;
      if(flake[i].time) {
        flake[i].time--;
      } else {
        flake[i].brightness = (flake[i].brightness * 253) >> 8;
        if(!flake[i].brightness) {
          memcpy(&flake[i], &flake[nFlakes-1], sizeof(struct flake)); // Move last flake to this pos.
          i--;           // Flake moved, so don't increment
          nFlakes--;     // Decrement number of flakes
          randomFlake(); // And add a new one in last pos.
        }
      }
    }
    overlay(255, 255, 255);
  }

  if(lightningBrightness) {
    for(i=0; i<NUM_LEDS; i++) alphaBuf[i] = lightningBrightness;
    overlay(255, 255, 255);
    lightningBrightness = (lightningBrightness * 220) >> 8;
  }
  if(lightningIntensity) {
    if(!random(50 + (255 - lightningIntensity) * 3)) {
      i = random(128, 256);
      if(i > lightningBrightness) lightningBrightness = i;
    }
  }

  sunCenter   += 65536 / 30 / FPS; // 30 sec for 1 revolution
  cloudOffset -= windSpeed;

//  timeOfDay += 65536/60/FPS; // 1 min for day/night cycle

  // Convert RGB renderbuf to gamma-corrected LED-native color order:
  for(uint16_t i=0; i<NUM_LEDS; i++) {
    leds.setPixelColor(i,
      pgm_read_byte(&gamma8[renderBuf[i][0]]),
      pgm_read_byte(&gamma8[renderBuf[i][1]]),
      pgm_read_byte(&gamma8[renderBuf[i][2]]));
  }
  // DON'T call leds.show() here! That's done at start of function.
}

// Gamma correction improves appearance of midrange colors
const uint8_t gamma8[] PROGMEM = {
    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,
    0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  1,  1,  1,  1,
    1,  1,  1,  1,  1,  1,  1,  1,  1,  2,  2,  2,  2,  2,  2,  2,
    2,  3,  3,  3,  3,  3,  3,  3,  4,  4,  4,  4,  4,  5,  5,  5,
    5,  6,  6,  6,  6,  7,  7,  7,  7,  8,  8,  8,  9,  9,  9, 10,
   10, 10, 11, 11, 11, 12, 12, 13, 13, 13, 14, 14, 15, 15, 16, 16,
   17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22, 23, 24, 24, 25,
   25, 26, 27, 27, 28, 29, 29, 30, 31, 32, 32, 33, 34, 35, 35, 36,
   37, 38, 39, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 50,
   51, 52, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 66, 67, 68,
   69, 70, 72, 73, 74, 75, 77, 78, 79, 81, 82, 83, 85, 86, 87, 89,
   90, 92, 93, 95, 96, 98, 99,101,102,104,105,107,109,110,112,114,
  115,117,119,120,122,124,126,127,129,131,133,135,137,138,140,142,
  144,146,148,150,152,154,156,158,160,162,164,167,169,171,173,175,
  177,180,182,184,186,189,191,193,196,198,200,203,205,208,210,213,
  215,218,220,223,225,228,231,233,236,239,241,244,247,249,252,255 };


Enter WiFi Creds & City

In the weatherPixels.ino file, you'll need to input your WiFi credentials and your city. Reference the screenshot above and highlighted text to see which lines to modify.

Note that for the city, you'll need to keep the %2C and %20 text between the city and state/region. %2C encodes into a , an the %20 encodes into a space, when we do the API query

Number of Pixels

In the animation.cpp, you can enter the number of pixels you'd like to use - in our project we need 24.

3D Printing

Materials

We suggest using PLA material but your free to use ABS, PET or exotic composites like wood, metals and others. The parts are listed in the tablet below.

Slice Settings

Depending on your 3D printers hardware, you'll need to use your prefered slice settings. The parts are oriented to print "as-is" and doesn't require any support materials (very minimal overhangs).

These are the slice settings we used on our Printrbot Play, sliced using Simplify3D

  • 220C Extruder (on a non-heated bed)
  • 20% Infill
  • 2 shells/parameters 
  • 4 top and bottom layers
  • 1.0 Extrusion multiplier 
  • 0.48 Extrusion width

Customize Design

The enclosure parts are available to modify and download. Click below to download the source. Size it to fit the bed of your 3D printer.

Download STLs

3D print the files "as-is" if they can fit on the bed of your 3D printer (case is 80mm x 80mm x 20mm)

case.stl

1x enclosure

PLA

top-dual.stl

1x twist top for dual extruders

PLA

top.stl

1x twist top for regular single extruders

PLA

But I Don't Have a 3D Printer!

That's totally OK, you can still buy the parts and have them shipped to you! You can use a service like 3D Hubs to 3D print all of the parts for you. Just download all of the STL files from our Thingiverse page and upload them to their website.

Using 3D Hubs App on Thingiverse

The easiest way to do this is to use the "Print" button on the Thingiverse page. Then, click on the Launch App button to automatically load the STL files into 3D Hubs. From there, you can select colors, materials and enter your shipping address. A total price will let you know how much the parts will cost. A list of local hubs will appear and you can select which ever looks good to you. Hubs have different prices, ratings, reviews, and sample photos so you can narrow down your choice.

Assembly

Prep Wires

Let's start by making a set of wires for connecting our components.

We'll need 5 pieces of wire about 8cm in length. I suggest using 26AWG silicone coated stranded wires.

Remove about 3mm of insulation of from the tips of each wire. Apply solder to the tips to tin them - this prevents wires from fraying.

Tap Mounting Holes

We'll need to create threads in the two mounting holes of teh Adafruit Feather HUZZAH board. You can use a 4-40 tap drill or a 4-40 machine screw. 

Prep Slide Switch

Trim leads short to about half their size. Remove one of the three leads from the slide - either the far left or right. Apply solder to the two remaining leads to tin them.

Wire Slide Switch

Connect one wire to each lead by heating up the tip of the lead and placing the tip of the wire.

Slide Switch

You can optionally apply some pieces of heat shrink tubing to insulate the exposed areas. The slide switch is now ready!

Wire NeoPixel Ring

Apply solder to data in, ground and pwr +5V pins on the NeoPixel ring. Then, connect three wires to these pins on the NeoPixel ring.

Wire Slide Switch to NeoPixel Ring

Connect one of the wire from slide switch to GND pin on NeoPixel.

Wire NeoPixel to Adafruit Feather

Connect the three wires from NeoPixel to Adafruit Feather HUZZAH.

  • Data in from NeoPixel to Pin #14 Adafruit Feather Huzzah
  • Ground from NeoPixel to gnd on Adafruit Feather Huzzah
  • 5V PWR from NeoPixel to USB on Adafruit Feather Huzzah
  • Wire from Slide switch to EN on Adafruit Feather Huzzah

Test Circuit

The arduino code should already be uploaded to the Adafruit Feather Huzzah. Connect a microUSB cable to your computer (or a 5V power supply) and to the HUZZAH board. NeoPixels should power on!

Install Slide Switch

Insert Slide Switch into case. Push it through the hole.

Install HUZZAH

Lay the board over the standoffs and align up with screw threads with the moutning holes. Fasten screws until board is secured in place.

Install NeoPixel Ring

Add pieces of mounting tack to case, press NeoPixel ring into case. Secure in place. Be cautious not to kink any wires!

Install Cover

Install the cover by twisting it onto the case like a bottle cap. It has threads, like a twisty top!

Final Build

Yahoo! Congrats on building your very own Feather Weather IoT Lamp. Now you can see what the weather is like... In lovely NeoPixel RGB color!

This guide was first published on Jun 16, 2016. It was last updated on Jun 16, 2016.