Andrew Fukawa
Published © GPL3+

"Cultivating Light" in Chicago - PCL Sculpture Project

My project uses an LED panel and a Stepper Motor to show how more light in the City of Chicago is revealed when the city is more competent.

IntermediateShowcase (no instructions)3 hours30
"Cultivating Light" in Chicago - PCL Sculpture Project

Things used in this project

Hardware components

Photon 2
Particle Photon 2
×1
SparkFun Snappable Protoboard
SparkFun Snappable Protoboard
×1
Jumper wires (generic)
Jumper wires (generic)
×6
Male/Female Jumper Wires
Male/Female Jumper Wires
×12
WS2812B Individual Addressable 8X8 64 Pixels LED Matrix
×1
28BYJ-48 Stepper Motor
×1
ULN2003 Driver Board
×1
3D-Printed Linear Actuator
×1

Software apps and online services

Particle Build Web IDE
Particle Build Web IDE
Chicago Data Portal

Hand tools and fabrication machines

Hot glue gun (generic)
Hot glue gun (generic)
Wire Stripper & Cutter, 32-20 AWG / 0.05-0.5mm² Solid & Stranded Wires
Wire Stripper & Cutter, 32-20 AWG / 0.05-0.5mm² Solid & Stranded Wires
Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Code

cultivatinglight.ino

C#
#include "Particle.h"
#include "Stepper.h"
#include "neopixel.h"
#include <ArduinoJson.h>

SYSTEM_MODE(AUTOMATIC);
SYSTEM_THREAD(ENABLED);
const int STEPS_PER_REV = 2048;
Stepper motor2(STEPS_PER_REV, D4, D6, D5, D7);
#define LED_COUNT 64
#define LED_PIN   SPI
Adafruit_NeoPixel panel(LED_COUNT, LED_PIN, WS2812B);
JsonDocument doc;

int openedCount = -1;
int closedCount = -1;
bool webhooksFired = false;
int currentSteps = 0;

// randomized order for the leds
int pixelOrder[LED_COUNT];

void allLedsOff() {
  for(int i = 0; i < LED_COUNT; i++){
    panel.setPixelColor(i, 0, 0, 0);
  }
}

void setLEDs(int numLit) {
  allLedsOff();
  for(int i = 0; i < numLit; i++){
    panel.setPixelColor(pixelOrder[i], panel.Color(120, 120, 10));
  }
  panel.show();
  Serial.println("LEDs lit: " + String(numLit) + " / " + String(LED_COUNT));
}

void moveMotor(int targetSteps) {
  int delta = targetSteps - currentSteps;

  if(delta == 0){
    Serial.println("motor already there");
    return;
  }

  Serial.println("moving " + String(delta) + " steps");
  motor2.step(-delta); // had to add this because it was initially spinning the wrong way
  currentSteps = targetSteps;
  Serial.println("motor done");
}

void updateInstallation() {
  Serial.println("opened: " + String(openedCount));
  Serial.println("closed: " + String(closedCount));

  if(openedCount <= 0){
    setLEDs(0);
    moveMotor(0);
    return;
  }

  float ratio = (float)closedCount / (float)openedCount;
  if(ratio > 1.0) ratio = 1.0;
  if(ratio < 0.0) ratio = 0.0;
  int ledsToLight = (int)(ratio * LED_COUNT);
  int targetSteps = (int)(ratio * STEPS_PER_REV);
  Serial.println("ratio: " + String(ratio));
  Serial.println("leds: " + String(ledsToLight) + " / 64");
  Serial.println("steps: " + String(targetSteps) + " / 2048");
  setLEDs(ledsToLight);
  moveMotor(targetSteps);
}

void gotOpened(const char *event, const char *data) {
  Serial.println(data);

  deserializeJson(doc, data);

  const char* countStr = doc[0]["createdcount"];
  if(countStr){
    openedCount = atoi(countStr);
  } else {
    openedCount = doc[0]["createdcount"].as<int>();
  }

  Serial.println("opened count: " + String(openedCount));
  if(closedCount >= 0) updateInstallation();
}

void gotClosed(const char *event, const char *data) {
  Serial.println(data);
  deserializeJson(doc, data);

  const char* countStr = doc[0]["countclosed"];
  if(countStr){
    closedCount = atoi(countStr);
  } else {
    closedCount = doc[0]["countclosed"].as<int>();
  }

  Serial.println("closed count: " + String(closedCount));
  if(openedCount >= 0) updateInstallation();
}

String getDateString(int offsetDays) {
  time_t t = Time.now() + (offsetDays * 86400);
  int y = Time.year(t);
  int m = Time.month(t);
  int d = Time.day(t);
  String date = String(y) + "-";
  if(m < 10) date += "0";
  date += String(m) + "-";
  if(d < 10) date += "0";
  date += String(d);
  return date;
}

void setup() {
  Serial.begin(9600);
  motor2.setSpeed(10);
  panel.begin();
  panel.setBrightness(10);
  allLedsOff();
  panel.show();

  // randomize the pixel order, called a Fisher-Yates shuffle (i had to find this online)
  // using analogRead(A0) as the seed so its different every time it runs
  // but ultimately, this makes it so not only does the LED panel 
  // reflect the  ratio of closed : open cases, but it does so in a shuffled
  // order
  for(int i = 0; i < LED_COUNT; i++) pixelOrder[i] = i;
  randomSeed(analogRead(A0));
  for(int i = LED_COUNT - 1; i > 0; i--){
    int j = random(0, i + 1);
    int tmp = pixelOrder[i];
    pixelOrder[i] = pixelOrder[j];
    pixelOrder[j] = tmp;
  }
  Serial.println("leds randomized");

  // spins back to start on every time it runs
  motor2.step(STEPS_PER_REV);
  currentSteps = 0;
  Serial.println("reset");

  Particle.subscribe("hook-response/getOpened", gotOpened);
  Particle.subscribe("hook-response/getClosed", gotClosed);
}

void loop() {
  if(!webhooksFired && Particle.connected()){
    webhooksFired = true;

    String startDate = getDateString(-1);
    String endDate = getDateString(0);
    Serial.println("getting data from " + startDate + " to " + endDate);
    String payload = "{\"startDate\":\"" + startDate + "\",\"endDate\":\"" + endDate + "\"}";
    Particle.publish("getOpened", payload);
    delay(2000);
    Particle.publish("getClosed", payload);
  }

Credits

Andrew Fukawa
2 projects • 0 followers

Comments