Project: "Color Theremin" capacitive sensing LED toy

Thread Starter

ebeowulf17

Joined Aug 12, 2014
3,307
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.

Color-Theremin_0955_small.jpg
Color-Theremin_bb.png

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);
}
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.
 

Attachments

Last edited:

Thread Starter

ebeowulf17

Joined Aug 12, 2014
3,307
Thanks!

I was wondering... I know multiple threads on the same subject are generally frowned upon, but it seems like not much Q&A or troubleshooting happens in this forum. If I wanted help understanding the issue I described above with runaway signals that I partially fixed without ever really understanding, would it be ok to start a thread in the projects forum on that question?

Thanks!
 

Thread Starter

ebeowulf17

Joined Aug 12, 2014
3,307
***EDIT: I accidentally did two separate write-ups on this project, a few years apart. This is the newer write-up and the two threads have now been merged. Oops!

Introduction
I built this project a few years ago as a present for my wife. The RGB LED in the middle of the housing is controlled by three capacitive sensors, one for each color. Moving your hand near a sensor increases the brightness of that color. With practice, you can make any color, at any brightness, and move fluidly between them with hand gestures.
IMG_8279_lo-res.jpg
I designed the housing and a friend ran it for me on his 3D printer - there were a few hitches along the way, which is why there are odd breaks and gaps, but for a variety of reasons we decided to just use it as is and not try re-printing.

The real joy of this project is making the colors dance with your hand movements, so here are a few videos:

Parts:
Code:
1 Microcontroller with PWM outputs (I used Arduino ProMicro)
1 5V power supply, with grounding
1 RGB LED (common anode)
3 resistors - 5Meg
3 resistors - 330R
1 breadboard or perf-board
3 foil, soda cans, etc. as capacitive sensor
1 (optional) ping pong ball to diffuse light
1 (optional) housing for sensors, electronics, and LED

Theory:

There are two main concepts at work in this project. The first is PWM control of LEDs for dimming purposes. The applications of PWM dimming are so well documented, it seems silly to recreate that work here, so here are a few links (if you want more info, google "PWM LED")
The second main concept is capacitive sensing. The basic idea here is that a human being can effectively act as one plate in a capacitor, with the other plate being a suitably large piece of conductive metal (scraps of aluminum foil work fine for experimenting.) The closer you are to the metal, the higher the value of the virtual capacitor you've created. The microcontroller measures that capacitance by changing the state of an output pin which is connected to the capacitor plate through a resistor, the measuring how long it takes for the capacitor plate, tied to an input pin, to climb up (or drop down) to the same state as the output pin. Closer proximity means higher capacitance, which means it takes longer to charge the cap to the same level, so the charging time is longer (read more about RC circuit timing here: https://en.wikipedia.org/wiki/RC_time_constant.) The not-so-obvious quirk in this system is that the circuit must be grounded in order for it to sense you. If the circuit is battery operated or powered through an isolated, floating power system, then the circuit voltages will float relative to your body and not respond to your proximity.
CapSense.gif
(this image borrowed from the Arduino website)
For a more thorough description of how this works, read the description from the author of library that's handling all of this:

Schematics and Code:
I never made a "proper" schematic for this project, but there's really nothing to it as far as circuit components - it's basically all handled by the microcontroller. I did make a sketch of the breadboard layout for testing in Fritzing, and I've shared that image and table listing the pinouts below:

Color-Theremin_bb.png Color-Theremin_pinout.png

Here's the code, written using the Arduino IDE, executed on an Arduino ProMicro (Leonardo compatible model.) In addition to the code below, you'll need to install the "CapSense" library (the version I used a few years ago is in an attached .ZIP file. You can also download the latest official versions from the Arduino website here: https://playground.arduino.cc/Main/CapacitiveSensor?from=Main.CapSense)
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.
*/

/*
* 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=65; // lower limit for capSense reading to trigger any output
int capMax1=130;   // upper value for capSense reading - this will be scaled to max output
int capThresh2=65; // lower limit for capSense reading to trigger any output
int capMax2=130;   // upper value for capSense reading - this will be scaled to max output
int capThresh3=65; // lower limit for capSense reading to trigger any output
int capMax3=130;   // 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
      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);
}
Assembly:
Putting this together is pretty straightforward. Follow the pinout diagrams to connect the 6 resistors. Connect the 5V line to your LED anode and the three LED resistors to the three LED cathodes. Solder (or otherwise make a good electrical connection with) your capacitive "sensors" (foil, soda cans, etc.) to wires leading back to your CapSense input pins.

When you connect a power supply to your device, make sure that the negative side of the power supply (GND on the Arduino) has a connection to Earth ground. It doesn't have to be perfect - this isn't a life and death connection here, but it needs to be solid enough for there to be something to "push" the capacitance "against" for lack of better words. If you're not Earth grounded, sensing performance will be erratic at best.

I'd recommend doing some very basic testing with the capsense library demo sketches to get a general idea how sensitive your capacitive elements are before trying to get the Color Theremin up and running. You may need to adjust some constants (threshCalPercent & maxCalPercent) at the beginning of the code or modify the calibration routine at the end of the code to get the response you want with different sensing elements.

Once you are ready to fire up this code for the first time, turn power on and then quickly move away from the sensors (I'm not sure exactly how far you need to be, but let's just say at least 12 inches.) When the machine powers up, it will do nothing for the first 4 seconds. Next it will run a self-calibration routine. This is because even small changes in local weather conditions will change the capacitance sensitivity quite a bit. The self-calibration greatly improves the odds that things will just work when it starts running... at least with my soda-can sensors. With different sensors the math in the calibration routine may need to be adjusted. While the calibration is running the LED will be cycling through colors. This provides a visual indication of when calibration is happening, and also serves as a diagnostic tool to see how the LED is working (for example, if blue lights during the calibration routine, but not when you're near blue's sensor, it's a sensor problem; if blue never lights, even during the calibration, it's an LED problem.)

Once the lights have stopped cycling on their own, the calibration routine is done. If the calibration has worked properly, the LED should be totally off now. If you move your hand towards a sensor, its corresponding color should light up, getting brighter as you get closer. Try this for all three colors - if they're all working, you're done!

IMG_8279.JPG IMG_8278.JPG IMG_8276.JPG
 

Attachments

Last edited:

Reloadron

Joined Jan 15, 2015
7,523
Thanks for posting this as a result of the other thread. While you may have posted this project previously it is new to me. :) I see it as a vehicle to maybe catch the interest of a 13 year old grandchild. Again, really pretty cool and thank you.

Ron
 

Thread Starter

ebeowulf17

Joined Aug 12, 2014
3,307
Thanks for posting this as a result of the other thread. While you may have posted this project previously it is new to me. :) I see it as a vehicle to maybe catch the interest of a 13 year old grandchild. Again, really pretty cool and thank you.

Ron
Sweet! I'm glad you like it, and I hope your grandson does too!
 

Thread Starter

ebeowulf17

Joined Aug 12, 2014
3,307
A recent discussion of PWM lighting reminded me of this old project a few days ago, and I thought I should share its details with people here. So, I spent a little time digging through my notes and doing a write-up on the project, only to realize shortly after posting that new one that I had already posted this a few years earlier. A very embarrassing mix-up!

Anyway, the new write-up included a better video, some better pictures, and a few more references to the theory behind stuff, so I thought I'd share those things here and maybe shut down the duplicate thread that I never should've started in the first place!

Here goes:






The image below, borrowed from the Arduino website (https://playground.arduino.cc/Main/CapacitiveSensor?from=Main.CapSense,) shows the basic concept of how a human effectively becomes part of a capacitor as far as the sensing circuit is concerned.


Here are a few links that help explain PWM control of LEDs for brightness and color-mixing applications:
https://www.arduino.cc/en/Tutorial/PWM
https://www.allaboutcircuits.com/projects/using-the-arduinos-analog-io/
https://learn.sparkfun.com/tutorials/pulse-width-modulation
 
Top