The hoodie is finished!
A few weeks ago, I started dreaming up this wacky project. And now I have a fun flower-covered Spring Hoodie that lights up depending on the weather forecast.
What's in it?
The components in the hoodie include an Adafruit Flora, a CC3000 WiFi module, and a lithium battery, all concealed inside an inner pocket. Eighteen NeoPixel LEDs are hidden inside of fake flowers covering the hood. I used two Old Navy hoodies, turning one inside out and putting it inside the other as a lining that protects and conceals all the wiring.
I bought the flowers at a craft store and put LEDs inside of 18 of them, wiring the lights together and connecting them to the Flora. Then I sewed or glued the rest of my flowers all over the hood.
I added a clicky button stitched into the pocket to make it easy to turn on and off without needing to pull out the main components.
How does it work?
When I turn on the hoodie, the WiFi module tethers to my phone, and the Flora uses it to connect to a simple PHP web page pulling 3-hour forecast data for the predicted precipitation, temperature, and wind speed from the Open Weather Map API.
When the Flora is connecting over WiFi, the lapel flower glows green to show that it’s working. Once it connects to this web page and grabs the weather information, the Flora uses it to control the color, brightness, and rate of change of the LED flowers.
The color changes based on amount of predicted precipitation. The more rain that’s forecasted, the more LEDs will be blue.
Intensity is dictated by temperature. The warmer it is, the brighter the lights. One thing I like about this feature is that if it’s too cold out, the hoodie will protect me by not lighting up at all. (funny story: I forgot about this feature the day after coding it. It was a cold day and I could not for the life of me figure out why the lights weren’t lighting up! After my “doh” moment, I decided to add that green lapel indicator light.)
I wanted there to always be some slight pulsing or suggestion of movement in the lights, but the speed of this movement is dictated by the predicted wind speed. The faster the wind, the faster the lights will change or flicker.
The Code
The vast majority of the code was written by my Hunky Assistant. He's the best!
//first version of SpringHoodie code #include <Adafruit_NeoPixel.h> #include <Adafruit_CC3000.h> #include <ccspi.h> #include <SPI.h> // ========= CC3000 Parameters ========= // These are the interrupt and control pins #define ADAFRUIT_CC3000_IRQ 2 // MUST be an interrupt pin! // These can be any two pins #define ADAFRUIT_CC3000_VBAT 9 #define ADAFRUIT_CC3000_CS 10 // Use hardware SPI for the remaining pins // On an UNO, SCK = 13, MISO = 12, and MOSI = 11 Adafruit_CC3000 cc3000 = Adafruit_CC3000(ADAFRUIT_CC3000_CS, ADAFRUIT_CC3000_IRQ, ADAFRUIT_CC3000_VBAT, SPI_CLOCK_DIVIDER); // you can change this clock speed #define WLAN_SSID "NAME OF WIFI NETWORK" // cannot be longer than 32 characters! #define WLAN_PASS "WIFI PASSWORD" // Security can be WLAN_SEC_UNSEC, WLAN_SEC_WEP, WLAN_SEC_WPA or WLAN_SEC_WPA2 #define WLAN_SECURITY WLAN_SEC_WPA2 #define IDLE_TIMEOUT_MS 10000 // Amount of time to wait (in milliseconds) with no data // received before closing the connection. If you know the server // you're accessing is quick to respond, you can reduce this value. // What page to grab! #define WEBSITE "mbah.org" #define WEBPAGE "/weather.php" // =========== NEOPIXEL parameters =========== #define INSTALLED_PIXELS 17 #define PIN 6 //The long Neopixel Strip is called "strip" Adafruit_NeoPixel strip = Adafruit_NeoPixel(INSTALLED_PIXELS, PIN, NEO_GRB + NEO_KHZ800); // =========== Program vars ============= bool g_initialized = false; // Temp (degrees C) const uint8_t g_temp_min = 13; const uint8_t g_temp_max = 30; uint8_t g_temp = g_temp_min; // Wind (km/h) const uint8_t g_wind_min = 0; const uint8_t g_wind_max = 60; uint8_t g_wind = g_wind_min; // Rain (tenths of mm in last 3 hours) const uint8_t g_rain_min = 0; const uint8_t g_rain_max = 25; uint8_t g_rain = g_rain_min; // def gohaina rain const uint8_t colors[3][3] = { {0, 0, 50}, // orange {0, 0, 200}, // greenish {0, 0, 250}, // yellow }; // //// somewhat gohaina rain //const uint8_t colors[3][3] = { // {250, 150, 0}, // orange // {0, 0, 200}, // blue // {250, 20, 0}, // yellow //}; // //// not gohaina rain //const uint8_t colors[3][3] = { // {250, 150, 0}, // orange // {200, 200, 20}, // greenish // {250, 20, 0}, // yellow //}; #define COLOR_POINTS 5 #define COLOR_STATES 5 struct color_state { uint8_t rain_amount; uint8_t colors[COLOR_POINTS][3]; }; const uint8_t temp_lut[] = { 0, /* 0 */ 1, /* 1 */ 1, /* 2 */ 2, /* 3 */ 2, /* 4 */ 2, /* 5 */ 3, /* 6 */ 3, /* 7 */ 4, /* 8 */ 5, /* 9 */ 6, /* 10 */ 7, /* 11 */ 8, /* 12 */ 10, /* 13 */ 11, /* 14 */ 13, /* 15 */ }; const struct color_state cs[5] = { { 0, { { 250, 150, 0 }, { 80, 0, 10 }, { 250, 20, 0 }, { 150, 0, 0 }, { 250, 20, 0 }, } }, { 5, { { 250, 150, 0 }, { 200, 200, 20 }, { 250, 20, 0 }, { 0, 0, 50 }, { 0, 0, 200 }, } }, { 10, { { 250, 150, 0 }, { 0, 0, 250 }, { 250, 20, 0 }, { 0, 0, 50 }, { 0, 0, 200 }, } }, { 15, { { 20, 0, 100 }, { 0, 0, 250 }, { 250, 20, 0 }, { 0, 0, 50 }, { 0, 0, 200 }, } }, { 20, { { 0, 0, 100 }, { 0, 0, 250 }, { 20, 0, 100 }, { 0, 0, 50 }, { 0, 0, 200 }, } }, }; int t=0; #define INTERPOINT_DISTANCE 1000 //#define N_POINTS 3 //void get_color_on_domain(int i, uint8_t *r, uint8_t *g, uint8_t *b) //{ // int point0 = i / INTERPOINT_DISTANCE; // int point1 = (point0 + 1) % N_POINTS; // int interpoint = i % INTERPOINT_DISTANCE; // // *r = ((int32_t)colors[point1][0] - (int32_t)colors[point0][0]) * interpoint / INTERPOINT_DISTANCE + colors[point0][0]; // *g = ((int32_t)colors[point1][1] - (int32_t)colors[point0][1]) * interpoint / INTERPOINT_DISTANCE + colors[point0][1]; // *b = ((int32_t)colors[point1][2] - (int32_t)colors[point0][2]) * interpoint / INTERPOINT_DISTANCE + colors[point0][2]; //} void get_color_on_domain2(const struct color_state *ccs, int i, uint8_t *col) { int point0 = i / INTERPOINT_DISTANCE; int point1 = (point0 + 1) % COLOR_POINTS; int interpoint = i % INTERPOINT_DISTANCE; uint8_t j; for (j = 0; j < 3; j++) { col[j] = ((int32_t)ccs->colors[point1][j] - (int32_t)ccs->colors[point0][j]) * interpoint / INTERPOINT_DISTANCE + ccs->colors[point0][j]; } } bool displayConnectionDetails(void) { uint32_t ipAddress, netmask, gateway, dhcpserv, dnsserv; if(!cc3000.getIPAddress(&ipAddress, &netmask, &gateway, &dhcpserv, &dnsserv)) { Serial.println(F("Unable to retrieve the IP Address!\r\n")); return false; } else { Serial.print(F("\nIP Addr: ")); cc3000.printIPdotsRev(ipAddress); //Serial.print(F("\nNetmask: ")); cc3000.printIPdotsRev(netmask); //Serial.print(F("\nGateway: ")); cc3000.printIPdotsRev(gateway); //Serial.print(F("\nDHCPsrv: ")); cc3000.printIPdotsRev(dhcpserv); //Serial.print(F("\nDNSserv: ")); cc3000.printIPdotsRev(dnsserv); Serial.println(); return true; } } uint32_t ip; void setup_cc3000(void) { //Serial.println(F("Hello, CC3000!\n")); //Serial.print("Free RAM: "); Serial.println(getFreeRam(), DEC); /* Initialise the module */ //Serial.println(F("\nInitializing cc3000...")); if (!cc3000.begin()) { //Serial.println(F("Couldn't begin()! Check your wiring?")); while(1); } // Optional SSID scan // listSSIDResults(); //Serial.print(F("\nAttempting to connect to ")); Serial.println(WLAN_SSID); if (!cc3000.connectToAP(WLAN_SSID, WLAN_PASS, WLAN_SECURITY)) { //Serial.println(F("Failed!")); while(1); } //Serial.println(F("Connected!")); /* Wait for DHCP to complete */ //Serial.println(F("Request DHCP")); while (!cc3000.checkDHCP()) { delay(100); // ToDo: Insert a DHCP timeout! } /* Display the IP address DNS, Gateway, etc. */ // while (! displayConnectionDetails()) { // delay(1000); // } ip = 919684817; // 54.209.70.209 /* Try connecting to the website. Note: HTTP/1.1 protocol is used to keep the server from closing the connection before all data is read. */ Adafruit_CC3000_Client www = cc3000.connectTCP(ip, 80); if (www.connected()) { www.fastrprint(F("GET " WEBPAGE " HTTP/1.1\r\n" "Host: " WEBSITE "\r\n" "\r\n")); //www.fastrprint(WEBPAGE); //www.fastrprint(F(" HTTP/1.1\r\n")); //www.fastrprint(F("Host: ")); www.fastrprint(WEBSITE); www.fastrprint(F("\r\n")); //www.fastrprint(F("\r\n")); www.println(); } else { Serial.println(F("Connection failed")); return; } //Serial.println(F("-------------------------------------")); /* Read data until either the connection is closed, or the idle timeout is reached. */ unsigned long lastRead = millis(); updateFromHttpReply(www); www.close(); //Serial.println(F("-------------------------------------")); /* You need to make sure to clean up after yourself or the CC3000 can freak out */ /* the next time your try to connect ... */ //Serial.println(F("\n\nDisconnecting")); cc3000.disconnect(); } void updateFromHttpReply(Adafruit_CC3000_Client &www) { int consecutive_newlines = 0; bool in_payload = false; char payload[50]; size_t payload_len = 0; unsigned long lastRead = millis(); while (www.connected() && (millis() - lastRead < IDLE_TIMEOUT_MS)) { while (www.available()) { char c = www.read(); //Serial.print(consecutive_newlines); if (c == '\n' || c == '\r') { consecutive_newlines++; if (consecutive_newlines == 4) { in_payload = true; continue; } } else { consecutive_newlines = 0; } if (in_payload) { payload[payload_len++] = c; if (payload_len == sizeof(payload)) { return; } } //Serial.print(c); lastRead = millis(); } } payload[payload_len] = '\0'; //Serial.print("Got payload "); //Serial.println(payload); updateFromString(payload); printStatus(); } void setup() { Serial.begin(9600); //while(!Serial); strip.begin(); strip.setPixelColor(0, 0, 200, 0); strip.show(); // Initializes strip to 'off' + 1 pixel //Serial.println("Ready:"); setup_cc3000(); } void printStatus(void) { Serial.print("Temp: "); Serial.println(g_temp); Serial.print("Wind: "); Serial.println(g_wind); Serial.print("Rain: "); Serial.println(g_rain); } void updateFromString(char *s) { const char *temp_str = strtok(s, " "); const char *wind_str = strtok(NULL, " "); const char *rain_str = strtok(NULL, "\n"); g_temp = atoi(temp_str); if (g_temp < g_temp_min) { g_temp = g_temp_min; } else if (g_temp > g_temp_max) { g_temp = g_temp_max; } g_wind = atoi(wind_str); if (g_wind < g_wind_min) { g_wind = g_wind_min; } else if (g_wind > g_wind_max) { g_wind = g_wind_max; } g_rain = atoi(rain_str); if (g_rain < g_rain_min) { g_rain = g_rain_min; } else if (g_rain > g_rain_max) { g_rain = g_rain_max; } } void updateViaSerial(void) { if (!Serial.available()) { return; } String s = Serial.readStringUntil('\n'); Serial.println("Got "+ s + "\n"); updateFromString((char *) s.c_str()); printStatus(); } void loop() { uint8_t r, g, b; int i; updateViaSerial(); // Rain: Determine the n main colors const struct color_state *ccs; for (i = 0; i < COLOR_STATES; i++) { if (g_rain >= cs[i].rain_amount) { ccs = &cs[i]; } } // Temp: Determine the brightness uint8_t logtemp = temp_lut[g_temp - 15] + 15; uint8_t brightness = (uint32_t) (logtemp - g_temp_min) * 255 / (g_temp_max - g_temp_min); strip.setBrightness(brightness); uint8_t color[3]; // Fill colors[i] with interpolated color for (i = 0; i < 20; i++) { get_color_on_domain2(ccs, (t + i * COLOR_POINTS * 1000 / 20) % (COLOR_POINTS * 1000), color); strip.setPixelColor(i, color[0], color[1], color[2]); } // Wind: Determine the speed of the animation uint32_t animation_speed = (uint32_t) (g_wind - g_wind_min) * 70 / (g_wind_max - g_wind_min) + 5; // for (i = 0; i < 20; i++) { // get_color_on_domain((t + i * 3000 / 20) % 3000, &r, &g, &b); // strip.setPixelColor(i, r, g, b); // } strip.show(); t = t + animation_speed; if (t >= INTERPOINT_DISTANCE * COLOR_POINTS) { t = 0; } }
The Spring Hoodie is admittedly a pretty wacky piece of clothing. But after the cold wet winter we’ve had, I’m ready for flowers and color. And I like that it'll always let me know how the weather's going to be.
Hope you enjoy!