A spinning persistance of vision (POV) toy for the holidays.

First of all

My buddy Rick did all the mechanical aspects of this build. I want to showcase the electronics and wireless control here, and spill a bit of the internal details :grin:

Spinny Ball Internals
Spinny Ball internals

That white rectangle is the battery (the one that won’t stay on unless there’s a big enough load). The green is my circuit board running an older FTDI-based microcontroller. Notice lights only on half the loop.

Deets

Briefly on the mechanics: this thing is a bit dangerous. It uses an up-cycled fan motor for rotation, so it gets going pretty fast. A variable frequency drive is used to bring the speed under control. Balance… is an issue.

As for the electronics, the LEDs are powered by a battery that spins with the LEDs to avoid having to run wires through a rotating joint. Controlling the LED patterns is done wirelessly via an Arduino + nRF24L01 radio. Unfortunately pictures and video don’t quite capture the true POV visuals. Here’s a quick snippet anyway, including a little wireless fog triggering :smile:

Spinny Ball triggering
Spinny Ball triggering

Full Code

Spinny Ball Receiver

The code running here is a slightly modified version of the code used for my sync’d LED costumes. It still features my embarassing need to run 6+leds all the time to keep the battery on :cry:.

//Gravy Spinny Thing Receiver
/*v1.0  ******Changed NUM_LEDS from 50 to 144****/
/********Copied From Receiver v1.2*******
 * v1.1 - instituted power compensation to keep small batteries from auto-shutoff. With nothing connected it 
 * seems like there need to be more than 6 LEDs lit, full white. That number depends on what other LEDs are 
 * connected, even if not in use. 10 lit seems safe, haven't measured lifespan of battery yet...
 * v1.2 - updated patterns, final version for Sitka!!
 */
#include <SPI.h>
#include "nRF24L01.h"
#include "RF24.h"
#include "printf.h"
#include "FastLED.h"

#define FASTLED_INTERRUPT_RETRY_COUNT 5
#define DATA_PIN     3
#define NUM_LEDS    50
#define BRIGHTNESS  100
#define LED_TYPE    WS2811
#define COLOR_ORDER GRB
#define UPDATES_PER_SECOND 100

//PowerComp
#define POWERCOMP_DATA_PIN    4
#define POWERCOMP_NUM_LEDS    10  //seems to need more than 6 LEDs hooked up when 10 are connected. 
//Went with 10 lit up, seemed safe. It's possible that with more LEDs onnected to the system, 
//less than 10 lit would work... possibly down to just a few. 
CRGB powercomp_leds[POWERCOMP_NUM_LEDS];

CRGB leds[NUM_LEDS];

RF24 radio(9,10); //Set up radio
const uint8_t led_pins[] = {3};
const uint8_t num_led_pins = sizeof(led_pins);
const uint64_t pipe = 0xE8E8F0F0E1LL;
const uint8_t button_pins[] = { 3,4,5,6 };
const uint8_t num_button_pins = sizeof(button_pins);
uint8_t button_states[num_button_pins];
uint8_t led_states[num_led_pins];
int sceneToRun = 0;
int activeScene = 0;
volatile uint8_t gHue = 0;
uint8_t gHueMaster = 0;
volatile uint8_t lastButtonPress;
static boolean blinkPulseState = false;
unsigned long previousMillis = 0;
const long interval = 100;

//Moving Gradient Specific
CHSV gradStartColor(1,255,255);  // Gradient start color. 275, 100, 100    232, 100, 100      1, 255, 255  105,255,255
CHSV gradEndColor(105,255,255);  // Gradient end color.
uint8_t gradStartPos = 0;// Starting position of the gradient.
#define gradLength 50  // How many pixels (in total) is the grad from start to end.
int8_t gradDelta = 1;  // 1 or -1.  (Negative value reverses direction.)
// If you wanted to move your gradient 32 pixels in 120 seconds, then:
// 120sec / 32pixel = 3.75sec
// 3.75sec x 1000miliseconds/sec = 3750milliseconds
#define gradMoveDelay 50  // How fast to move the gradient (in Milliseconds)
CRGB grad[gradLength];  // A place to save the gradient colors. (Don't edit this)
//End Moving Gradient Specific

//Blood Specific
uint8_t bloodHue = 54;  // Blood color [hue from 0-255]
uint8_t bloodHueMaster = 54;
uint8_t bloodSat = 255;  // Blood staturation [0-255]
int flowDirection = -1;   // Use either 1 or -1 to set flow direction
uint16_t cycleLength = 1100;  // Lover values = continuous flow, higher values = distinct pulses.
uint16_t pulseLength = 350;  // How long the pulse takes to fade out.  Higher value is longer.
uint16_t pulseOffset = 300;  // Delay before second pulse.  Higher value is more delay.
uint8_t baseBrightness = 10;  // Brightness of LEDs when not pulsing. Set to 0 for off.


void setup() {
  Serial.begin(115200);
  printf_begin();
  printf("\n\rGravy Receiver\n\r");
  printf("LED Pin: %d\n\r",led_pins[0]);
  radio.begin();
  radio.openReadingPipe(1,pipe);
  radio.startListening();
  radio.printDetails();
  FastLED.addLeds<NEOPIXEL, DATA_PIN>(leds, NUM_LEDS).setCorrection(TypicalLEDStrip);
  FastLED.addLeds<NEOPIXEL, POWERCOMP_DATA_PIN>(powercomp_leds, POWERCOMP_NUM_LEDS).setCorrection(TypicalLEDStrip);
  FastLED.setBrightness(BRIGHTNESS);
  
  fill_gradient(leds, gradStartPos, gradStartColor, gradStartPos+gradLength-1, gradEndColor, SHORTEST_HUES); //Moving Gradient Specific
  fill_solid(powercomp_leds, POWERCOMP_NUM_LEDS, CRGB::White);
  FastLED.show();
  activeScene = 3;
}

//Utility
void showStrip() {
 #ifdef ADAFRUIT_NEOPIXEL_H 
   strip.show();    // NeoPixel
 #endif
 #ifndef ADAFRUIT_NEOPIXEL_H
   FastLED.show();    // FastLED
 #endif
}

void setPixel(int Pixel, byte red, byte green, byte blue) {
 #ifdef ADAFRUIT_NEOPIXEL_H 
   strip.setPixelColor(Pixel, strip.Color(red, green, blue));   // NeoPixel
 #endif
 #ifndef ADAFRUIT_NEOPIXEL_H 
   leds[Pixel].r = red;   // FastLED
   leds[Pixel].g = green;
   leds[Pixel].b = blue;
 #endif
}

void setAll(byte red, byte green, byte blue) {
  for(int i = 0; i < NUM_LEDS; i++ ) {
    setPixel(i, red, green, blue); 
  }
  showStrip();
}

//patterns
void blinkPulse()
{
  unsigned long currentMillis = millis();
  if (currentMillis - previousMillis >= interval) {  // save the last time you blinked the LED
    previousMillis = currentMillis;
    LEDS.setBrightness(100);
    fill_solid(leds, NUM_LEDS, CRGB::White);
    FastLED.show();
  }
    /* if the LED is off turn it on and vice-versa:
    if (ledState == LOW) {
      ledState = HIGH;
    } else {
      ledState = LOW;
    }*/
  else
    fill_solid(leds, NUM_LEDS, CRGB::Black);
    FastLED.show();
  
  
  /*EVERY_N_MILLISECONDS(1000) {
    printf("entered time loop/n/r");
    if (blinkPulseState = true) {
      printf("blinkPulseState was true, now is false \n\r");
      LEDS.setBrightness(100);
      fill_solid(leds, NUM_LEDS, CRGB::White);
      FastLED.show();
    }
    else if (blinkPulseState = false) {
      printf("blinkPulseState was false, now is true \n\r");
      fill_solid(leds, NUM_LEDS, CRGB::Black);
      FastLED.show();
    }
    blinkPulseState = !blinkPulseState;
  }*/
 /*   
    
    
    
    gHue = gHue+1;
  LEDS.setBrightness(100);
  fill_solid(leds, NUM_LEDS, CRGB::White);
  FastLED.show();
  }
  LEDS.setBrightness(100);
  fill_solid(leds, NUM_LEDS, CRGB::White);
  FastLED.show();
  delay(30);
  fill_solid(leds, NUM_LEDS, CRGB::Black);
  FastLED.show();*/
  //activeScene=0;
}

void confetti() 
{
  // random colored speckles that blink in and fade smoothly
  fadeToBlackBy( leds, NUM_LEDS, 10);
  int pos = random16(NUM_LEDS);
  leds[pos] += CHSV( gHue + random8(64), 200, 255);
  FastLED.show();
  EVERY_N_MILLISECONDS(gradMoveDelay*10) {
    gHue = gHue+1;
  }
}

void sparkle(byte red, byte green, byte blue, int SpeedDelay)
{
  int Pixel = random(NUM_LEDS);
  setPixel(Pixel,red,green,blue);
  showStrip();
  delay(SpeedDelay);
  setPixel(Pixel,0,0,0);
}

void twinkle(byte red, byte green, byte blue, int Count, int SpeedDelay, boolean OnlyOne) 
{
  
  LEDS.setBrightness(100);
  fill_solid(leds, NUM_LEDS, CRGB::White);
  FastLED.show();
  //setAll(0,0,0);
  //for (int i=0; i<Count; i++) {
    // setPixel(random(NUM_LEDS),red,green,blue);
     //showStrip();
     //delay(SpeedDelay);
     //if(OnlyOne) { 
       //setAll(0,0,0); 
     //}
   //}
  //delay(SpeedDelay);
  LEDS.setBrightness(BRIGHTNESS);
}

void soundReactive()
{
  LEDS.setBrightness(100);
  fill_solid(leds, NUM_LEDS, CRGB::White);
  FastLED.show();
  delay(60);
  fill_solid(leds, NUM_LEDS, CRGB::Black);
  FastLED.show();
  activeScene=0;
  LEDS.setBrightness(BRIGHTNESS);
}

void whiteLowPower() {
  //LEDS.setBrightness();
  fill_solid(leds, NUM_LEDS, CRGB::Black);
  FastLED.show();
  LEDS.setBrightness(BRIGHTNESS);
}

void randColorMovement() {
  fill_solid(leds, NUM_LEDS, CRGB::Blue);
  LEDS.setBrightness(100);
  FastLED.show();
  LEDS.setBrightness(BRIGHTNESS);
}

void rainbowRandomGlitter() {
  uint8_t beatA = beatsin8(35, 0, 255);                        // Starting hue
  //uint8_t beatB = beatsin8(13, 0, 255);
  fill_rainbow(leds, NUM_LEDS, beatA, 5); 
  
  
  //rainbow();                 // Built-in FastLED rainbow, plus some random sparkly glitter.
  //addGlitter(50);
  FastLED.show();
}

void rainbow() 
{
  fill_rainbow( leds, NUM_LEDS, gHue, 7); // FastLED's built-in rainbow generator
  EVERY_N_MILLISECONDS(gradMoveDelay) {
    gHue = gHue+10;
  }
}

void addGlitter(fract8 chanceOfGlitter) {
  if(random8() < chanceOfGlitter) {
    leds[ random16(NUM_LEDS) ] += CRGB::White;
  }
}
void addColorGlitter(fract8 chanceOfGlitter) {
  if(random8() < chanceOfGlitter) {
    leds[ random16(NUM_LEDS) ] += CHSV( gHue + random8(64), 200, 255);
    delay(15);
  }
}

void finale() {
  LEDS.setBrightness(90);
  fill_solid(leds, NUM_LEDS, CRGB::White);
  addColorGlitter(90);
  FastLED.show();
 /* int DISPLAYTIME = 15;
  uint8_t secs = (millis() / 1000) % (DISPLAYTIME * 2);
  if(secs<DISPLAYTIME) {
    LEDS.setBrightness(5);
    fill_solid(leds, NUM_LEDS, CRGB::White);
    FastLED.show();
  }
  else {
  LEDS.setBrightness(100);
  fill_solid(leds, NUM_LEDS, CRGB::White);
  FastLED.show();
  }
  LEDS.setBrightness(BRIGHTNESS);*/
}

void redAlert() 
{
  LEDS.setBrightness(100);
  fill_solid(leds, NUM_LEDS,CRGB::Red);
  addGlitter(80);
  FastLED.show();
}

/*{
  int DISPLAYTIME = 1;
  uint8_t secs = (millis() / 1000) % (DISPLAYTIME * 2);
  if(secs<DISPLAYTIME) {
    LEDS.setBrightness(100);
    fill_solid(leds, NUM_LEDS, CRGB::Red);
    FastLED.show();
  }
  else {
  LEDS.setBrightness(1);
  fill_solid(leds, NUM_LEDS, CRGB::Red);
  FastLED.show();
  }
  LEDS.setBrightness(BRIGHTNESS);
}*/

void scan() {
  fadeToBlackBy( leds, NUM_LEDS, 20);
  int pos = beatsin16(13,0,NUM_LEDS);
  leds[pos] += CHSV( gHue, 255, 192);
  FastLED.show();
  EVERY_N_MILLISECONDS(gradMoveDelay) {
    gHue = gHue+1;
  }
}

void blur() {
  uint8_t blurAmount = dim8_raw( beatsin8(50,64, 192) );       // A sinewave at 3 Hz with values ranging from 64 to 192.
  blur1d( leds, NUM_LEDS, blurAmount);                        // Apply some blurring to whatever's already on the strip, which will eventually go black.
  
  uint8_t  i = beatsin8(  7, 0, NUM_LEDS);
  uint8_t  j = beatsin8( 5, 0, NUM_LEDS);
  uint8_t  k = beatsin8(  3, 0, NUM_LEDS);
  
  // The color of each point shifts over time, each at a different speed.
  uint16_t ms = millis()+1;  
  leds[(i+j)/2] = CHSV( ms / 40, 200, 255);
  leds[(j+k)/2] = CHSV( ms / 45, 200, 255);
  leds[(k+i)/2] = CHSV( ms / 50, 200, 255);
  leds[(k+i+j)/3] = CHSV( ms / 55, 200, 255);
  
  FastLED.show();
}

void moveGradient() {
  EVERY_N_MILLISECONDS(gradMoveDelay) {
    uint8_t count = 0;
    fill_gradient (leds, gradStartPos, gradStartColor, gradLength, gradEndColor, SHORTEST_HUES);
/*    for (uint8_t i = gradStartPos; i < gradStartPos+gradLength; i++) {
      leds[i % NUM_LEDS] = leds[count];
      count++;
    }
    */
    FastLED.show();  // Display the pixels.
    FastLED.clear();  // Clear the strip to not leave behind lit pixels as grad moves.
    /*gradStartPos = gradStartPos + gradDelta;  // Update start position.
    if ( (gradStartPos > NUM_LEDS-1) || (gradStartPos < 0) ) {  // Check if outside NUM_LEDS range
      gradStartPos = gradStartPos % NUM_LEDS;  // Loop around as needed
    }*/
  }
}

void heartBeat(){
  for (int i = 0; i < NUM_LEDS ; i++) {
    uint8_t bloodVal = sumPulse( (5/NUM_LEDS/2) + (NUM_LEDS/2) * i * flowDirection );
    leds[i] = CHSV( bloodHue, bloodSat, bloodVal );
  }
  FastLED.show();
}
int sumPulse(int time_shift) {
  EVERY_N_MILLISECONDS(3000) {
    bloodHue = bloodHue+5;
    uint8_t prev_time_shift = time_shift;
    printf("entered every 'n' milliseconds\n\r");
    if(prev_time_shift < 1){
      time_shift = (5/NUM_LEDS/2) + (NUM_LEDS/2) * flowDirection;
      printf("time shift = something\n\r");
    }
    else time_shift = 0;
    printf("time shift = 0\n\r");
  }
  //time_shift = 0;  //Uncomment to heart beat/pulse all LEDs together
  int pulse1 = pulseWave8( millis() + time_shift, cycleLength, pulseLength );
  int pulse2 = pulseWave8( millis() + time_shift + pulseOffset, cycleLength, pulseLength );
  return qadd8( pulse1, pulse2 );  // Add pulses together without overflow
}

uint8_t pulseWave8(uint32_t ms, uint16_t cycleLength, uint16_t pulseLength) {
  uint16_t T = ms % cycleLength;
  if ( T > pulseLength) return baseBrightness;
  uint16_t halfPulse = pulseLength / 2;
  if (T <= halfPulse ) {
    return (T * 255) / halfPulse;  //first half = going up
  } else {
    return((pulseLength - T) * 255) / halfPulse;  //second half = going down
  }
}

void solidFill() {
  fill_solid( leds, NUM_LEDS, CHSV(gHue,255,255));
  FastLED.show();
  EVERY_N_MILLISECONDS(100) {
    gHue+=1;
  }  
}


//Main loop
void loop() {
  if ( radio.available() ) {// if there is data ready
    while (radio.available()) {// Dump the payloads until we've gotten everything
      radio.read( &sceneToRun, num_button_pins );// Fetch the payload, and see if this was the last one.
      printf("Got buttons\n\r"); // Spew it
      activeScene = sceneToRun;
      printf("Received: %d\n\r", activeScene);
      lastButtonPress = millis();
      gHue = gHueMaster;
      bloodHue = bloodHueMaster;
      
      }
  }
  else {
  // run case/switch to determine which loop to run
    switch(activeScene) {
      case 1: solidFill();
      break;
      case 2: blinkPulse();
      break;
      case 3: rainbowRandomGlitter();
      break;
      case 4: finale();
      break;
      case 5: confetti();
      break;
      case 6: sparkle(0xff, 0xff, 0xff, 0);
      break;
      case 7: heartBeat();
      break;
      case 8: redAlert();//sparkle(0xff,0,0,0);
      break;
      case 9: whiteLowPower();
      break;
      case 10: solidFill();
      break;
      
      /*
      case 1: blinkPulse();
      break;
      case 2: confetti();
      break;
      case 3: sparkle(0xff, 0xff, 0xff, 0);
      break;
      case 4: twinkle(0xff, 0, 0, 10, 100, false);
      break;*/
    }
  }
}

Relay Receiver

//Gravy Relay Receiver
/*v1.0  Created to plug into wired switch on fog machine and relay a button press to turn on a fog burst****/
/********Copied From Receiver v1.2*******
 * [legacy]v1.1 - instituted power compensation to keep small batteries from auto-shutoff. With nothing connected it 
 * seems like there need to be more than 6 LEDs lit, full white. That number depends on what other LEDs are 
 * connected, even if not in use. 10 lit seems safe, haven't measured lifespan of battery yet...
 * [legacy]v1.2 - updated patterns, final version for Sitka!!
 */
#include <SPI.h>
#include "nRF24L01.h"
#include "RF24.h"
#include "printf.h"
#include "FastLED.h"
#define RELAY1 3

//PowerComp
#define BRIGHTNESS  100
#define LED_TYPE    WS2811
#define COLOR_ORDER GRB
#define POWERCOMP_DATA_PIN    5
#define POWERCOMP_NUM_LEDS    10  //seems to need more than 6 LEDs hooked up when 10 are connected. 
//Went with 10 lit up, seemed safe. It's possible that with more LEDs onnected to the system, 
//less than 10 lit would work... possibly down to just a few. 
CRGB powercomp_leds[POWERCOMP_NUM_LEDS];

RF24 radio(9,10); //Set up radio
const uint64_t pipe = 0xE8E8F0F0E1LL;
int sceneToRun = 0;
int activeScene = 0;
const uint8_t num_button_pins = 9;
volatile long lastButtonPress = 0;
volatile long lastFog = 0;
long interval = 2000;

void setup()
{
  pinMode(RELAY1, OUTPUT);
  Serial.begin(115200);
  printf_begin();
  printf("\n\rGravy Relay Receiver\n\r");
  radio.begin();
  radio.openReadingPipe(1,pipe);
  radio.startListening();
  radio.printDetails();

  //PowerComp
  FastLED.addLeds<NEOPIXEL, POWERCOMP_DATA_PIN>(powercomp_leds, POWERCOMP_NUM_LEDS).setCorrection(TypicalLEDStrip);
  FastLED.setBrightness(BRIGHTNESS);
  fill_solid(powercomp_leds, POWERCOMP_NUM_LEDS, CRGB::White);
  FastLED.show();
}

void fogOn(){
   digitalWrite(RELAY1,LOW);           // Turns ON Relays 1
   lastFog = millis();
   printf("fogOn ran \n\r");
   activeScene = 2;
}

void fogOff(){
  digitalWrite(RELAY1,HIGH);          // Turns Relay Off
  printf("fogOff ran \n\r");
}

void loop()
{
  unsigned long currentMillis = millis();
  EVERY_N_MILLISECONDS(3000) {
    if ( currentMillis - lastFog > interval) {
      fogOff();
    }
  }
  
  if ( radio.available() ) {// if there is data ready
    while (radio.available()) {// Dump the payloads until we've gotten everything
      radio.read( &sceneToRun, num_button_pins );// Fetch the payload, and see if this was the last one.
      printf("Got buttons\n\r"); // Spew it
      activeScene = sceneToRun;
      printf("Received: %d\n\r", activeScene);
      lastButtonPress = millis();      
      }
    }
  else {
  // run case/switch to determine which loop to run
    switch(activeScene) {
      case 1:
      break;
      case 2:
      break;
      case 3: 
      break;
      case 4: 
      break;
      case 5: 
      break;
      case 6: 
      break;
      case 7: 
      break;
      case 8: 
      break;
      case 9: 
      break;
      case 10:  fogOn();
    }
  }
}

For details on the footswitch, see the Costumes write-up.

Tags:

Updated: