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:
- 1x WS2812B strip (dense, for the ball): https://amzn.to/2wljngs
- Pulse sensor: https://amzn.to/2HWoAwE
- Wemos D1 mini: https://amzn.to/2JdrdL6
- Wemos D1 charging board: https://amzn.to/2JxZ1FY
- Battery, LiPo 3.7V 350mah – https://www.adafruit.com/product/2750
- 3D printed parts (all designs are on github)
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);
}
}



























