Parts:
Pro Micro compatible (described as Arduino Leonardo equivalent in smaller package)
RGB LED, common anode (from Adafruit)
grounded dc power supply (negative output must connect to earth ground)
(3) 330 ohm resistors
(3) 5 megaohm resistors
(3) soda can bottoms used as sensors (aluminum or copper foil, or any other conductive object would do)
ping pong ball (as diffuser for LED)
3D printed housing
Theory:
I don't really understand how capacitive sensing works, but it's really fun! This project is based heavily on the Arduino Capacitive Sensing Library which can be found here. Essentially each sensor (soda can) connects directly to an input and also through a high value resistor to an output. The output is switched and then the time it takes for the input to change states is recorded. That amount of time varies based on the capacitance of the sensor (it forms an RC circuit along with the resistor) and that time measurement is the output.
For this project, each of the three sensors drives one of the three colors of the LED such that you can create any color/brightness combination by moving your hands around the sensors. I originally imagined it would be like moving your hands around a crystal ball and seeing cool light effects, but the geometry and sensing ranges didn't work out right for that.
Description:
Connect power to Arduino. Connect 3 PWM outputs through suitable resistors to LED cathodes. Connect anode to positive voltage. Connect 3 sensor input pins to conductive object (in this case, soda can bottoms) and also to high value resistors which then connect to the corresponding sensor output pins. Arrange the sensors such that you can trigger each one individually or reach to overlap more than one at a time. Then power it up and wait for the initial flashing light sequence to end (this is a self-calibration procedure.) After that, wave your hands near the sensors and watch the light change.
I can't really describe the effect as well as video shows it, so here are a few videos. The second one with low exposure shows the colors much better, and the third has a brighter exposure that washes out the colors but shows hand movement better:
https://www.youtube.com/embed/GmMkZciW_Ec
https://www.youtube.com/embed/tO5m1a98xDo
Questions I'd love to have answered:
If it's left on for an extended period, it has a tendency to start responding as if it's detecting proximity even when there's nothing there. The problem was much worse before I tried an experimental fix based on guesses and whims. It's much improved now, but still acts funny sometimes. Turning power off and right back on fixes it every time.
The "fix" that I put in is this: Instead of reading each of the three sensors in sequence and updating the LED as fast as possible, the code waits for a defined period (25ms) between cycles. That change alone helped a little. The next change was setting the three inputs to outputs and forcing them low (grounding them out) during the idle time on each cycle. I had a vague idea that maybe charge was building up from the cycling and not dissipating entirely before the next cycle, altering subsequent readings. Grounding each input between each cycle seems to have made a big difference.
Does this make ANY sense? Or does the code solution make sense, but for entirely different reasons than I was thinking? Any insights into this aspect of the project would be greatly appreciated. As it stands, I'm pretty satisfied already with how it works now, but I'd still love to better understand what's going on!
Code (Arduino IDE)
3D models for the housing:
Color-Theremin_3D-models.zip
The housing has mounting holes patterned for an Adafruit "Perma-Proto" half-sized breadboard, has a cable entry area with room for zip-tie strain relief, and an opening for a nice big rocker switch.
Pro Micro compatible (described as Arduino Leonardo equivalent in smaller package)
RGB LED, common anode (from Adafruit)
grounded dc power supply (negative output must connect to earth ground)
(3) 330 ohm resistors
(3) 5 megaohm resistors
(3) soda can bottoms used as sensors (aluminum or copper foil, or any other conductive object would do)
ping pong ball (as diffuser for LED)
3D printed housing
Theory:
I don't really understand how capacitive sensing works, but it's really fun! This project is based heavily on the Arduino Capacitive Sensing Library which can be found here. Essentially each sensor (soda can) connects directly to an input and also through a high value resistor to an output. The output is switched and then the time it takes for the input to change states is recorded. That amount of time varies based on the capacitance of the sensor (it forms an RC circuit along with the resistor) and that time measurement is the output.
For this project, each of the three sensors drives one of the three colors of the LED such that you can create any color/brightness combination by moving your hands around the sensors. I originally imagined it would be like moving your hands around a crystal ball and seeing cool light effects, but the geometry and sensing ranges didn't work out right for that.
Description:
Connect power to Arduino. Connect 3 PWM outputs through suitable resistors to LED cathodes. Connect anode to positive voltage. Connect 3 sensor input pins to conductive object (in this case, soda can bottoms) and also to high value resistors which then connect to the corresponding sensor output pins. Arrange the sensors such that you can trigger each one individually or reach to overlap more than one at a time. Then power it up and wait for the initial flashing light sequence to end (this is a self-calibration procedure.) After that, wave your hands near the sensors and watch the light change.
I can't really describe the effect as well as video shows it, so here are a few videos. The second one with low exposure shows the colors much better, and the third has a brighter exposure that washes out the colors but shows hand movement better:
https://www.youtube.com/embed/GmMkZciW_Ec
https://www.youtube.com/embed/tO5m1a98xDo
Questions I'd love to have answered:
If it's left on for an extended period, it has a tendency to start responding as if it's detecting proximity even when there's nothing there. The problem was much worse before I tried an experimental fix based on guesses and whims. It's much improved now, but still acts funny sometimes. Turning power off and right back on fixes it every time.
The "fix" that I put in is this: Instead of reading each of the three sensors in sequence and updating the LED as fast as possible, the code waits for a defined period (25ms) between cycles. That change alone helped a little. The next change was setting the three inputs to outputs and forcing them low (grounding them out) during the idle time on each cycle. I had a vague idea that maybe charge was building up from the cycling and not dissipating entirely before the next cycle, altering subsequent readings. Grounding each input between each cycle seems to have made a big difference.
Does this make ANY sense? Or does the code solution make sense, but for entirely different reasons than I was thinking? Any insights into this aspect of the project would be greatly appreciated. As it stands, I'm pretty satisfied already with how it works now, but I'd still love to better understand what's going on!
Code (Arduino IDE)
Code:
/*
*** Color Theremin ***
*** by Eric Schaefer ***
*** 2015 ***
Added experimental step in between each set of sensor readings. During the extra step time,
each input is switched to an output and forced LOW to dissipate any charge that may have accumulated.
Then the input is switched back to input mode before the next sensing cycle.
This seems to stop the apparent build up of higher and higher readings on a given sensor that
would sometimes happen, especially after directly touching one of the sensors.
To maximize your odds of getting a good sensor calibration, keep hands well away from sensors until
after the LEDs stop their initial sine wave patterns during the boot sequence.
After installing in custom enclosure, one sensor suddenly became more sensitive than others.
I tried re-routing wires to no avail and eventually just added a 1.4x scaling factor in the
calibration function for input #2. It's an awkward fix that I'm not proud of, but the results are great!
*/
/*
* This Sketch uses parts of "CapitiveSense Library Demo Sketch"
* Paul Badger 2008
* Uses a high value resistor e.g. 10M between send pin and receive pin
* Resistor effects sensitivity, experiment with values, 50K - 50M. Larger resistor values yield larger sensor values.
* Receive pin is the sensor pin - try different amounts of foil/metal on this pin
*/
// include the library code:
#include <CapacitiveSensor.h>
// These constants won't change:
const int cyclePeriod=25; // how long to wait between groups of cap sense readings (ms)
const int capSamples=25; // number of samples per capSense reading
const int smoothFactorCap=150; // smoothing factor in tenths (i.e. 10=1.0, 25=2.5)
const int threshCalPercent=200;// multiply average resting calibration level by this percentage to find threshold sensor value at which LED activates
const int maxCalPercent=225; // multiply threshold level by this percentage to find sensor value which delivers max LED output
const int bootWaitTime=4000; // time in ms to wait on fresh reboot before starting calibration
const int bootCalibrateTime=3000; // time in ms to collect calibration data on reboot, after wait time
const byte pinRed=5; // Arduino pin PWM output numbers
const byte pinGreen=6; // Arduino pin PWM output numbers
const byte pinBlue=9; // Arduino pin PWM output numbers
const byte pinCap1out=2; // capSense pin assignments
const byte pinCap1sense=3; // capSense pin assignments
const byte pinCap2out=14; // capSense pin assignments
const byte pinCap2sense=15; // capSense pin assignments
const byte pinCap3out=16; // capSense pin assignments
const byte pinCap3sense=10; // capSense pin assignments
CapacitiveSensor cs_1 = CapacitiveSensor(pinCap1out,pinCap1sense); // 5Mohm resistor between pins, second pin is sensor pin, add a wire and or foil if desired
CapacitiveSensor cs_2 = CapacitiveSensor(pinCap2out,pinCap2sense); // 5Mohm resistor between pins, second pin is sensor pin, add a wire and or foil if desired
CapacitiveSensor cs_3 = CapacitiveSensor(pinCap3out,pinCap3sense); // 5Mohm resistor between pins, second pin is sensor pin, add a wire and or foil if desired
int capThresh1=0; // lower limit for capSense reading to trigger any output
int capMax1=0; // upper value for capSense reading - this will be scaled to max output
int capThresh2=0; // lower limit for capSense reading to trigger any output
int capMax2=0; // upper value for capSense reading - this will be scaled to max output
int capThresh3=0; // lower limit for capSense reading to trigger any output
int capMax3=0; // upper value for capSense reading - this will be scaled to max output
// the next four variables are just for the initial calibration at startup
int capCalCycles=0;
unsigned long capSum1=0;
unsigned long capSum2=0;
unsigned long capSum3=0;
unsigned long bootTime=0; // for recording start time of program
boolean freshBoot=1; // set to 1 until after initial calibration is complete
unsigned long cycleStart=0; // time (ms) that last capSense readings were taken
const int serialTime=1000; // cycle length for serial output refresh (milliseconds)
unsigned long serialLast=0; // time of last serial output
int dutyRed=0; // duty cycle (0-100%)
int dutyGreen=0; // duty cycle (0-100%)
int dutyBlue=0; // duty cycle (0-100%)
unsigned long timeNow=0; // current time reading
unsigned long lastdisp=0; // time(ms) when last dispay occurred
long capSense01=0; // value from capacitive sensing circuit
long capSenseAvg01=0; // averaged capSense value for smoothing purposes
long capSense02=0; // value from capacitive sensing circuit
long capSenseAvg02=0; // averaged capSense value for smoothing purposes
long capSense03=0; // value from capacitive sensing circuit
long capSenseAvg03=0; // averaged capSense value for smoothing purposes
// the setup routine runs once when you press reset:
void setup() {
cs_1.set_CS_AutocaL_Millis(0xFFFFFFFF); // turn off autocalibrate on channel 1
cs_2.set_CS_AutocaL_Millis(0xFFFFFFFF); // turn off autocalibrate on channel 2
cs_3.set_CS_AutocaL_Millis(0xFFFFFFFF); // turn off autocalibrate on channel 3
// cs_1.reset_CS_AutoCal(); // optional one-time calibration routine from capSense library
// cs_2.reset_CS_AutoCal(); // optional one-time calibration routine from capSense library
// cs_3.reset_CS_AutoCal(); // optional one-time calibration routine from capSense library
// initialize the digital pins as input and output, begin serial output.
// Serial.begin(9600); // start serial data connection for display on computer
pinMode(pinRed, OUTPUT);
pinMode(pinGreen, OUTPUT);
pinMode(pinBlue, OUTPUT);
digitalWrite(pinRed, HIGH);
digitalWrite(pinGreen, HIGH);
digitalWrite(pinBlue, HIGH);
bootTime=timeNow; // record time that main loop starts (used to determine calibration timing, etc.)
}
// ******************************************************************************************************
// ******************************************************************************************************
// ******************************************************************************************************
// ******************************************** Start of Main Loop *************************************
// ******************************************************************************************************
// ******************************************************************************************************
// ******************************************************************************************************
void loop() {
timeNow=millis(); // record current time reading for this cycle in millisecond
cycleStart=timeNow; // record current time reading as start of this cycle
capSense01=cs_1.capacitiveSensor(capSamples); // get a capacitive sensor reading
capSense02=cs_2.capacitiveSensor(capSamples); // get a capacitive sensor reading
capSense03=cs_3.capacitiveSensor(capSamples); // get a capacitive sensor reading
capSenseAvg01=(capSenseAvg01*(float(smoothFactorCap)/10)+capSense01)/(float(smoothFactorCap)/10+1); // smooths values
dutyRed = map(capSenseAvg01,capThresh1,capMax1,0,255); // scales values
dutyRed = constrain(dutyRed,0,255); // limits values to 0-255 range
capSenseAvg02=(capSenseAvg02*(float(smoothFactorCap)/10)+capSense02)/(float(smoothFactorCap)/10+1); // smooths values
dutyGreen = map(capSenseAvg02,capThresh2,capMax2,0,255); // scales values
dutyGreen = constrain(dutyGreen,0,255); // limits values to 0-255 range
capSenseAvg03=(capSenseAvg03*(float(smoothFactorCap)/10)+capSense03)/(float(smoothFactorCap)/10+1); // smooths values
dutyBlue = map(capSenseAvg03,capThresh3,capMax3,0,255); // scales values
dutyBlue = constrain(dutyBlue,0,255); // limits values to 0-255 range
if(freshBoot==1) calibrate(); // if we're on a fresh bootup, include light flashing and calibration routines
// With common-cathode wiring, setting outputs lower delivers more power (0=brightest, 255=off)
analogWrite(pinRed,255-dutyRed); // (pin #, duty cycle scaled from 0-255)
analogWrite(pinGreen,255-dutyGreen); // (pin #, duty cycle scaled from 0-255)
analogWrite(pinBlue,255-dutyBlue); // (pin #, duty cycle scaled from 0-255)
/*
if (timeNow>serialLast+serialTime){
// these lines print PWM duty cycle data and cycle time to the serial output
// capSense performance got much buggier when I used serial output and instantly improved when I dropped it :(
Serial.print (capThresh1);
Serial.print (", ");
Serial.print (capThresh2);
Serial.print (", ");
Serial.print (capThresh3);
Serial.print (". ");
Serial.print ("Red ");
Serial.print (capSenseAvg01);
Serial.print (" Green ");
Serial.print (capSenseAvg02);
Serial.print (" Blue ");
Serial.print (capSenseAvg03);
Serial.print (" Time (ms) per cycle ");
timeNow=millis();
Serial.println (timeNow-cycleStart);
serialLast=timeNow;
}
*/
pinGrounding(); // this function grounds all input pins until they're needed again, which seems to reduce noise and/or charge build-ups.
}
// ******************************************************************************************************
// ******************************************************************************************************
// ******************************************************************************************************
// ************************************************* End of Main Loop *********************************
// ******************************************************************************************************
// ******************************************************************************************************
// ******************************************************************************************************
void calibrate(){
// this routine creates a cycle of fun lighting effects while performing an initial calibration of sensitivity
dutyRed=sin(((timeNow-freshBoot)*0.007))*100+100; // cycles Red intensity with a sine wave
dutyGreen=sin(((timeNow-freshBoot)*0.007)+2.09)*100+100; // cycles Green intensity with a sine wave, offset ~120 degrees from Red
dutyBlue=sin(((timeNow-freshBoot)*0.007)+4.18)*100+100; // cycles Blue intensity with a sine wave, offset ~240 degrees from Red
if(timeNow>bootTime+bootWaitTime){ // if we're more than wait time into boot
if(timeNow<bootTime+bootWaitTime+bootCalibrateTime){ // if we're within calibration time (after wait time), gather capSense average data
capCalCycles+=1; // counts the number of calibration cycles
capSum1+=capSense01; // adds a running total of readings on sensor 1
capSum2+=capSense02; // adds a running total of readings on sensor 2
capSum3+=capSense03; // adds a running total of readings on sensor 3
}
else { // if more than 7 seconds has passed on first cycle, set threshholds based on captured average readings
freshBoot=0; // changes state so that boot/calibration routine won't get called again, even when millis() loops back around to 0
capThresh1=(capSum1/capCalCycles)*(float(threshCalPercent)/100); // sets threshold based on the average reading during calibration
capThresh2=(capSum2/capCalCycles)*(float(threshCalPercent)/100)*1.4; // sets threshold based on the average reading during calibration, includes extra scaling
capThresh3=(capSum3/capCalCycles)*(float(threshCalPercent)/100); // sets threshold based on the average reading during calibration
capMax1=capThresh1*(float(maxCalPercent)/100); // sets max based on the threshold value
capMax2=capThresh2*(float(maxCalPercent)/100); // sets max based on the threshold value
capMax3=capThresh3*(float(maxCalPercent)/100); // sets max based on the threshold value
}
}
}
void pinGrounding(){
// this routine grounds each capSense input pin until it's time to start new readings again
// this seems to help a lot with ever-growing accumulated capacitance readings that were building up
// on one or more pins, especially after physical contact with a sensor, prior to this change.
// switch relevant pins to outputs
pinMode (pinCap1sense, OUTPUT);
pinMode (pinCap2sense, OUTPUT);
pinMode (pinCap3sense, OUTPUT);
// set relevant pins low (grounded)
digitalWrite (pinCap1sense, LOW);
digitalWrite (pinCap2sense, LOW);
digitalWrite (pinCap3sense, LOW);
// wait until it's time for next cycle
while (timeNow<cycleStart+cyclePeriod) {
timeNow=millis();
}
// set all pins back to inputs for next round of readings
pinMode (pinCap1sense, INPUT);
pinMode (pinCap2sense, INPUT);
pinMode (pinCap3sense, INPUT);
}
Color-Theremin_3D-models.zip
The housing has mounting holes patterned for an Adafruit "Perma-Proto" half-sized breadboard, has a cable entry area with room for zip-tie strain relief, and an opening for a nice big rocker switch.
Attachments
-
108.8 KB Views: 82
Last edited: