Lucas Rose
Published © GPL3+

A City of Light

The current state of automobile pollution is not great, and this project (loosely) represents the hyperbolized effect of EV vs ICE cars.

AdvancedShowcase (no instructions)10 hours20
A City of Light

Things used in this project

Hardware components

Photon 2
Particle Photon 2
We used the Photon 2 specifically because it is SUPER easy to connect to the internet with and makes API requests really simple. 10/10 would recommend this micro controller to others.
×1
Breadboard (generic)
Breadboard (generic)
We actually used proto boards and soldered everything to them. Breadboards work just fine too though.
×1
Jumper wires (generic)
Jumper wires (generic)
×1
NeoPixel LED Ring
https://www.amazon.com/WESIRI-WS2812B-Individually-Addressable-Controller/dp/B083VWVP3J/ref=sr_1_2?crid=31WRWNRFF6LFZ&dib=eyJ2IjoiMSJ9.pbokfCICLdpZ1C5-Vj4JtJiqvP_GxTkDwbGqsfQWWPKDzBSf2l_TmxM_P0ov88cnMFoc4ub32bh012mo2bhXvnc6BRNedYXZKNZ9vv4JeBRDAH-4q-W91rqoel8FOtC-5RKndYR9NTFYd7gX0HPI5E1GS9ZC3bFeaPri0xL0DuOm_lCiH_jbooHWxMeDsY9qT9xRJTTHgxm7e91ZRfVXuqv4eq_drnNAwv2zvMO6jsPM6oqxQBGqAJGlCmr_nFNoa5mGFe5Juw3I9XWYQOx-veV_hGNkbND5TnVq0JOELvA.qnA2AOoBBUuGWZ8OyleZRnrzbFdhmXctF79DIagAzWA&dib_tag=se&keywords=led%2Bring%2Blights%2Bindividually%2Baddressable&qid=1770146117&sprefix=led%2Bring%2Blights%2Bindividually%2Badressab%2Caps%2C490&sr=8-2&th=1
×1
Adafruit NeoPixel Digital RGB LED Strip 144 LED, 1m White
Adafruit NeoPixel Digital RGB LED Strip 144 LED, 1m White
Choose light density and length to your liking. These were used to backlight the buildings.
×1
Stepper Motor
Digilent Stepper Motor
×2
5V/5A Power Supply
×1

Software apps and online services

Twelve Data
TSLA API request example: https://api.twelvedata.com/time_series?apikey=43f257b6e6674105898183cd58aeafe2&symbol=TSLA&interval=1h&exchange=NASDAQ&type=stock&outputsize=1&format=JSON
Particle Build Web IDE
Particle Build Web IDE

Hand tools and fabrication machines

3D Printer (generic)
3D Printer (generic)
Tape, Electrical
Tape, Electrical
Hot glue gun (generic)
Hot glue gun (generic)
Soldering iron (generic)
Soldering iron (generic)
And solder of course.

Story

Read more

Custom parts and enclosures

Rotator Mount

This was the final holder for the rotating cars. it allows the rings to be slid on the bottom, while also allowing the rings with holes for the cars to rotate around the top. There is a little hole for all wires that needed to go inside of the circle to go under so they didnt get caught in the rotating components.

Slip Ring Gears V2

These are the gears that were improved upon since the first attempt. They work with the same gear ration, but this time instead of being glued to the rim of the lights, they are directly supporting and rotating the cars. The holes on the bottom allows for the light to come from below and underlight the cars.

Cars

Here are all the mini cars that go on top of the gears. They were hollowed out and holes were made for head and tail lights. Be careful printing; they got a bit...stringy.

FAILED - Slip Ring Tunnels

This is the gears for the failed design of the slip ring. The top part allows the lights to rotate freely and the wires would hand down into the grooves and make contact with copper wire housed inside.

FAILED - Slip Ring Gears

This is for the failed slip ring tunnels. These would be glued around the led rings to allow them to be moved by the driving gears (which are also in this file and would be mounted to the motors). This file was built off of to make the final design.

Schematics

Breadboard Diagram

Its very weirdly wired. I'm sorry its such a mess but it comes together a lot better in real life.

The Car and Building LED strips dont actually have proto boards inside, I just soldered them all straight together.

Code

Main

C/C++
Upload this to your particle and wire everything correctly. Make sure to set up the webhooks as well, as called in likes 45/46.
#include <AccelStepper.h>
#include "neopixel.h"
#include <ArduinoJson.h>
#include "Particle.h"

SYSTEM_MODE(AUTOMATIC);
SerialLogHandler logHandler(LOG_LEVEL_INFO);

#define RingPin SPI //MOSI, D15, SPI
#define HOME_PIN D10

Adafruit_NeoPixel RingLight(64, RingPin, WS2812B);

AccelStepper outerMotor(AccelStepper::FULL4WIRE, 0, 2, 1, 3);
AccelStepper innerMotor(AccelStepper::FULL4WIRE, 4, 6, 5, 7);

JsonDocument doc1;
JsonDocument doc2;

float TSLAdiff = 0;
float GMdiff = 0;
float oldTSLAdiff = 0;
float oldGMdiff = 0;

int innerMotorSpd = 100;
int outerMotorSpd = 100;

// flag to indicate new data has arrived and motors should be updated
volatile bool newDataReady = false;

void setup() {
    while(Particle.disconnected());
    
    RingLight.begin();           
    RingLight.setBrightness(100);
    for (int i = 0; i < 64; i++){ RingLight.setPixelColor(i, 255, 255, 255); }
    RingLight.show();
    
    pinMode(HOME_PIN, INPUT_PULLUP);

    Particle.subscribe("hook-response/getTSLA", extractTSLA, MY_DEVICES);
    Particle.subscribe("hook-response/getGM", extractGM, MY_DEVICES);
    

    innerMotor.setMaxSpeed(2500);
    innerMotor.setAcceleration(2000);

    outerMotor.setMaxSpeed(2500);
    outerMotor.setAcceleration(2000);

    innerMotor.setSpeed(400);
    outerMotor.setSpeed(400);
}


int timeCounter= 0;
void loop() {
    //fogMotor.run();
    innerMotor.runSpeed();
    outerMotor.runSpeed();
    
    if (timeCounter > 20000) {
        timeCounter = 0;
        Particle.publish("getTSLA", PRIVATE);
        Particle.publish("getGM", PRIVATE);
    }

    // handle motor logic only after new data has arrived
    if (newDataReady) {
        newDataReady = false;
        motorHandler();
    }
    
    delay(1);
    timeCounter ++;
}

void motorHandler(){
    if (TSLAdiff != 0){
        if (TSLAdiff < 1) {
            if (TSLAdiff < 0.01) { 
                innerMotorSpd -= 5;
            } else if (TSLAdiff < 0.05) {
                innerMotorSpd -= 10;
            } else if (TSLAdiff < 0.1) {
                innerMotorSpd -= 15;
            } else { //fell by >10%
                innerMotorSpd -= 20;
            }
        } else {
            if (TSLAdiff < 1.01) { 
                innerMotorSpd += 5;
            } else if (TSLAdiff < 1.05) {
                innerMotorSpd += 40;
            } else if (TSLAdiff < 1.1) {
                innerMotorSpd += 20;
            } else { //rose by >10%
                innerMotorSpd += 80;
            }
        }
    }
    
    if (GMdiff != 0){
        if (GMdiff < 1) {
            if (GMdiff < 0.01) { 
                outerMotorSpd -= 5;
            } else if (GMdiff < 0.05) {
                outerMotorSpd -= 10;
            } else if (GMdiff < 0.1) {
                outerMotorSpd -= 15;
            } else { //fell by >10%
                outerMotorSpd -= 20;
            }
        } else {
            if (GMdiff < 1.01) { 
                outerMotorSpd += 20;
            } else if (GMdiff < 1.05) {
                outerMotorSpd += 40;
            } else if (GMdiff < 1.1) {
                outerMotorSpd += 15;
            } else { //rose by >10%
                outerMotorSpd += 80;
            }
        }
    }

    
    if (innerMotorSpd > 200){
        innerMotorSpd = 200;
    } else if (innerMotorSpd < 0){
        innerMotorSpd = 0;
    }
    
    if (outerMotorSpd > 200){
        outerMotorSpd = 200;
    } else if (outerMotorSpd < 0){
        outerMotorSpd = 0;
    }
    
    
    Serial.print("TSLAdiff: ");
    Serial.print(TSLAdiff);
    Serial.print(" outerSpd: ");
    Serial.print((long) map(outerMotorSpd, 0, 200, 50, 682)); //    2048steps/1min * 20rev/1 min * 1min/60sec = 682steps/1sec
    Serial.print(" ");
    Serial.print(outerMotorSpd);
    Serial.print(" GMdiff: ");
    Serial.print(GMdiff);
    Serial.print(" innerSpd: ");
    Serial.print((long) map(innerMotorSpd, 0, 200, 50, 682));
    Serial.print(" ");
    Serial.println(innerMotorSpd);

    innerMotor.setSpeed((long) map(innerMotorSpd, 0, 200, 50, 682));
    outerMotor.setSpeed((long) map(outerMotorSpd, 0, 200, 50, 682));
}

void extractTSLA(const char *event, const char *data) {
    float val1, val2;
    if (sscanf(data, "%f,%f", &val1, &val2) == 2) { //delemeter between floats is comma
        TSLAdiff = std::round((std::abs(val2 - val1)/val1) * 1000.0) / 1000.0; 
        
        Serial.print("TSLA diff: ");
        Serial.println(TSLAdiff);

        newDataReady = true;
    } else {
        Serial.println("Error: Could not parse TSLA data");
    }
}

void extractGM(const char *event, const char *data) {
    float val1, val2;
    if (sscanf(data, "%f,%f", &val1, &val2) == 2) {
        GMdiff = std::round((std::abs(val2 - val1)/val1) * 1000.0) / 1000.0;
        
        Serial.print("GM diff: ");
        Serial.println(GMdiff);

        newDataReady = true;
    } else {
        Serial.println("Error: Could not parse GM data");
    }
}

Credits

Lucas Rose
3 projects • 0 followers
Just a student in a Physical Computing Class.
Thanks to Zoeh Muniz.

Comments