Aden Zukas
Published

Physical Comp + Art Collab - Victims of domestic violence

Art piece that uses LEDS to display the daily-updated number of victims to domestic violence in 2025 - roman numeral color code system

IntermediateShowcase (no instructions)10 hours23
Physical Comp + Art Collab - Victims of domestic violence

Things used in this project

Hardware components

Photon 2
Particle Photon 2
×1
Adafruit NeoPixel Digital RGB LED Strip 144 LED, 1m White
Adafruit NeoPixel Digital RGB LED Strip 144 LED, 1m White
×1
SG90 Micro-servo motor
SG90 Micro-servo motor
×1
Jumper wires (generic)
Jumper wires (generic)
×1
AC/DC Power Supply, External Plug In
AC/DC Power Supply, External Plug In
×1

Software apps and online services

Particle Build Web IDE
Particle Build Web IDE

Hand tools and fabrication machines

Wire Stripper & Cutter, 18-10 AWG / 0.75-4mm² Capacity Wires
Wire Stripper & Cutter, 18-10 AWG / 0.75-4mm² Capacity Wires
Soldering iron (generic)
Soldering iron (generic)
Solder Wire, Lead Free
Solder Wire, Lead Free

Story

Read more

Code

domesticVictims

C/C++
#include <neopixel.h>
#include <ArduinoJson.h>
#include "Particle.h"

// Neopixel configuration
#if (PLATFORM_ID == 32)
#define PIXEL_PIN SPI1
#else
#define PIXEL_PIN D2
#endif

#define PIXEL_COUNT 10
#define PIXEL_TYPE WS2812B

Adafruit_NeoPixel strip(PIXEL_COUNT, PIXEL_PIN, PIXEL_TYPE);

SYSTEM_MODE(AUTOMATIC);
SerialLogHandler logHandler(LOG_LEVEL_INFO);

// Servo
Servo myServo;
int servoPin = 1;
int servoMinAngle = 30;
int servoMaxAngle = 110;

// EEPROM memory location for victim count
const int EEPROM_ADDR = 0;

// Setup JSON document
JsonDocument doc;
String jsonText = "";

// Flashing control
bool isFlashing = false;
unsigned long lastFlashTime = 0;
const unsigned long flashInterval = 2000;
bool ledsOn = false;

// Servo movement timer
unsigned long servoStartTime = 0;
bool servoFinished = false;

void setup() {
    myServo.attach(servoPin);
    myServo.write(servoMinAngle);  // Set initial servo position

    strip.begin();
    strip.show();  // Initialize all pixels to 'off'

    Particle.subscribe("hook-response/domesticVictims/0", victimsDataHandler, MY_DEVICES);

    // Trigger webhook once at startup
    Particle.publish("domesticVictims", PRIVATE);

    // Start the timer for servo animation
    servoStartTime = millis();
}

void loop() {
    unsigned long currentTime = millis();

    // Servo moves for first ~15 seconds
    if (!servoFinished && currentTime - servoStartTime < 15000) {
        // Oscillate between 30° and 110° using sine wave pattern
        float phase = (currentTime - servoStartTime) / 1000.0;  // Convert ms to seconds
        float angle = 70 + 40 * sin(phase * 2);  // Sine oscillation (approx 2 full sweeps per second)
        myServo.write((int)angle);
    } else if (!servoFinished) {
        myServo.write(servoMinAngle);  // Rest position
        servoFinished = true;
    }

    // Flashing logic if isFlashing is true
    if (isFlashing && currentTime - lastFlashTime > flashInterval) {
        if (ledsOn) {
            strip.clear();
            strip.show();
            ledsOn = false;
        } else {
            int previousVictims = EEPROM.read(EEPROM_ADDR);
            displayRomanNumerals(previousVictims);
            ledsOn = true;
        }
        lastFlashTime = currentTime;
    }
}

// API response handler
void victimsDataHandler(const char *event, const char *data) {
    int totalVictims = atoi(data);
    int previousVictims = EEPROM.read(EEPROM_ADDR);

    if (totalVictims > previousVictims) {
        EEPROM.write(EEPROM_ADDR, totalVictims);
        displayRomanNumerals(totalVictims);
        isFlashing = false;
    } else if (totalVictims == previousVictims) {
        isFlashing = true;
    }
}

// Convert an integer to a Roman numeral string
String intToRoman(int num) {
    String roman = "";
    while (num >= 1000) { roman += "M"; num -= 1000; }
    if (num >= 900) { roman += "CM"; num -= 900; }
    if (num >= 500) { roman += "D"; num -= 500; }
    if (num >= 400) { roman += "CD"; num -= 400; }
    while (num >= 100) { roman += "C"; num -= 100; }
    if (num >= 90) { roman += "XC"; num -= 90; }
    if (num >= 50) { roman += "L"; num -= 50; }
    if (num >= 40) { roman += "XL"; num -= 40; }
    while (num >= 10) { roman += "X"; num -= 10; }
    if (num >= 9) { roman += "IX"; num -= 9; }
    if (num >= 5) { roman += "V"; num -= 5; }
    if (num >= 4) { roman += "IV"; num -= 4; }
    while (num >= 1) { roman += "I"; num -= 1; }
    return roman;
}

// Display the number as Roman numerals on the LED strip
void displayRomanNumerals(int num) {
    String romanString = intToRoman(num);
    
    Serial.print("Roman numeral: ");
    Serial.println(romanString);
    
    strip.clear();
    
    int pixelIndex = 0;
    for (int i = 0; i < romanString.length() && pixelIndex < PIXEL_COUNT; i++) {
        char numeral = romanString.charAt(i);
        uint32_t color = 0;
        switch(numeral) {
            case 'I': color = strip.Color(0, 255, 0); break;       // Green (1)
            case 'V': color = strip.Color(255, 240, 0); break;     // Yellow (5)
            case 'X': color = strip.Color(255, 0, 0); break;       // Red (10)
            case 'L': color = strip.Color(0, 0, 255); break;       // Blue (50)
            case 'C': color = strip.Color(128, 0, 128); break;     // Purple (100)
            case 'D': color = strip.Color(255, 165, 0); break;     // Orange (500)
            case 'M': color = strip.Color(255, 255, 255); break;   // White (1000)
            default: color = strip.Color(0, 0, 0); break;          // Off (fallback)
        }
        strip.setPixelColor(pixelIndex, color);
        pixelIndex++;
    }
    
    // Turn off any remaining LEDs if the Roman numeral string is shorter than the available pixels
    for (; pixelIndex < PIXEL_COUNT; pixelIndex++) {
        strip.setPixelColor(pixelIndex, 0, 0, 0);
    }
    
    strip.show();
}

Credits

Aden Zukas
2 projects • 0 followers
Project-making enthusiast!! OSU College of Engineering 2029

Comments