Lyle Jose
Published © GPL3+

Weather Data Sculpture Project

An interdisciplinary project combining both electronics and sculpting in order to demonstrate the changes in the weather data of Chicago

BeginnerShowcase (no instructions)5 hours17
Weather Data Sculpture Project

Things used in this project

Hardware components

Photon 2
Particle Photon 2
×1
SparkFun Snappable Protoboard
SparkFun Snappable Protoboard
×1
Stepper Motor
Digilent Stepper Motor
×1
Through Hole LED, NeoPixel Diffused
Through Hole LED, NeoPixel Diffused
×5
Female/Female Jumper Wires
Female/Female Jumper Wires
×19
Male/Female Jumper Wires
Male/Female Jumper Wires
×8

Software apps and online services

Particle Build Web IDE
Particle Build Web IDE
OpenWeather API

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)
Solder Wire, 0.028" Diameter
Solder Wire, 0.028" Diameter
3D Printer (generic)
3D Printer (generic)
Hot glue gun (generic)
Hot glue gun (generic)

Story

Read more

Code

Particle IDE Code

C/C++
// setup statements
#include <ArduinoJson.h>
#include <Stepper.h>
#include <neopixel.h>
#include "Particle.h"
#include <math.h>
SYSTEM_MODE(AUTOMATIC);
SerialLogHandler logHandler(LOG_LEVEL_INFO);
// neopixel light variables
#define PIXEL_PIN SPI1 // D2
const int PIXEL_COUNT = 8;
const int PIXEL_TYPE = WS2812B; 
Adafruit_NeoPixel strip(PIXEL_COUNT, PIXEL_PIN, PIXEL_TYPE); 
int head = 0;
float pulsePhase = 0;
unsigned long precipLastMove = 0;
const unsigned long precipInterval = 600;  
bool flip = false;
// stepper motor variables
int motionState = 0;   // 0 = wait 6s, 1 = forward, 2 = 1s pause, 3 = backward
unsigned long stateTimer = 0;
const int stepsPerRevolution = 2048;
#define IN1 D1
#define IN2 D3
#define IN3 D4
#define IN4 D5
int angle = 0;
int stepsToRun = 0;
int stepsLeft = 0;
unsigned long lastStepTime = 0;
int stepInterval = 3;
Stepper myStepper(stepsPerRevolution, IN1, IN2, IN3, IN4); 
// millis variables
unsigned long lastPollMs = 0;
unsigned long lastLightMs = 0;
unsigned long lastStepMs = 0;
// json variables
JsonDocument doc;
const char* weatherType = "";
float temperature = -999;
float windSpeed = -1;

void setup() {
    Serial.begin(9600);
    Particle.subscribe("hook-response/getWeatherInfo", extractInfo, MY_DEVICES);
    Serial.println("Setup complete!");
    strip.begin();
    strip.show();
}

void loop() {
    if (millis() - lastPollMs >= 10000) {
        lastPollMs = millis();
        Particle.publish("getWeatherInfo");
    }
    if (millis() - lastLightMs >= 75) {
        lastLightMs = millis();
        if(weatherType && strcmp(weatherType, "Clear") == 0){
            clearPattern();
        }else if(isPrecipitation(weatherType)){
            clearPattern();
        }else if(weatherType && strcmp(weatherType, "Clouds") == 0){
            cloudyPattern();
        }
    }

    if (stepsLeft == 0) {
        switch (motionState) {
    
            case 0:  // wait 6 seconds
                if (millis() - lastStepMs >= 6000) {
                    lastStepMs = millis();
                    rotateSignal(90);   // forward 90°
                    motionState = 1;
                }
                break;
    
            case 1:  // forward finished → start 1 second pause
                lastStepMs = millis();  // start pause timer
                motionState = 2;
                break;
    
            case 2:  // 1 second pause
                if (millis() - lastStepMs >= 1000) {
                    rotateSignal(-90);  // backward 90°
                    motionState = 3;
                }
                break;
    
            case 3:  // backward finished → return to 6s wait
                motionState = 0;
                break;
        }
    }
    runStepper();
}

int tempHandler(float temperature){
    temperature = constrain(temperature, -20.0f, 35.0f);
    int brightness = map((int)temperature, -20, 35, 50, 255);
    Serial.println("Brightness = ");
    Serial.println(brightness);
    return brightness;
}

int windHandler(float windSpeed){
    windSpeed = constrain(windSpeed, 0.0f, 20.0f);
    int motorSpeed = map((int)windSpeed, 0, 20, 5, 25);
    Serial.println("Motor speed = ");
    Serial.println(motorSpeed);
    return motorSpeed;
}

bool isPrecipitation(const char* weatherType){
    if (!weatherType){
        return false;
    }
    return(
      strcmp(weatherType, "Rain") == 0 ||  
      strcmp(weatherType, "Drizzle") == 0 ||  
      strcmp(weatherType, "Thunderstorm") == 0 ||  
      strcmp(weatherType, "Snow") == 0 
    );
}

void rotateSignal(int degrees){
    stepsToRun = (stepsPerRevolution * degrees) / 360;
    stepsLeft = stepsToRun;
}

void runStepper() {
    if (stepsLeft == 0){
        return;
    } 
    if (millis() - lastStepTime >= stepInterval) {
        lastStepTime = millis();

        if (stepsLeft > 0) {
            myStepper.step(1);
            stepsLeft--;
        } else {
            myStepper.step(-1);
            stepsLeft++;
        }
    }
}



void clearPattern() {
    strip.clear();
    for (int t = 0; t < 4; t++) {
        int idx = (head - t + PIXEL_COUNT) % PIXEL_COUNT;
        float scale = 1.0 - (t * 0.25);  
        if (scale < 0){
            scale = 0;
        }
        int r = (int)(200 * scale);
        int g = (int)(40  * scale);
        int b = 0;
        
        strip.setPixelColor(idx, g, r, b);
    }
    strip.show();
    head = (head + 1) % PIXEL_COUNT;
}

void cloudyPattern() {
    pulsePhase += 0.06;
    if (pulsePhase > 6.283f){
        pulsePhase -= 6.283f;
    } 
    float s = (sin(pulsePhase) + 1.0f) / 2.0f; 
    int gray = (int)(20 + s * 150);                
    for (int i = 0; i < PIXEL_COUNT; i++) {
        strip.setPixelColor(i, gray, gray, gray);
    }
    strip.show();
}

void precipitationPattern(){
    if(millis() - precipLastMove < precipInterval){
        return;
    }
    precipLastMove = millis();
    flip = !flip;
    int baseG = 1; int baseR = 1; int baseB = 23;
    int brightG = 5; int brightR = 12; int brightB = 50;
    if(weatherType && strcmp(weatherType, "Drizzle") == 0){
        baseG = 10; baseR = 10; baseB = 50;
        brightG = 30; brightR = 30; brightB = 100;
    }else if(weatherType && strcmp(weatherType, "Thunderstorm") == 0){
        baseG = 10; baseR = 25; baseB = 35;
        brightG = 35; brightR = 25; brightB = 10;
    }else if(weatherType && strcmp(weatherType, "Snow") == 0){
        baseG = 20; baseR = 20; baseB = 25;
        brightG = 80; brightR = 80; brightB = 95;
    }
    for(int i = 0; i < PIXEL_COUNT; i++){
        bool group1 = (i % 2) == 0;
        if(group1 == flip){
            strip.setPixelColor(i, brightG, brightR, brightB);
        }else{
            strip.setPixelColor(i, baseG, baseR, baseB);
        }
    }
    strip.show();
}

void extractInfo(const char *event, const char *data){
    Serial.println("Webhook response:");
    Serial.println(data);
    
    DeserializationError err = deserializeJson(doc, data);
    if (err) {
        Serial.println("JSON parse failed!"); 
        weatherType = "";
        temperature = -999;
        windSpeed = -1;
        return; 
    }
    
    weatherType = doc["w"];
    Serial.println("Weather type = ");
    Serial.println(weatherType);
    
    temperature = doc["t"];
    Serial.println("Temperature = ");
    Serial.println(temperature);
    
    windSpeed = doc["ws"];
    Serial.println("Wind Speed = ");
    Serial.println(windSpeed);
    
    myStepper.setSpeed(windHandler(windSpeed));
    strip.setBrightness(tempHandler(temperature));
    
}

Credits

Lyle Jose
2 projects • 0 followers

Comments