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 hours130

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