Introduction of the Rolling Light:

The Rolling Light, specifically the PulseSphere is a project I worked on during Spring 2018 quarter at GIX / UW as a part of fabrication / prototyping class. Here is the overview of the overall project:

I worked on the ball that interacts with people holding it and also interacts with a special tray designed for the ball by my classmate Ryan Wu.

Object Description

The ball has three modes of interaction :

  • Lonely (no interaction with objects or people)
  • Human touch (human is holding it)
  • Tray interaction

For each of those three states, there are different lighting patterns displayed depending on the state.

When the ball is held, it can detect the heart beat of the person holding it and pulsate red lights according to the heart beat.

When it’s laying on a table or flat surface by itself, it glows in subtle blue color

When the ball is placed in a placeholder spot on the tray, it glows with all colors of the rainbow, moving the colors around.


Sketch Exploration


Implementation

The ball is using WeMos D1 Mini Lite as the Arduino-compatible microcontroller.

The ball uses one sensor and one button to switch between various states.

The Heart Rate sensor inserted on the bottom of the ball reads the heart rate via infrared pulses and infrared receiver.

When the heart rate of a human is detected (approx 45 – 130BPM), the ball assumes that a person is holding it, therefore activating a red pulsating light pattern. When the heart rate detected is out of this range, the ball switches to “standby” mode and slowly glows in blue (a “breathing”-like pattern needs to be implemented).

When the ball is placed onto the tray in a specific placeholder, a pin protruding from the placeholder pushes a button in a cavity in the ball, switching the ball to another state and activating rainbow pattern.

Hardware:

Fabrication

 

Reflections on the fabrication process:

  • FormLabs and DFM printers have vastly different tolerances and side effects
  • It is extremely easy to sand 3D printed parts to make them feel like a (literally) polished product
  • Making things screw into each other is an art
  • Half of the time on this project was spent removing the supports (SLA, PLA, PVA)
  • Painting the ball was not pursued because the resulting object was very unappealing, versus the polished PLA print that feels amazing in your hand
  • Choosing a button was one of the hardest things and required me to go to the electronics store multiple times, often without any luck

Schematics and Layouts

The 2D and 3D files are available at https://github.com/msurguy/rolling-light/tree/master/pulseSphere

The schematic of the ball circuit is presented below:


Software

The software is available at https://github.com/msurguy/rolling-light/tree/master/pulseSphere/code

Please see the comments in the code for the detailed description of how the software works


The Code

The code for the ball is below:

// Created by Maksim Surguy, Spring 2018

/*

   This code runs on WeMos D1 mini board with a pulse sensor connected to pin A0, 9 Neopixel LEDs
      connected to pin D2, a button connected to pin D3.

   The objective of this code is to read human heart rate and to pulsate lights according to the
      heart rate
   When the ball is not held, it simply stays blue

 */

#include <Arduino.h>
#include <Adafruit_NeoPixel.h>
#include <Ticker.h>

Ticker flipper;

//  VARIABLES
#define LEDCOUNT 9

int inputPin = D3; // pushbutton connected to digital pin D3
// int input2Pin = D10;               // pushbutton connected to digital pin D3
int buttonVal = 1; // variable to store the read value

int fadeRate = 0;  // used to fade LED on with PWM on fadePin

// these variables are volatile because they are used during the interrupt
// service routine!
volatile int BPM;                  // used to hold the pulse rate
volatile int Signal;               // holds the incoming raw data
volatile int IBI = 600;            // holds the time between beats, must be
                                   // seeded!
volatile boolean Pulse = false;    // true when pulse wave is high, false when
                                   // it's low
volatile boolean QS = false;       // becomes true when Arduoino finds a beat.

int rate[10];                      // array to hold last ten IBI values
unsigned long sampleCounter = 0;   // used to determine pulse timing
unsigned long lastBeatTime  = 0;   // used to find IBI
int P                       = 512; // used to find peak in pulse wave, seeded
int T                       = 512; // used to find trough in pulse wave,
                                   // seeded
int thresh = 512;                  // used to find instant moment of heart
                                   // beat, seeded
int amp = 100;                     // used to hold amplitude of pulse
                                   // waveform, seeded
boolean firstBeat = true;          // used to seed rate array so we startup
                                   // with reasonable BPM
boolean secondBeat = false;        // used to seed rate array so we startup
                                   // with reasonable BPM

// Set up the LED strip
Adafruit_NeoPixel strip = Adafruit_NeoPixel(LEDCOUNT, D2, NEO_GRB + NEO_KHZ800);

int i;

// THIS IS THE TICKER INTERRUPT SERVICE ROUTINE.
// Ticker makes sure that we take a reading every 2 miliseconds
void ISRTr() {                                    // triggered when flipper
                                                  // fires....
  cli();                                          // disable interrupts while we
                                                  // do this
  Signal         = analogRead(A0);                // read the Pulse Sensor
  sampleCounter += 2;                             // keep track of the time in
                                                  // mS with this variable
  int N = sampleCounter - lastBeatTime;           // monitor the time since the
                                                  // last beat to avoid noise

  //  find the peak and trough of the pulse wave
  if ((Signal < thresh) && (N > (IBI / 5) * 3)) { // avoid dichrotic noise by
                                                  // waiting 3/5 of last IBI
    if (Signal < T) {                             // T is the trough
      T = Signal;                                 // keep track of lowest point
                                                  // in pulse wave
    }
  }

  if ((Signal > thresh) && (Signal > P)) { // thresh condition helps avoid noise
    P = Signal;                            // P is the peak
  }                                        // keep track of highest point in
                                           // pulse wave

  //  NOW IT'S TIME TO LOOK FOR THE HEART BEAT
  // signal surges up in value every time there is a pulse
  if (N > 250) {                          // avoid high frequency noise
    if ((Signal > thresh) && (Pulse == false) && (N > (IBI / 5) * 3)) {
      Pulse = true;                       // set the Pulse flag when we
                                          // think there is a pulse
      // digitalWrite(blinkPin, HIGH);       // turn on pin 13 LED
      IBI = sampleCounter - lastBeatTime; // measure time between beats
                                          // in mS
      lastBeatTime = sampleCounter;       // keep track of time for
                                          // next pulse

      if (secondBeat) {                   // if this is the second
                                          // beat, if secondBeat ==
                                          // TRUE
        secondBeat = false;               // clear secondBeat flag

        for (int i = 0; i <= 9; i++) {    // seed the running total to
                                          // get a realisitic BPM at
                                          // startup
          rate[i] = IBI;
        }
      }

      if (firstBeat) {      // if it's the first time we found a beat, if
                            // firstBeat == TRUE
        firstBeat  = false; // clear firstBeat flag
        secondBeat = true;  // set the second beat flag
        sei();              // enable interrupts again
        return;             // IBI value is unreliable so discard it
      }


      // keep a running total of the last 10 IBI values
      word runningTotal = 0;                // clear the runningTotal variable

      for (int i = 0; i <= 8; i++) {        // shift data in the rate array
        rate[i]       = rate[i + 1];        // and drop the oldest IBI value
        runningTotal += rate[i];            // add up the 9 oldest IBI values
      }

      rate[9] = IBI;                        // add the latest IBI to the rate
                                            // array
      runningTotal += rate[9];              // add the latest IBI to
                                            // runningTotal
      runningTotal /= 10;                   // average the last 10 IBI values
      BPM           = 60000 / runningTotal; // how many beats can fit into a
                                            // minute? that's BPM!
      QS = true;                            // set Quantified Self flag
      // QS FLAG IS NOT CLEARED INSIDE THIS ISR
    }
  }

  if ((Signal < thresh) && (Pulse == true)) { // when the values are going down,
                                              // the beat is over
    // digitalWrite(blinkPin, LOW);              // turn off pin 13 LED
    Pulse = false;                            // reset the Pulse flag so we can
                                              // do it again
    amp    = P - T;                           // get amplitude of the pulse wave
    thresh = amp / 2 + T;                     // set thresh at 50% of the
                                              // amplitude
    P = thresh;                               // reset these for next time
    T = thresh;
  }

  if (N > 2500) {                 // if 2.5 seconds go by without a beat
    thresh       = 512;           // set thresh default
    P            = 512;           // set P default
    T            = 512;           // set T default
    lastBeatTime = sampleCounter; // bring the lastBeatTime up to date
    firstBeat    = true;          // set these to avoid noise
    secondBeat   = false;         // when we get the heartbeat back
  }

  sei();                          // enable interrupts when youre done!
}// end isr

void interruptSetup() {
  // Initializes Ticker to have flipper run the ISR to sample every 2mS as per
  // original Sketch.
  flipper.attach_ms(2, ISRTr);
}

void setup() {
  Serial.begin(115200);
  pinMode(inputPin, INPUT); // set pin as input

  strip.setBrightness(255);
  strip.begin();

  // Play initialization animation
  for (int j = 0; j < 5; j++) {
    for (int i = 0; i < 9; i++) {
      // pixels.Color takes RGB values, from 0,0,0 up to 255,255,255
      strip.setPixelColor(i, strip.Color(0, 0, 255));
      strip.show();
      delay(100);
    }
  }

  interruptSetup(); // sets up to read Pulse Sensor signal every 2mS
}

/* Returns a hexadecimal value of color wheel, based on input value ranging from
   0 to 255 */
uint32_t Wheel(byte WheelPos) {
  WheelPos = 255 - WheelPos;

  if (WheelPos < 85) {
    return strip.Color(255 - WheelPos * 3, 0, WheelPos * 3);
  }

  if (WheelPos < 170) {
    WheelPos -= 85;
    return strip.Color(0, WheelPos * 3, 255 - WheelPos * 3);
  }
  WheelPos -= 170;
  return strip.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
}

// Slightly different, this makes the rainbow equally distributed throughout the strip
void rainbowCycle(uint8_t wait) {
  uint16_t i, j;

  for (j = 0; j < 256; j++) { // 5 cycles of all colors on wheel
    for (i = 0; i < strip.numPixels(); i++) {
      strip.setPixelColor(i, Wheel(((i * 256 / strip.numPixels()) + j) & 255));
    }
    strip.show();
    delay(wait);
  }
}

// After the heart beat is detected, dim the lights
void ledFadeToBeat() {
  fadeRate -= 0.25; //  set LED fade value

  for (int i = 0; i < 9; i++) {
    // strip.Color takes RGB values, from 0,0,0 up to 255,255,255
    // Let's map the fadeRate to some shade of red
    strip.setPixelColor(i, strip.Color(map(fadeRate, 0, 1024, 60, 255), 0, 0));
  }
  strip.show();
}

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

  buttonVal = digitalRead(inputPin); // read the button input pin

  // Sensor data
  if (QS == true) {                  // Quantified Self flag is true when arduino finds a heartbeat
    fadeRate = 1024;

    // Uncomment this line if want to see heart rate in beats per minute
    // Serial.println(BPM);
    QS = false; // reset the Quantified Self flag
    // for next time
  }

  // When button is not pressed, we are showing the heart rate or default animation
  if (buttonVal == 1) {
    if ((BPM > 50) && (BPM < 140)) {
      ledFadeToBeat();
    } else {
      for (int i = 0; i < 9; i++) {
        // for now set the whole strip as blue
        // TODO: animate into slow breathing pattern
        strip.setPixelColor(i, strip.Color(0, 0, 255));
      }
      strip.show();
    }
  }

  // When button on the ball is pressed, make a rainbow. Wohoo!
  if (buttonVal == 0) {
    rainbowCycle(10);
  }
}