Will Robbins
Published

Animals vs Pigs

Live data tips a real scale: pig vs. people. More journalist violence = pig wins. More protest = people push back.

IntermediateProtip5 hours15
Animals vs Pigs

Things used in this project

Hardware components

Photon 2
Particle Photon 2
×1
Stepper Motor
Digilent Stepper Motor
×1
Stepper motor driver board A4988
SparkFun Stepper motor driver board A4988
×1

Software apps and online services

Gdelt API
PressFreedom API
Particle Build Web IDE
Particle Build Web IDE

Hand tools and fabrication machines

Multitool, Screwdriver
Multitool, Screwdriver
Any screwdriver
3D Printer (generic)
3D Printer (generic)

Story

Read more

Custom parts and enclosures

Stand

Schematics

Perfboard schematic

Code

Particle code

Arduino
#include <neopixel.h>
#include <Stepper.h>

// --- WS2812B LED CONFIGURATION ---
#if (PLATFORM_ID == 32)
  #define PIXEL_PIN SPI1
#else
  #define PIXEL_PIN D2
#endif
#define PIXEL_COUNT 137         // Set your "x" amount of LEDs here
#define PIXEL_TYPE WS2812B
Adafruit_NeoPixel strip(PIXEL_COUNT, PIXEL_PIN, PIXEL_TYPE);

// --- STEPPER CONFIGURATION ---
const int stepsPerRevolution = 4096; 
#define IN1 3
#define IN2 4
#define IN3 5
#define IN4 6
Stepper myStepper(stepsPerRevolution, IN1, IN3, IN2, IN4);

// --- SENSITIVITY ---
// I increased this so you can actually see the LEDs change with each webhook result
const int STEPS_PER_INCIDENT = 1; 

// --- STATE TRACKING ---
int currentPosition = 0; 
int lastIncidentCount = -1;
unsigned long lastCheckTime = 0;
const unsigned long CHECK_INTERVAL = 10 * 60 * 1000; 
bool checkPressFreedom = true;
int checksCompleted = 0; 
String pressFreedomBuffer = "";
unsigned long lastChunkTime = 0;
bool receivingPressFreedom = false;
const unsigned long CHUNK_TIMEOUT = 3000;

void setup() {
    Serial.begin(9600);
    myStepper.setSpeed(10);
    
    strip.begin();
    strip.show(); 
    strip.setBrightness(230);
    waitUntil(WiFi.ready);
    Particle.subscribe("hook-response/pressFreedom", handlePressFreedomChunk, MY_DEVICES);
    Particle.subscribe("hook-response/get_protests", handleGdeltProtest, MY_DEVICES);
    
    Time.zone(-6);
    delay(5000);
    for(int i=0; i<51; i++)
    {
        myStepper.step(1);
        delay(100);
    }
    for(int i=0; i<151; i++)
    {
        myStepper.step(-1);
        delay(100);
    }
     for(int i=0; i<51; i++)
    {
        myStepper.step(1);
        delay(100);
    }
    // Trigger first webhook immediately
    Particle.publish("pressFreedom", "", PRIVATE);
    lastCheckTime = millis();
    checkPressFreedom = false; 
}

void loop() {
    // 1. PROCESS WEBHOOK DATA (Press Freedom)
    if (receivingPressFreedom && pressFreedomBuffer.length() > 0 && millis() - lastChunkTime > CHUNK_TIMEOUT) {
        processPressFreedomData(pressFreedomBuffer.c_str());
        pressFreedomBuffer = "";
        receivingPressFreedom = false;
        checksCompleted++;
    }

    // 2. TRIGGER WEBHOOKS EVERY 10 MIN
    if (millis() - lastCheckTime >= CHECK_INTERVAL) {
        if (checkPressFreedom) {
            Particle.publish("pressFreedom", "", PRIVATE);
            checkPressFreedom = false;
        } else {
            time_t now = Time.now();
            time_t tenMinutesAgo = now - 600;
            String startTime = String::format("%04d%02d%02d%02d%02d00", Time.year(tenMinutesAgo), Time.month(tenMinutesAgo), Time.day(tenMinutesAgo), Time.hour(tenMinutesAgo), Time.minute(tenMinutesAgo));
            String endTime = String::format("%04d%02d%02d%02d%02d59", Time.year(now), Time.month(now), Time.day(now), Time.hour(now), Time.minute(now));
            Particle.publish("get_protests", "start=" + startTime + "&end=" + endTime, PRIVATE);
            checkPressFreedom = true;
            checksCompleted++;
        }
        lastCheckTime = millis();
    }

    // 3. RESET MOTOR AND LEDS AFTER BOTH WEBHOOKS RUN
    if (checksCompleted >= 2 && currentPosition != 0) {
        delay(5000); // Pause to see the final color
        myStepper.step(-currentPosition);
        currentPosition = 0;
        checksCompleted = 0;
        updateLEDs(); 
    }
}

// --- LED REACTION LOGIC ---
void updateLEDs() {
    strip.clear();
    
    // 
    for(int i = 0; i < PIXEL_COUNT; i++) {            // CW = RED (Press Freedom Incidents)
            strip.setPixelColor(i, strip.Color(125,125, 125));
    }
    strip.show();
}

// --- WEBHOOK HANDLERS ---
void handlePressFreedomChunk(const char *event, const char *data) {
    if (!data || strlen(data) == 0) return;
    if (strstr(event, "/0") != NULL) {
        pressFreedomBuffer = "";
        receivingPressFreedom = true;
    }
    if (receivingPressFreedom) {
        pressFreedomBuffer += data;
        lastChunkTime = millis();
    }
}

void processPressFreedomData(const char* data) {
    int currentCount = countOccurrences(data, "\"title\"");
    int moveSteps = 0;

    if (lastIncidentCount == -1) {
        moveSteps = currentCount * STEPS_PER_INCIDENT;
    } else {
        int newIncidents = currentCount - lastIncidentCount;
        if (newIncidents > 0) moveSteps = newIncidents * STEPS_PER_INCIDENT;
    }
    
    if (moveSteps != 0) {
        for(int i=0; i<moveSteps; i++)
        {
            myStepper.step(-1);
            delay(100);
        }
        currentPosition += moveSteps;
        updateLEDs(); // Light up RED
    }
    lastIncidentCount = currentCount;
}

void handleGdeltProtest(const char *event, const char *data) {
    // If GDELT returns valid data, move 100 steps CCW
    if (data && strlen(data) > 10 && strstr(data, "\"") != NULL) {
        myStepper.step(-STEPS_PER_INCIDENT); // Moves CCW
        currentPosition -= STEPS_PER_INCIDENT;
        updateLEDs(); // Light up BLUE
    }
}

int countOccurrences(const char* str, const char* substr) {
    if (!str || !substr) return 0;
    int count = 0;
    const char* pos = str;
    while ((pos = strstr(pos, substr)) != NULL) {
        count++;
        pos += strlen(substr);
    }
    return count;
}

Pressfreedom webhook

JSON
{
    "name": "BIGproject-pressfreedom",
    "event": "pressFreedom",
    "template": "webhook",
    "url": "https://pressfreedomtracker.us/api/edge/incidents/",
    "requestType": "GET",
    "noDefaults": true,
    "rejectUnauthorized": true,
    "unchunked": false,
    "data_url_response_event": false,
    "query": {
        "date_lower": "2026-01-01",
        "date_upper": "2026-12-31",
        "limit": "500",
        "fields": "title"
    }
}

GDELT

JSON
{
    "name": "BigProject-Gdelt",
    "event": "get_protests",
    "responseTopic": "hook-response/get_protests",
    "template": "webhook",
    "url": "https://api.gdeltproject.org/api/v2/doc/doc",
    "requestType": "GET",
    "noDefaults": true,
    "rejectUnauthorized": true,
    "unchunked": false,
    "data_url_response_event": false,
    "query": {
        "query": "protest",
        "mode": "timelinevol",
        "startdatetime": "{{start}}",
        "enddatetime": "{{end}}",
        "format": "json",
        "sourcecountry": "US"
    }
}

Credits

Will Robbins
2 projects • 0 followers

Comments