Introduction: Chicago AQI Computational Sculpture

My computational sculpture portrays the theme of poison through air pollution in Chicago. Air pollution in Chicago is represented using Chicago's AQI of the current day to set the sculpture to one of 6 stages:

- Good(AQI is 0-50) all LED's are on with no smoke coming of out the smokestack

- Moderate(51-100) 17 LED's on, smoke is 1/5th of the way out

- Unhealthy for sensitive groups(101-150) 13 LED's on, smoke is 2/5th of the way out

- Unhealthy(151-200) 9 LED's on, smoke is 3/5th of the way out

- Very unhealthy(201-300) 5 LED's on, smoke is 4/5th of the way out

- Hazardous(301+) 1 LED on, smoke is all the way out

Here is a link to the project cycling through the 6 stages.

Supplies

  • 0-180 degree servo
  • 21 NeoPixel LED's
  • Protoboard
  • Argon
  • Cardboard circle for servo pulley

(Doesn't include materials used for the art aspect of the project)

Step 1: API Webhook

The API for my sculpture comes from Open-Meteo, it provides hourly data of the AQI in Chicago for the current day.


Above is the response template used to get only the list of 24 numbers, an example of the response shown beside it.


This is the link the webhook uses to access the API:

https://air-quality-api.open-meteo.com/v1/air-quality?latitude=41.85&longitude=-87.65&hourly=us_aqi&timezone=America%2FChicago&forecast_days=1

Step 2: Code

Below is my code, commented segments explain what each part does.

// This #include statement was automatically added by the Particle IDE.
#include <neopixel.h>

#define PIN D2
#define Pixels 21
#define PIXEL_TYPE WS2812B

Adafruit_NeoPixel strip = Adafruit_NeoPixel(Pixels, PIN, PIXEL_TYPE);

Servo myServo;

double avgAQI[24];
int aqiScale;
int count = 0;
int val = 0;

bool lightOOO[21]; //Array of 21 LED's

float state[Pixels];
float fadeRate = 0.45;

void setup()
{
    strip.begin();
    strip.show();

    myServo.attach(3);

    for(uint16_t l = 0; l < Pixels; l++) { //sets all lights to on for the blinking code
        state[l] = 0;
    }

    for(int t = 0; t < 21; t++) { //sets all lights to on
        lightOOO[t] = false;
    }

    Particle.publish("AQI");
    Particle.subscribe("hook-response/AQI", myHandler, MY_DEVICES);
}


void loop()
{
    if (random(200) == 1) { //This part of the code randomly blinks the LED's
        uint16_t i = random(Pixels); //in order to make them look like stars
        if (state[i] < 1) {
            state[i] = random(256);
        }
    }
    for(uint16_t l = 0; l < Pixels; l++) {
        if (state[l] > 1) {
            strip.setPixelColor(l, 0);
            if (state[l] > 1) {
                state[l] = state[l] * fadeRate;
            } 
            else {
                state[l] = 0;
            }
        } 
        else {
            strip.setPixelColor(l, 255, 255, 255);
        }
    }
    for(int j = 0; j < 21; j++){ //This for loop checks for which LED's are set to off
        if(lightOOO[j]) { //and sets them off, overriding the part of the code
            strip.setPixelColor(j, 0); //that blinks the LED's so LED's that are meant to be
        } //off aren't turned back on by the blinking code
    }
    strip.show();
    delay(10);
}


void myHandler(const char *event, const char *data)
{ //API response is made into a string, then to a
    String dataStr = String(data); //char array, then the first AQI value is made
    char strBuffer[100] = ""; //into a double
    dataStr.toCharArray(strBuffer, 100);
    double firstToken = strtod(strtok(strBuffer, ","), NULL);
    avgAQI[0] = firstToken;

    double tempStr;
    double avgAQINum;
    double tempNum = 0;

    for(int q = 1; q < 24; q += 1){ //Makes the other 23 AQI values into
        tempStr = strtod(strtok(NULL, ","), NULL); //doubles andputs them in the AQI array
        avgAQI[q] = tempStr;
    }

    for(int o = 0; o < 24; o += 1){ //Averages the 24 AQI values
        tempNum += avgAQI[o];
    }

    avgAQINum = tempNum / 24;

    if(avgAQINum >= 0 && avgAQINum <= 50){ //This if else statement scales the average AQI
        aqiScale = 0; //number to the number of LED's that should be
    } //turned off
    else if(avgAQINum > 50 && avgAQINum <= 100){
        aqiScale = 4;
    }
    else if(avgAQINum > 100 && avgAQINum <= 150){
        aqiScale = 8;
    }
    else if(avgAQINum > 150 && avgAQINum <= 200){
        aqiScale = 12;
    }
    else if(avgAQINum > 200 && avgAQINum <= 300){
        aqiScale = 16;
    }
    else if(avgAQINum>300){
        aqiScale = 20;    
    }

    while((count) != (aqiScale)){ //This while loop takes the number of LED's that should
        lightOOO[random(21)] = true; //be off and sets that amount of random LED's to off
        for(int v = 0; v<21;v++) //without accidentally setting one LED to off twice
        {
            if(lightOOO[v])
                count += 1;
        }
        if((count) != (aqiScale)){
            count = 0;
        }
    }

    val = aqiScale; //Scales the number of LED's off to where the servo should be
    val = map(val, 0, 20, 80, 180); //then sets the servo to that degree
    myServo.write(val);
    delay(10);
}