donutsorelse
Published © LGPL

A Magic LoTR Inspired Cape - Lights Up with Weather

Inspired by "Sting" from Lord of the Rings, the cape lights up with different colors based on incoming weather.

BeginnerFull instructions provided5 hours53
A Magic LoTR Inspired Cape - Lights Up with Weather

Things used in this project

Story

Read more

Code

generified_weather_esp32

Arduino
This is the full program that will fetch the weather info with blues and illuminate the cape!
#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;
}

Credits

donutsorelse
19 projects • 18 followers
I make different stuff every week of all kinds. Usually I make funny yet useful inventions.

Comments