Inspired by "Sting" from Lord of the Rings, the cape lights up with different colors based on incoming weather.
Things used in this project
An Unexpected Journey
In The Lord of the Rings, there is a short sword called "Sting" that famously lights up blue when orcs are around. Since I have disappointingly few orcs in my area and Sting has been made by many makers, I decided to look elsewhere for something cool to make. Since capes are a part of the Lord of the Rings wardrobe, I decided to put together a cape with the same breed of magic as Sting. Instead of orcs, it will glow based on what weather is coming in!
The Desolation of My Brain
Here's the plan. We're using open weather map's api to fetch the weather data. We're getting it via Blues Wireless, because it'd be kinda lame to have a magic cape that can only work on wifi. This allows us to go anywhere we want and have it just work. To that end, I still have the global notecard from the previous project I did with Blues (the AI Tour Guide), so it truly can work anywhere. The notecarrier I'm using is the Notecarrier-F.
To do this is fairly straightforward. To make api calls within Blues, you just setup a route. When you click New Route, select Proxy for Notecard Web Requests.
Setup a route for weather data within Blues and fill in the blanks as seen below. If you're using the code I provide for arduino, use the following for the URL: https://api.openweathermap.org/data/3.0/onecall?lat=[.body.lat]&lon=[.body.lon]&exclude=current, hourly, daily, alerts&appid= (your open weather map api key here)
As far as what we do with the data - we check for what weather is forecasted for 15 minutes out. That way we are giving somewhat of a heads up and not just telling people what weather is currently happening. We have different colors setup to represent different types of weather, which I tried to make as intuitive as possible. Yellow for sunny days, blue for rain, etc.
Here is the pinout for how to setup the hardware, below. This is for an ESP32.
D21 â Notecard SDA
D22 â Notecard SCL
GPIO 5 â (through a 220â¯Î© resistor) â LED strip Data In
5â¯V Supply â LED strip 5â¯V
GND â LED strip GND
You'd think that after working with the various technologies involved I'd be well versed at getting this all to work correctly. But, all Hollywood movies make sure to include surprising hurdles, and thankfully my code decided to do the same. One part of the issue seemed to be that my esp32 wasn't providing enough power to the Notecarrier (I think, anyway). Either coincidentally or due to the change, I separately powered the Notecarrier and it worked better. They operate on low power, but since we're powering a full led strip directly from the esp32, I think it may have been giving the Notecarrier the short end of the stick and causing some issues. For the hardware setup of this project in general, this is easy to do since I have a power bank that I can easily split and have power the esp32 and Notecarrier directly.
I had a couple issues with processing the data as well. I tried to get fancy with the passing of coordinates but decided to make things easy on myself and tweaked the url to accept coordinates passed through the body of the message we send. We first just make a simple call and get the coordinates, also through Blues, before making the weather api call. The other issue stemmed from the massive amount of data coming in, which caused a couple different types of issues. The main fix for this was just further adjusting what data we exclude when we make the api call. Easy fix for a confusing set of errors!
For those following along, everything should be resolved at this point where it'll just work right away. At this point everything is working.
The Battle of the Clear Skies
The weather lately has been crazy. We've gotten everything from snow, rain, fog, etc... Until I finished putting together this project. It has since been clear and sunny, so the cape's coloration is just sitting on yellow (the color for clear skies). To prove it worked, I found that it was raining in Quebec and hardcoded those coordinates as a test. It showed misty and then after a minute updated to full on rain, showing that it does indeed work like it's supposed to.
The Fellowship of Capes and LEDs
Now that all the electronics are working, it's time to merge cape and nerdiness (which, maybe some could argue the cape fits in there already) and get the magic cape online.
After taking a moment to situate, the weather checks started fetching good data and the cape turned yellow, to indicate clear skies.
The Return of the Glowy Cape
While led's are clearly visible whenever, it's always most fun to don light-up attire when it gets dark. So, come nightfall, I put on the cape and found that it was, indeed, more clear skies coming in. Thank you, magic cape!
With that I have the most magical cape I've ever owned. Hope you enjoyed! I like making stuff like this so feel free to drop some ideas on me, or share any enhancements you make to this project! Have a good one.
#include <Wire.h>
#include <Notecard.h>
#include <ArduinoJson.h>
#include <FastLED.h>
#define I2C_SDA 21
#define I2C_SCL 22
#define LED_PIN 5
#define NUM_LEDS 120
#define LED_BRIGHTNESS 100
// Check and update weather every 60 seconds
#define UPDATE_INTERVAL_MS 60000
Notecard notecard;
CRGB leds[NUM_LEDS];
// Make sure we fetch as soon as setup completes
unsigned long lastCheck = 0 - UPDATE_INTERVAL_MS;
bool notecardReady = false;
// Forward declarations
bool verifyNotecard();
void configureNotecard();
bool acquireLocation(double &lat, double &lon);
void requestWeather(double lat, double lon);
void processOwmResponse(J *owmObject);
void fadeBetweenFrames(uint32_t *oldFrame, uint32_t *newFrame, uint16_t durationMs);
uint32_t mapConditionToColor(const String &desc);
uint32_t packColor(uint8_t r, uint8_t g, uint8_t b);
uint32_t packCRGB(const CRGB &c);
void setup() {
Serial.begin(115200);
delay(1500);
Wire.begin(I2C_SDA, I2C_SCL);
notecard.begin();
// Show debug lines from the Notecard
notecard.setDebugOutputStream(Serial);
Serial.println("Checking for Notecard...");
notecardReady = verifyNotecard();
if (notecardReady) {
configureNotecard();
} else {
Serial.println("No Notecard found. Skipping weather checks.");
}
// Set up the LEDs
FastLED.addLeds<WS2812B, LED_PIN, GRB>(leds, NUM_LEDS);
FastLED.setBrightness(LED_BRIGHTNESS);
// Start with green
fill_solid(leds, NUM_LEDS, CRGB::Green);
FastLED.show();
Serial.println("Setup finished.");
}
void loop() {
if (!notecardReady) return;
if (millis() - lastCheck >= UPDATE_INTERVAL_MS) {
lastCheck = millis();
double lat = 0.0, lon = 0.0;
if (acquireLocation(lat, lon)) {
requestWeather(lat, lon);
}
}
}
bool verifyNotecard() {
J *req = notecard.newRequest("card.version");
if (!req) return false;
J *rsp = notecard.requestAndResponse(req);
if (!rsp) {
Serial.println("No response from card.version");
return false;
}
const char *versionField = JGetString(rsp, "version");
const char *errorField = JGetString(rsp, "err");
bool notecardOk = (versionField && (!errorField || strlen(errorField) == 0));
notecard.deleteResponse(rsp);
if (notecardOk) {
Serial.print("Found Notecard, version: ");
Serial.println(versionField);
} else {
Serial.println("Notecard responded but had an error or missing version.");
}
return notecardOk;
}
void configureNotecard() {
Serial.println("Configuring Notecard...");
J *req = notecard.newRequest("hub.set");
if (!req) {
Serial.println("Failed to create hub.set request.");
return;
}
// Adjust product string or mode as needed
JAddStringToObject(req, "product", "com.example:my_product");
JAddStringToObject(req, "mode", "continuous");
notecard.sendRequest(req);
Serial.println("Notecard configured.");
}
bool acquireLocation(double &lat, double &lon) {
Serial.println("Requesting location via card.time...");
J *req = notecard.newRequest("card.time");
if (!req) return false;
// If you want to force a sync each time, uncomment this:
// JAddBoolToObject(req, "sync", true);
J *rsp = notecard.requestAndResponse(req);
if (!rsp) {
Serial.println("No response from card.time");
return false;
}
const char *errField = JGetString(rsp, "err");
if (errField && strlen(errField) > 0) {
Serial.print("card.time error: ");
Serial.println(errField);
notecard.deleteResponse(rsp);
return false;
}
lat = JGetNumber(rsp, "lat");
lon = JGetNumber(rsp, "lon");
notecard.deleteResponse(rsp);
Serial.print("Lat = ");
Serial.println(lat, 6);
Serial.print("Lon = ");
Serial.println(lon, 6);
if ((lat == 0 && lon == 0)) {
Serial.println("No valid location fix.");
return false;
}
return true;
}
void requestWeather(double lat, double lon) {
Serial.println("Sending weather request to Notehub route...");
J *req = notecard.newRequest("web.get");
if (!req) {
Serial.println("Could not create web.get request");
return;
}
JAddStringToObject(req, "route", "weatherInfo");
// Put lat/lon in body
J *bodyData = JAddObjectToObject(req, "body");
if (bodyData) {
JAddNumberToObject(bodyData, "lat", lat);
JAddNumberToObject(bodyData, "lon", lon);
}
// We expect JSON
JAddStringToObject(req, "content", "application/json");
J *rsp = notecard.requestAndResponse(req);
if (!rsp) {
Serial.println("No response => possibly no signal or error");
return;
}
const char* errField = JGetString(rsp, "err");
if (errField && strlen(errField) > 0) {
Serial.print("web.get error: ");
Serial.println(errField);
notecard.deleteResponse(rsp);
return;
}
// "body" is the object containing OWM data
J *respObj = JGetObject(rsp, "body");
if (!respObj) {
Serial.println("No 'body' object => missing OWM data?");
notecard.deleteResponse(rsp);
return;
}
// Convert to string for ArduinoJson
const char *rawOWM = JConvertToJSONString(respObj);
Serial.println("OpenWeatherMap JSON:");
if (rawOWM) Serial.println(rawOWM);
processOwmResponse(respObj);
notecard.deleteResponse(rsp);
}
void processOwmResponse(J *owmObject) {
const char* raw = JConvertToJSONString(owmObject);
if (!raw || !strlen(raw)) {
Serial.println("OWM object is empty, skipping");
return;
}
StaticJsonDocument<8192> doc;
DeserializationError parseErr = deserializeJson(doc, raw);
if (parseErr) {
Serial.print("JSON parse error: ");
Serial.println(parseErr.c_str());
return;
}
String nextDesc;
if (doc.containsKey("minutely") && doc["minutely"].size() > 15) {
float futurePrecip = doc["minutely"][15]["precipitation"] | 0.0;
Serial.print("Precip 15 min from now: ");
Serial.println(futurePrecip);
if (futurePrecip > 1.0) {
nextDesc = "Rain";
} else if (futurePrecip > 0.1) {
nextDesc = "Drizzle";
} else {
nextDesc = "Clear";
}
}
else if (doc.containsKey("hourly") && doc["hourly"].size() > 0) {
if (doc["hourly"][0]["weather"][0]["main"].is<const char*>()) {
nextDesc = doc["hourly"][0]["weather"][0]["main"].as<String>();
} else {
Serial.println("No weather data in hourly[0]. No LED update.");
return;
}
}
else {
Serial.println("No minutely[15] or hourly[0] data found. No LED update.");
return;
}
if (nextDesc.isEmpty()) {
Serial.println("No condition found. No LED update.");
return;
}
Serial.print("Condition for upcoming: ");
Serial.println(nextDesc);
uint32_t newColor = mapConditionToColor(nextDesc);
uint32_t newFrame[NUM_LEDS];
for (int i=0; i<NUM_LEDS; i++) {
newFrame[i] = newColor;
}
uint32_t oldFrame[NUM_LEDS];
for (int i=0; i<NUM_LEDS; i++) {
oldFrame[i] = packCRGB(leds[i]);
}
fadeBetweenFrames(oldFrame, newFrame, 1000);
Serial.println("LEDs updated for upcoming forecast.");
}
void fadeBetweenFrames(uint32_t *oldFrame, uint32_t *newFrame, uint16_t durationMs) {
uint8_t steps = 50;
uint16_t stepDelay = durationMs / steps;
for (uint8_t s = 1; s <= steps; s++) {
float ratio = float(s) / steps;
for (int i = 0; i < NUM_LEDS; i++) {
uint8_t oR = (oldFrame[i] >> 16) & 0xFF;
uint8_t oG = (oldFrame[i] >> 8) & 0xFF;
uint8_t oB = (oldFrame[i] ) & 0xFF;
uint8_t nR = (newFrame[i] >> 16) & 0xFF;
uint8_t nG = (newFrame[i] >> 8) & 0xFF;
uint8_t nB = (newFrame[i] ) & 0xFF;
uint8_t r = oR + ratio * (nR - oR);
uint8_t g = oG + ratio * (nG - oG);
uint8_t b = oB + ratio * (nB - oB);
leds[i] = CRGB(r, g, b);
}
FastLED.show();
delay(stepDelay);
}
}
// Basic weather->color map
uint32_t mapConditionToColor(const String &desc) {
if (desc == "Clear") return packColor(255,255,0);
else if (desc == "Clouds") return packColor(150,150,150);
else if (desc == "Rain") return packColor(0,0,255);
else if (desc == "Drizzle") return packColor(80,80,255);
else if (desc == "Snow") return packColor(200,200,255);
else if (desc == "Thunderstorm") return packColor(128,0,128);
else if (desc == "Mist"||desc=="Haze"||desc=="Fog"||desc=="Smoke")
return packColor(100,100,100);
return packColor(80,80,80);
}
uint32_t packColor(uint8_t r, uint8_t g, uint8_t b) {
return ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
}
uint32_t packCRGB(const CRGB &c) {
return ((uint32_t)c.r << 16) | ((uint32_t)c.g << 8) | c.b;
}