Hardware components | ||||||
| × | 1 | ||||
| × | 1 | ||||
| × | 100 | ||||
| × | 100 | ||||
| × | 1 | ||||
| × | 1 |
Watching the weather nationwide at a glance, see temperatures rise and fall, watch the rain fall, all from a wireless, wall mounted installation.
Lite-Brite Weather Map
Continuing the post-hackathon weather gadget making, fueled by the Spark Core (Think Arduino with WiFi) I got from Zack at TechCrunch and liking shiny lights, I started on the weather map. A matrix of addressable LEDs that were in the general form of the United States. The idea was that the spark could talk to the Weather Underground API, pull down temperature data for each of the lat/long locations assigned to each light, and the light would change color to reflect the temperature.
The built in WiFi made talking to the API pretty straight forward, the challenge was more about memory and the API limit, making 100 requests per map refresh was not something my free api account could sustain. Luckily the API team helped me out with my limit to keep the project going.
The big break came when Adam Williams got involved, he was able to program node.js to read from the Wunderground.com API and push the data to the spark core and the light matrix to show temperature, precipitation, and more from the weather data available.
Bonus story: while working on the map all the lights switched purple (not assigned to anything), cluing the team into an API problem. Needless to say it was almost immediately fixed and the map came back to life. My little side project accidentally became a status board.
Hardware3>
Warning: embedding parts within the project story has been deprecated. To edit, remove or add more parts, go to the "Hardware" tab. To remove this list from the story, click on it to trigger the context menu, then click the trash can button (this won't delete it from the "Hardware" tab). | ||||||
|
||||||
|
||||||
|
||||||
|
||||||
|
||||||
|
Software
The color/number comes from a node.js script that queries an array of latitude/longitudes from the Weather Underground API.
Each lat/long is assigned to an LED number.
The Spark listens for a number and RGB value and sends it out to the LED string.
#include "application.h"
//#include "spark_disable_wlan.h" (for faster local debugging only)
#include "neopixel/neopixel.h"
// IMPORTANT: Set pixel COUNT, PIN and TYPE
#define PIXEL_PIN D2
#define PIXEL_COUNT 100
#define PIXEL_TYPE WS2812B
Adafruit_NeoPixel strip = Adafruit_NeoPixel(PIXEL_COUNT, PIXEL_PIN, PIXEL_TYPE);
Adafruit_NeoPixel rain = Adafruit_NeoPixel(PIXEL_COUNT, PIXEL_PIN, PIXEL_TYPE);
Adafruit_NeoPixel heat = Adafruit_NeoPixel(PIXEL_COUNT, PIXEL_PIN, PIXEL_TYPE);
void setup()
{
strip.begin();
Spark.function("rainbow", call_rainbow);
Spark.function("heatmap", call_heatmap);
Spark.function("rainmap", call_rainmap);
strip.show(); // Initialize all pixels to 'off'
}
void loop()
{
//loop_rainbow(1);
//loop_heatmap(3000);
//loop_rainmap(3000);
}
int call_rainmap(String number) {
rain.show();
int delim_index = 0;
int start_index = 0;
int second_index = 0;
//String loc = number.substring(0, delim_index);
//String val = number.substring(delim_index+1, number.length());
//uint16_t[] vals = new uint16_t[100];
for(int i=0; i<5; i++) {
delim_index = number.indexOf(':', start_index);
second_index = number.indexOf(';', delim_index);
String loc = number.substring(start_index, delim_index);
String val = number.substring(delim_index+1, second_index);
start_index = second_index+1;
int pos = loc.toInt();
int col = val.toInt();
//uint16_t[i] = col;
uint16_t num16 = pos;
uint16_t col16 = col;
rain.setPixelColor(num16, Wheel(col16 & 255));
}
rain.setBrightness(25);
//rain.show();
//rainbow(5);
delay(20);
return 0;
}
int call_heatmap(String number) {
heat.show();
int delim_index = 0;
int start_index = 0;
int second_index = 0;
//String loc = number.substring(0, delim_index);
//String val = number.substring(delim_index+1, number.length());
//uint16_t[] vals = new uint16_t[100];
for(int i=0; i<5; i++) {
delim_index = number.indexOf(':', start_index);
second_index = number.indexOf(';', delim_index);
String loc = number.substring(start_index, delim_index);
String val = number.substring(delim_index+1, second_index);
start_index = second_index+1;
int pos = loc.toInt();
int temp = val.toInt();
//uint32_t col = 0;
uint16_t num16 = pos;
if(temp < 30) {
heat.setPixelColor(num16, 0, 164, 255);
} else if (temp < 40) {
heat.setPixelColor(num16, 255, 0, 255);
} else if (temp < 45) {
heat.setPixelColor(num16, 255, 127, 0);
} else if (temp < 50) {
heat.setPixelColor(num16, 255, 206, 0);
} else if (temp < 55) {
heat.setPixelColor(num16, 255, 254, 0);
} else if (temp < 60) {
heat.setPixelColor(num16, 230, 255, 1);
} else if (temp < 65) {
heat.setPixelColor(num16, 203, 255, 0);
} else if (temp < 70) {
heat.setPixelColor(num16, 174, 255, 0);
} else if (temp < 75) {
heat.setPixelColor(num16, 153, 255, 0);
} else if (temp < 80) {
heat.setPixelColor(num16, 127, 255, 0);
} else {
heat.setPixelColor(num16, 7, 255, 0);
}
//uint16_t[i] = col;
//uint16_t num16 = pos;
//uint16_t col16 = col;
//heat.setPixelColor(num16, Wheel(col16 & 255));
//heat.setPixelColor(num16, col);
}
heat.setBrightness(25);
heat.show();
//rainbow(5);
delay(20);
return 0;
}
int call_rainbow(String number) {
rainbow(5);
}
void loop_rainbow(uint8_t wait) {
uint16_t i, j, k;
for(int k=0; k<wait; k++) {
for(j=0; j<256; j++) {
for(i=0; i<strip.numPixels(); i++) {
strip.setPixelColor(i, Wheel((i+j) & 255));
}
strip.show();
delay(20);
}
//delay(100);
}
}
void loop_rainmap(int wait) {
rain.show();
delay(wait);
}
void loop_heatmap(int wait) {
heat.show();
delay(wait);
}
void rainbow(uint8_t wait) {
uint16_t i, j;
for(j=0; j<256; j++) {
for(i=0; i<strip.numPixels(); i++) {
strip.setPixelColor(i, Wheel((i+j) & 255));
}
strip.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) {
if(WheelPos < 85) {
return strip.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
} else if(WheelPos < 170) {
WheelPos -= 85;
return strip.Color(255 - WheelPos * 3, 0, WheelPos * 3);
} else {
WheelPos -= 170;
return strip.Color(0, WheelPos * 3, 255 - WheelPos * 3);
}
}
file_7506.js
JavaScript/*jslint node: true */
"use strict";
var _ = require('underscore');
var request = require('request');
var spark = require('spark');
var async = require('async');
var map = [];
// Login as usual
var promise = spark.login({ username: 'xxxxxxxxxx', password: 'xxxxxxx' });
var fetch = function(loc, cb) {
request(loc.url, function(error, response, body) {
if (!error && response.statusCode == 200) {
// console.log(body) // Print the google web page.
var data = JSON.parse(body);
// console.log(data.current_observation.station.name);
// console.log(data.current_observation.temperature);
if(data && data.current_observation) {
var temp = data.current_observation.temperature;
var precip_today = data.current_observation.precip_today;
var color;
/*
if(precip_today) {
color = 155;
} else {
color = 65;
}
*/
/*
if(temp >= 60) {
color = '255';
}
if(temp < 60) {
color = '155';
}
*/
color = Math.floor(temp);
// var command = index + ':' + color;
// map[index].command = command;
cb(null, color);
// console.log(command);
// console.log(color);
} else {
cb(null, '001');
}
} else {
cb("error 1");
}
});
};
var spark_device = null;
var batch_send = function(command, cb) {
console.log('sending command: ' + command);
spark_device.callFunction('heatmap', command, function(err, data) {
if (err) {
console.log('An error occurred:', err);
cb(err);
} else {
console.log('Function called succesfully:', data);
cb(null);
// rec_call(device, index+1);
}
});
}
promise.then(
function(token){
// If login is successful we get and accessToken,
// we'll use that to call Spark API ListDevices
var devicesPr = spark.listDevices();
devicesPr.then(
// We get an array with devices back and we list them
function(devices){
console.log('API call List Devices completed on promise resolve: ', devices);
var device = devices[0];
spark_device = device;
async.map(map, fetch, function(err, results) {
if(err) {
console.log('async map error occurred:', err);
} else {
console.log('results found');
console.log(results);
var command = "";
var count = 4;
var it = 0;
var command_set = [];
_.each(results, function(el, index) {
command += index + ':' + el + ';';
if(it < count) {
it++;
} else {
command_set.push(command);
command = "";
it = 0;
}
});
// command_set.push(command);
console.log(command_set);
console.log(command);
async.eachSeries(command_set, batch_send, function(err) {
if(err) {
console.log("error occurred: " + err);
} else {
console.log("no errors!");
}
});
}
//rec_call(device, 0);
});
},
function(err) {
console.log('API call List Devices completed on promise fail: ', err);
}
);
},
function(err) {
console.log('API call completed on promise fail: ', err);
}
);
function rec_call(device, index) {
if(index > 99) return;
var command = map[index].command;
console.log("index: " + index);
device.callFunction('rainbow', command, function(err, data) {
if (err) {
console.log('An error occurred:', err);
} else {
console.log('Function called succesfully:', data);
rec_call(device, index+1);
}
});
}
function blah() {
var locations = [
{'lat':'34', 'lon': '-111'},
{'lat':'37', 'lon': '-112'},
{'lat':'41', 'lon': '-123'},
{'lat':'45', 'lon': '-122'},
{'lat':'47', 'lon': '-122'},
{'lat':'45', 'lon': '-119'},
{'lat':'42', 'lon': '-119'},
{'lat':'40', 'lon': '-118'},
{'lat':'38', 'lon': '-118'},
{'lat':'33', 'lon': '-116'},
{'lat':'35', 'lon': '-115'},
{'lat':'33', 'lon': '-114'},
{'lat':'34', 'lon': '-113'},
{'lat':'36', 'lon': '-113'},
{'lat':'39', 'lon': '-114'},
{'lat':'41', 'lon': '-117'},
{'lat':'45', 'lon': '-116'},
{'lat':'47', 'lon': '-117'},
{'lat':'47', 'lon': '-114'},
{'lat':'46', 'lon': '-110'},
{'lat':'42', 'lon': '-112'},
{'lat':'40', 'lon': '-111'},
{'lat':'39', 'lon': '-111'},
{'lat':'35', 'lon': '-110'},
{'lat':'32', 'lon': '-110'},
{'lat':'31', 'lon': '-109'},
{'lat':'33', 'lon': '-108'},
{'lat':'36', 'lon': '-108'},
{'lat':'39', 'lon': '-108'},
{'lat':'41', 'lon': '-108'},
{'lat':'45', 'lon': '-107'},
{'lat':'46', 'lon': '-108'},
{'lat':'47', 'lon': '-104'},
{'lat':'46', 'lon': '-103'},
{'lat':'42', 'lon': '-104'},
{'lat':'40', 'lon': '-104'},
{'lat':'38', 'lon': '-105'},
{'lat':'34', 'lon': '-106'},
{'lat':'32', 'lon': '-106'},
{'lat':'30', 'lon': '-103'},
{'lat':'33', 'lon': '-102'},
{'lat':'35', 'lon': '-102'},
{'lat':'39', 'lon': '-101'},
{'lat':'40', 'lon': '-101'},
{'lat':'43', 'lon': '-100'},
{'lat':'47', 'lon': '-101'},
{'lat':'48', 'lon': '-98'},
{'lat':'45', 'lon': '-98'},
{'lat':'41', 'lon': '-98'},
{'lat':'40', 'lon': '-99'},
{'lat':'36', 'lon': '-99'},
{'lat':'34', 'lon': '-99'},
{'lat':'33', 'lon': '-97'},
{'lat':'30', 'lon': '-100'},
{'lat':'28', 'lon': '-98'},
{'lat':'29', 'lon': '-95'},
{'lat':'33', 'lon': '-96'},
{'lat':'35', 'lon': '-94'},
{'lat':'37', 'lon': '-94'},
{'lat':'40', 'lon': '-93 '},
{'lat':'43', 'lon': '-94'},
{'lat':'47', 'lon': '-94'},
{'lat':'46', 'lon': '-90'},
{'lat':'42', 'lon': '-91'},
{'lat':'38', 'lon': '-90'},
{'lat':'36', 'lon': '-91'},
{'lat':'33', 'lon': '-92'},
{'lat':'30', 'lon': '-93'},
{'lat':'29', 'lon': '-90'},
{'lat':'33', 'lon': '-88'},
{'lat':'34', 'lon': '-88'},
{'lat':'39', 'lon': '-87'},
{'lat':'40', 'lon': '-87'},
{'lat':'44', 'lon': '-86'},
{'lat':'42', 'lon': '-86'},
{'lat':'38', 'lon': '-86'},
{'lat':'35', 'lon': '-86'},
{'lat':'33', 'lon': '-86'},
{'lat':'30', 'lon': '-85'},
{'lat':'33', 'lon': '-85'},
{'lat':'35', 'lon': '-85'},
{'lat':'39', 'lon': '-85'},
{'lat':'42', 'lon': '-84'},
{'lat':'45', 'lon': '-83'},
{'lat':'39', 'lon': '-82'},
{'lat':'38', 'lon': '-82'},
{'lat':'33', 'lon': '-82'},
{'lat':'31', 'lon': '-81'},
{'lat':'28', 'lon': '-82'},
{'lat':'25', 'lon': '-80'},
{'lat':'29', 'lon': '-79'},
{'lat':'33', 'lon': '-80'},
{'lat':'43', 'lon': '-80'},
{'lat':'39', 'lon': '-80'},
{'lat':'43', 'lon': '-80 '},
{'lat':'45', 'lon': '-80'},
{'lat':'46', 'lon': '-80'},
{'lat':'46', 'lon': '-70'},
{'lat':'42', 'lon': '-71'},
{'lat':'33', 'lon': '-44'}
];
var high = -999;
var low = 999;
for(var i=0; i<locations.length; i++) {
console.log(i);
var point = locations[i]
map[i] = {}
map[i].loc = point;
map[i].url = 'http://api.wxug.com/api/xxxxxAPIKEYxxxx/conditions/v:2.0/lang:EN/units:english/q/' + point.lat + ',' + point.lon + '.json';
//console.log(map[i]);
/*
request(map[i].url, function(error, response, body) {
if (!error && response.statusCode == 200) {
// console.log(body) // Print the google web page.
var data = JSON.parse(body);
// console.log(data.current_observation.station.name);
// console.log(data.current_observation.temperature);
if(data && data.current_observation) {
var temp = data.current_observation.temperature;
if(temp > 70) {
high = temp;
}
if(temp < low) {
low = temp;
}
console.log("high: " + high);
console.log("low: " + low);
}
}
});
*/
}
}
blah();
Comments