Project: "Color Theremin" capacitive sensing LED toy

Discussion in 'The Completed Projects Collection' started by ebeowulf17, Feb 28, 2015.

  1. ebeowulf17

    Thread Starter Active Member

    Aug 12, 2014
    678
    79
    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 two videos, one with low exposure that shows the colors much better, and the other with 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 (Text):
    1. /*
    2. ***  Color Theremin  ***
    3. *** by Eric Schaefer ***
    4. ***       2015       ***
    5.  
    6. Added experimental step in between each set of sensor readings.  During the extra step time,
    7. each input is switched to an output and forced LOW to dissipate any charge that may have accumulated.
    8. Then the input is switched back to input mode before the next sensing cycle.
    9. This seems to stop the apparent build up of higher and higher readings on a given sensor that
    10. would sometimes happen, especially after directly touching one of the sensors.
    11. To maximize your odds of getting a good sensor calibration, keep hands well away from sensors until
    12. after the LEDs stop their initial sine wave patterns during the boot sequence.
    13. After installing in custom enclosure, one sensor suddenly became more sensitive than others.
    14. I tried re-routing wires to no avail and eventually just added a 1.4x scaling factor in the
    15. calibration function for input #2.  It's an awkward fix that I'm not proud of, but the results are great!
    16. */
    17.  
    18. /*
    19. * This Sketch uses parts of "CapitiveSense Library Demo Sketch"
    20. * Paul Badger 2008
    21. * Uses a high value resistor e.g. 10M between send pin and receive pin
    22. * Resistor effects sensitivity, experiment with values, 50K - 50M. Larger resistor values yield larger sensor values.
    23. * Receive pin is the sensor pin - try different amounts of foil/metal on this pin
    24. */
    25.  
    26. // include the library code:
    27.  
    28. #include <CapacitiveSensor.h>
    29.  
    30.  
    31. // These constants won't change:
    32. const int cyclePeriod=25;      // how long to wait between groups of cap sense readings (ms)
    33. const int capSamples=25;       // number of samples per capSense reading
    34. const int smoothFactorCap=150; // smoothing factor in tenths (i.e. 10=1.0, 25=2.5)
    35.  
    36. const int threshCalPercent=200;// multiply average resting calibration level by this percentage to find threshold sensor value at which LED activates
    37. const int maxCalPercent=225;   // multiply threshold level by this percentage to find sensor value which delivers max LED output
    38.  
    39. const int bootWaitTime=4000;      // time in ms to wait on fresh reboot before starting calibration
    40. const int bootCalibrateTime=3000; // time in ms to collect calibration data on reboot, after wait time
    41.  
    42. const byte pinRed=5;        // Arduino pin PWM output numbers
    43. const byte pinGreen=6;      // Arduino pin PWM output numbers
    44. const byte pinBlue=9;       // Arduino pin PWM output numbers
    45.  
    46. const byte pinCap1out=2;    // capSense pin assignments
    47. const byte pinCap1sense=3;  // capSense pin assignments
    48. const byte pinCap2out=14;   // capSense pin assignments
    49. const byte pinCap2sense=15; // capSense pin assignments
    50. const byte pinCap3out=16;   // capSense pin assignments
    51. const byte pinCap3sense=10; // capSense pin assignments
    52.  
    53. CapacitiveSensor   cs_1 = CapacitiveSensor(pinCap1out,pinCap1sense);        // 5Mohm resistor between pins, second pin is sensor pin, add a wire and or foil if desired
    54. CapacitiveSensor   cs_2 = CapacitiveSensor(pinCap2out,pinCap2sense);        // 5Mohm resistor between pins, second pin is sensor pin, add a wire and or foil if desired
    55. CapacitiveSensor   cs_3 = CapacitiveSensor(pinCap3out,pinCap3sense);        // 5Mohm resistor between pins, second pin is sensor pin, add a wire and or foil if desired
    56.  
    57. int capThresh1=0; // lower limit for capSense reading to trigger any output
    58. int capMax1=0;    // upper value for capSense reading - this will be scaled to max output
    59. int capThresh2=0; // lower limit for capSense reading to trigger any output
    60. int capMax2=0;    // upper value for capSense reading - this will be scaled to max output
    61. int capThresh3=0; // lower limit for capSense reading to trigger any output
    62. int capMax3=0;    // upper value for capSense reading - this will be scaled to max output
    63.  
    64.  
    65. // the next four variables are just for the initial calibration at startup
    66. int capCalCycles=0;
    67. unsigned long capSum1=0;
    68. unsigned long capSum2=0;
    69. unsigned long capSum3=0;
    70.  
    71. unsigned long bootTime=0;   // for recording start time of program
    72. boolean freshBoot=1;        // set to 1 until after initial calibration is complete
    73.  
    74. unsigned long cycleStart=0; // time (ms) that last capSense readings were taken
    75.  
    76. const int serialTime=1000;  // cycle length for serial output refresh (milliseconds)
    77. unsigned long serialLast=0; // time of last serial output
    78.  
    79. int dutyRed=0;              // duty cycle (0-100%)
    80. int dutyGreen=0;            // duty cycle (0-100%)
    81. int dutyBlue=0;             // duty cycle (0-100%)
    82.  
    83. unsigned long timeNow=0;    // current time reading
    84. unsigned long lastdisp=0;   // time(ms) when last dispay occurred
    85.  
    86. long capSense01=0;          // value from capacitive sensing circuit
    87. long capSenseAvg01=0;       // averaged capSense value for smoothing purposes
    88. long capSense02=0;          // value from capacitive sensing circuit
    89. long capSenseAvg02=0;       // averaged capSense value for smoothing purposes
    90. long capSense03=0;          // value from capacitive sensing circuit
    91. long capSenseAvg03=0;       // averaged capSense value for smoothing purposes
    92.  
    93. // the setup routine runs once when you press reset:
    94. void setup() {
    95.   cs_1.set_CS_AutocaL_Millis(0xFFFFFFFF);     // turn off autocalibrate on channel 1
    96.   cs_2.set_CS_AutocaL_Millis(0xFFFFFFFF);     // turn off autocalibrate on channel 2
    97.   cs_3.set_CS_AutocaL_Millis(0xFFFFFFFF);     // turn off autocalibrate on channel 3
    98.  
    99.   // cs_1.reset_CS_AutoCal(); // optional one-time calibration routine from capSense library
    100.   // cs_2.reset_CS_AutoCal(); // optional one-time calibration routine from capSense library
    101.   // cs_3.reset_CS_AutoCal(); // optional one-time calibration routine from capSense library
    102.  
    103.   // initialize the digital pins as input and output, begin serial output.
    104.   // Serial.begin(9600);  // start serial data connection for display on computer
    105.   pinMode(pinRed, OUTPUT);
    106.   pinMode(pinGreen, OUTPUT);
    107.   pinMode(pinBlue, OUTPUT);
    108.  
    109.   digitalWrite(pinRed, HIGH);
    110.   digitalWrite(pinGreen, HIGH);
    111.   digitalWrite(pinBlue, HIGH);
    112.  
    113.   bootTime=timeNow; // record time that main loop starts (used to determine calibration timing, etc.)
    114.  
    115. }
    116.  
    117. // ******************************************************************************************************
    118. // ******************************************************************************************************
    119. // ******************************************************************************************************
    120. // ******************************************** Start of Main Loop  *************************************
    121. // ******************************************************************************************************
    122. // ******************************************************************************************************
    123. // ******************************************************************************************************
    124.  
    125. void loop() {
    126.   timeNow=millis(); // record current time reading for this cycle in millisecond
    127.   cycleStart=timeNow; // record current time reading as start of this cycle
    128.  
    129.   capSense01=cs_1.capacitiveSensor(capSamples); // get a capacitive sensor reading
    130.   capSense02=cs_2.capacitiveSensor(capSamples); // get a capacitive sensor reading
    131.   capSense03=cs_3.capacitiveSensor(capSamples); // get a capacitive sensor reading
    132.  
    133.   capSenseAvg01=(capSenseAvg01*(float(smoothFactorCap)/10)+capSense01)/(float(smoothFactorCap)/10+1); // smooths values
    134.   dutyRed = map(capSenseAvg01,capThresh1,capMax1,0,255);                                              // scales values
    135.   dutyRed = constrain(dutyRed,0,255);                                                                 // limits values to 0-255 range
    136.  
    137.   capSenseAvg02=(capSenseAvg02*(float(smoothFactorCap)/10)+capSense02)/(float(smoothFactorCap)/10+1); // smooths values
    138.   dutyGreen = map(capSenseAvg02,capThresh2,capMax2,0,255);                                            // scales values
    139.   dutyGreen = constrain(dutyGreen,0,255);                                                             // limits values to 0-255 range
    140.  
    141.   capSenseAvg03=(capSenseAvg03*(float(smoothFactorCap)/10)+capSense03)/(float(smoothFactorCap)/10+1); // smooths values
    142.   dutyBlue = map(capSenseAvg03,capThresh3,capMax3,0,255);                                             // scales values
    143.   dutyBlue = constrain(dutyBlue,0,255);                                                               // limits values to 0-255 range
    144.  
    145.   if(freshBoot==1) calibrate();        // if we're on a fresh bootup, include light flashing and calibration routines
    146.  
    147.   // With common-cathode wiring, setting outputs lower delivers more power (0=brightest, 255=off)
    148.   analogWrite(pinRed,255-dutyRed);     // (pin #, duty cycle scaled from 0-255)
    149.   analogWrite(pinGreen,255-dutyGreen); // (pin #, duty cycle scaled from 0-255)
    150.   analogWrite(pinBlue,255-dutyBlue);   // (pin #, duty cycle scaled from 0-255)
    151.  
    152.   /*
    153.   if (timeNow>serialLast+serialTime){
    154.    // these lines print PWM duty cycle data and cycle time to the serial output
    155.    // capSense performance got much buggier when I used serial output and instantly improved when I dropped it :(
    156.    Serial.print (capThresh1);
    157.    Serial.print (", ");
    158.    Serial.print (capThresh2);
    159.    Serial.print (", ");
    160.    Serial.print (capThresh3);
    161.    Serial.print (".   ");
    162.    Serial.print ("Red ");
    163.    Serial.print (capSenseAvg01);
    164.    Serial.print ("  Green ");
    165.    Serial.print (capSenseAvg02);
    166.    Serial.print ("  Blue ");
    167.    Serial.print (capSenseAvg03);
    168.    Serial.print ("  Time (ms) per cycle ");
    169.    timeNow=millis();
    170.    Serial.println (timeNow-cycleStart);
    171.    serialLast=timeNow;
    172.    }
    173.    */
    174.  
    175.   pinGrounding(); // this function grounds all input pins until they're needed again, which seems to reduce noise and/or charge build-ups.
    176.  
    177. }
    178.  
    179. // ******************************************************************************************************
    180. // ******************************************************************************************************
    181. // ******************************************************************************************************
    182. // *************************************************  End of Main Loop  *********************************
    183. // ******************************************************************************************************
    184. // ******************************************************************************************************
    185. // ******************************************************************************************************
    186.  
    187. void calibrate(){
    188.   // this routine creates a cycle of fun lighting effects while performing an initial calibration of sensitivity
    189.   dutyRed=sin(((timeNow-freshBoot)*0.007))*100+100;         // cycles Red intensity with a sine wave
    190.   dutyGreen=sin(((timeNow-freshBoot)*0.007)+2.09)*100+100;  // cycles Green intensity with a sine wave, offset ~120 degrees from Red
    191.   dutyBlue=sin(((timeNow-freshBoot)*0.007)+4.18)*100+100;   // cycles Blue intensity with a sine wave, offset ~240 degrees from Red
    192.  
    193.   if(timeNow>bootTime+bootWaitTime){                        // if we're more than wait time into boot
    194.     if(timeNow<bootTime+bootWaitTime+bootCalibrateTime){    // if we're within calibration time (after wait time), gather capSense average data
    195.       capCalCycles+=1;                                      // counts the number of calibration cycles
    196.       capSum1+=capSense01;                                  // adds a running total of readings on sensor 1
    197.       capSum2+=capSense02;                                  // adds a running total of readings on sensor 2
    198.       capSum3+=capSense03;                                  // adds a running total of readings on sensor 3
    199.  
    200.     }
    201.     else {          // if more than 7 seconds has passed on first cycle, set threshholds based on captured average readings
    202.       freshBoot=0;  // changes state so that boot/calibration routine won't get called again, even when millis() loops back around to 0
    203.       capThresh1=(capSum1/capCalCycles)*(float(threshCalPercent)/100);     // sets threshold based on the average reading during calibration
    204.       capThresh2=(capSum2/capCalCycles)*(float(threshCalPercent)/100)*1.4; // sets threshold based on the average reading during calibration, includes extra scaling
    205.       capThresh3=(capSum3/capCalCycles)*(float(threshCalPercent)/100);     // sets threshold based on the average reading during calibration
    206.       capMax1=capThresh1*(float(maxCalPercent)/100);                       // sets max based on the threshold value
    207.       capMax2=capThresh2*(float(maxCalPercent)/100);                       // sets max based on the threshold value
    208.       capMax3=capThresh3*(float(maxCalPercent)/100);                       // sets max based on the threshold value
    209.     }
    210.   }
    211. }
    212.  
    213. void pinGrounding(){
    214.   // this routine grounds each capSense input pin until it's time to start new readings again
    215.   // this seems to help a lot with ever-growing accumulated capacitance readings that were building up
    216.   // on one or more pins, especially after physical contact with a sensor, prior to this change.
    217.  
    218.   // switch relevant pins to outputs
    219.   pinMode (pinCap1sense, OUTPUT);
    220.   pinMode (pinCap2sense, OUTPUT);
    221.   pinMode (pinCap3sense, OUTPUT);
    222.  
    223.   // set relevant pins low (grounded)
    224.   digitalWrite (pinCap1sense, LOW);
    225.   digitalWrite (pinCap2sense, LOW);
    226.   digitalWrite (pinCap3sense, LOW);
    227.  
    228.   // wait until it's time for next cycle
    229.   while (timeNow<cycleStart+cyclePeriod) {
    230.     timeNow=millis();
    231.   }
    232.  
    233.   // set all pins back to inputs for next round of readings
    234.   pinMode (pinCap1sense, INPUT);
    235.   pinMode (pinCap2sense, INPUT);
    236.   pinMode (pinCap3sense, INPUT);
    237. }
    238.  
    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.
     
    Last edited: Feb 28, 2015
    darrough likes this.
  2. Wendy

    Moderator

    Mar 24, 2008
    20,766
    2,536
    Nice job! It has been added to the Index.
     
  3. ebeowulf17

    Thread Starter Active Member

    Aug 12, 2014
    678
    79
    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!
     
  4. Wendy

    Moderator

    Mar 24, 2008
    20,766
    2,536
    Yes, Asking questions and posting a project are different enough it would pass muster.
     
Loading...