I am happy to be able to show you my work again. This time I built a gesture-controlled Arduino lamp.
This project took me a lot of time, which I always have a shortage of.
The first version of the luminaire I built very quickly, and it worked fine on my breadboard, but when I brought it permanently into the enclosure, this is where it all started. After searching on the internet I’ve found lots of complaints about APDS9960 sensor problems, but no solutions for these problems. Little by little, I spent my time studying the datasheet of this sensor and understanding its functionality. But finally, it worked, and I could write a stable working code. So everything is in order.
Description of the Luminaire
The main emphasis in this luminaire I made not on the visualization and on the control of gestures. All other functions are secondary.
The lighting fixture is turned on by gesture to the left or right, as well as you can turn it on and a gesture to yourself, but it is not very convenient. Then with gestures to the left and right, you can flip through the lighting effects. If you start flipping to the right, the lamp will first change its colors from white to red, including all the primary colors and transitions between them.
If we flip to the left after turning on the lamp, we will see dynamic lighting effects such as “Fire”, “Matrix” and “Light”, “Matrix”, “Lava Lamp”, “Rainbow”, “Confetti”, “Sparks”, “Fire Lamp”. If you need to go back to a normal lamp, you can turn the lamp off with a gesture away from you and on with any of the three gestures instead of flipping.
Turning it off is done with a gesture away from you.
The brightness is adjusted by a gesture of approaching and moving away from the sensor. First, you bring your hand as close as possible to the sensor and then raise it sharply upwards. The luminaire goes into brightness adjustment mode. Bringing the palm closer and further away from the sensor, you need to find the appropriate brightness and fix your hand for a couple of seconds to store the brightness value. When the brightness is saved, the luminaire will let you know about it by smoothly turning off the light and then turning it on with a new brightness level.
I have not paid much attention to visual effects because I plan to make a second version of the Wemos D1 mini controller lamp. In which I plan to bring everything to perfection. For the same reason, for now, I only use one LED strip consisting of four WS2812B strips instead of four.
APDS9960 Interrupts
This is actually the main problem of the LED lamp. The interrupts in APDS9960 have a life of their own and can be triggered by anything, e.g., EM interference or a person near the sensor, pulsation of the power supply, and maybe something else.
At first, I tried to correct the problem by changing the hardware. Filtering of the power supply did not help. Even with a 18650 battery, the sensor still continued to live its life. I tried switching the INT signal terminating resistor to +5V. Also, it was no success.
As I studied it, I realized what causes false interrupt triggers. The main problem is the occasional reflection of the IR signal. The manufacturer recommends covering the sensor and everything around it with a black rubber coating. I don’t have that paint and didn’t bother with it. It could probably reduce the number of false positives a bit, though.
I wrote code that filters out all the random triggers but ran into another problem. When an interrupt is triggered, the gesture’s information is not transmitted on the I2C bus instantly but has a certain delay. And if the power supply due to the addressable LED strip is noisy, then the transfer time will always be different, taking into account these disturbances. I had to take this fact into account and rewrite the gesture detection code.
Gesture Detection
If you do not use the interrupt from the sensor in the code, then there is no problem with detecting the gesture. But in this case, we lose multitasking. If the lamp has a dynamic effect, then this requires a looping code, which is always executed all the time, and to get the controller out of the loop, we need to use interrupts.
In order to improve the stability of gesture recognition, the library also had to reduce the sensitivity of the receiver. To do this, in the file SparkFun_APDS9960.h replace the line:
if( ! setLEDBoost(LED_BOOST_300) )
to
if( !setLEDBoost(LED_BOOST_150) )
I chose experimentally a current of 50mA, which makes the gestures most stable, and I don’t need to burden the infrared LED with 100mA.
I also made the mistake of placing the microcontroller on the bottom of the lamp and the top cover’s gesture sensor. Because of this, I had to use 30 cm long wires to connect Arduino and apds9960. Which also added extra instability to the sensor. In the end, I overcame all these problems with the software.
Circuit
I use the charger from my smartphone to power the device, with an output voltage of 5V and a current of 2A. But in fact, the light with the white light on at maximum brightness does not consume more than 1.3A. For this reason, any power supply with an output current of at least 1.3A will do.
Any other voltage step-down converter can replace the dc-dc mini360 stabilizer with an output current of at least 150mA.
I do not use a logic level converter in the circuit below, but it is my personal decision. Therefore I take no responsibility in advance if you fail sensor APDS9960 connected without a TTL level converter.
Instead of the Arduino Nano, you can use the Arduino UNO controller or its clones.
Be sure to check the gesture sensor for jumpers. They are marked in red in the picture below. If they are missing, you should use a soldering iron to apply and heat solder to these contact tracks until a homogeneous drop is formed. Now the Chinese deliver these sensors without jumpers. Without these jumpers, the operation of the sensor is not guaranteed.
Sketch for the Arduino
The code is divided into two parts. The first part contains the basic code that works with the sensor, and the second part contains the code for all the light effects.
The first part:
#include <EEPROM.h> #include <avr/wdt.h> #include <Wire.h> #include <SparkFun_APDS9960.h> #include <Adafruit_NeoPixel.h> #define APDS9960_INT 3 // external interrupt pin #define PIN 4 // output to LED strip #define NUM_PIX 32 // number of LEDs in the strip #define effect_num 13 //total number of light effects in the cycle Adafruit_NeoPixel strip = Adafruit_NeoPixel(NUM_PIX, PIN, NEO_GRB + NEO_KHZ800); #define LED_PIN 13 // system LED for visual interrupt control #define PROX_INT_HIGH_P 25 //sensor threshold for interrupt triggering. #define PROX_INT_HIGH_G 60 //sensor threshold for interrupt triggering. #define PROX_INT_LOW 0 //no far interrupt #define range 0 // proximity sensor error margin 0 to 10 uint8_t old_proximity_data=0, proximity_data = 0, vol=255, effect_stat=0, tmp = 0; boolean out_effect=0; SparkFun_APDS9960 apds = SparkFun_APDS9960(); volatile uint8_t isr_flag = 0; //--------------------------------------------------------- void setup() { pinMode(LED_PIN, OUTPUT); //connect system LED pinMode(APDS9960_INT, INPUT); vol=EEPROM.read(0); // read brightness value from EEPROM strip.begin(); //initialize led strip set_color(0,0,0); //disable all leds wdt_enable (WDTO_8S); // connect watchdog timer enable_gesture(); // connect the gesture sensor } //***************Main loop********************************* //************************************************************ void loop() { if (isr_flag==1) { processingGesture(1); } if ( out_effect ) { out_effect=0; processingGesture(2); } if (isr_flag==2 ) { apds.clearProximityInt(); processingProximity(); } wdt_reset(); } //************************************************************* //*********************Gesture processing************************ void processingGesture(uint8_t int_out) { if(int_out==1 && check_ges()) int_out=2; if ( int_out==2 ) { switch ( tmp ) { case DIR_UP: { set_color(255,255,255); enable_gesture(); break; } case DIR_DOWN: { fade_off(); enable_gesture(); break;} case DIR_LEFT: { effect_stat++; effect_next(); break; } case DIR_RIGHT:{ effect_previous(); break; } case DIR_NEAR: { processingProximity(); enable_proximity(); break; } case DIR_FAR: { processingProximity(); enable_proximity(); break; } default: enable_gesture(); } } else { enable_gesture(); } } //********************************************** void effect_next() { if(effect_stat > effect_num) effect_stat=1; switch(effect_stat) { case 1: set_color(255,255,255); break; // // white case 2: set_color(255, 255, 0); break; // yellow case 3: set_color(0, 255, 0); break; // green case 4: set_color(0, 255, 255); break; // turquoise case 5: set_color(0, 0, 255); break; //blue case 6: set_color(255, 0, 255); break; //violet case 7: set_color(255, 0, 0); break; // red case 8: col_random(); break; // confetti case 9: sparkle(); break; // bubbles case 10: fire_new(700,120, 100); break; // fire case 11: matrix(0,255,0,5,90,true,40); break; //matrix case 12: rainbow(10); break; // rainbow case 13: { uint8_t colors[5][3] = {{255,0,0},{255,0,255},{0,0,255},{255,255,0},{0,255,0}} bouncing_balls(5, colors); // bouncing balls. } break; } enable_gesture(); } //-------------------- void effect_previous() { effect_stat--; if(effect_stat<1) effect_stat=effect_num; effect_next(); } //******************************** boolean check_int() { wdt_reset(); if (isr_flag==1 && apds.isGestureAvailable()) { tmp=apds.readGesture(); if(tmp>0 && tmp<7) { out_effect=true; Serial.println(tmp); //enable_gesture(); return true; } { out_effect=false; return false; } } else { out_effect=false; return false; } } //********************************* boolean check_ges() { wdt_reset(); if (apds.isGestureAvailable()) { tmp=apds.readGesture(); if(tmp > 0) { return true; } else { return false; } } else { enable_gesture(); return false; } } //************************************************* void processingProximity() { if( apds.readProximity(proximity_data)) { if( proximity_data <= old_proximity_data+range && proximity_data >= old_proximity_data-range ) { fade_off(); delay(700); set_color(255,255,255); EEPROM.write(0, vol); old_proximity_data = 0; enable_gesture(); } else { old_proximity_data = proximity_data; if(PROX_INT_HIGH_P > proximity_data) {proximity_data=PROX_INT_HIGH_P; old_proximity_data = 0;} vol = map(proximity_data, PROX_INT_HIGH_P, 255, 1, 255); light_on(); delay(map(vol, 25, 255, 70, 255)); enable_proximity(); } } else { enable_gesture(); } } //********************************************************** void enable_gesture() { if ( apds.init() ) { apds.setAmbientLightGain(AGAIN_1X); apds.enableLightSensor(false); apds.setAmbientLightIntEnable(false); apds.disableLightSensor(); apds.setProximityGain(PGAIN_1X); apds.setProximityIntHighThreshold(PROX_INT_HIGH_G); apds.setProximityIntLowThreshold(PROX_INT_LOW); apds.setProximityIntEnable(false); apds.enableProximitySensor(false); apds.disableProximitySensor(); apds.setGestureGain(GGAIN_2X); apds.setGestureLEDDrive(LED_DRIVE_50MA); apds.enableGestureSensor(true); apds.setGestureIntEnable(true); //apds.disableGestureSensor(); isr_flag = 0; attachInterrupt(1, gesture_int, FALLING); digitalWrite(LED_PIN, LOW); } else { while(true){}} } void enable_proximity() { if ( apds.init() ) { apds.setAmbientLightGain(AGAIN_1X); apds.enableLightSensor(false); apds.setAmbientLightIntEnable(false); apds.disableLightSensor(); apds.setGestureGain(GGAIN_1X); apds.enableGestureSensor(false); apds.setGestureIntEnable(false); apds.disableGestureSensor(); apds.setProximityGain(PGAIN_2X); apds.setGestureLEDDrive(LED_DRIVE_12_5MA); apds.setProximityIntLowThreshold(PROX_INT_LOW); apds.setProximityIntHighThreshold(PROX_INT_HIGH_P); apds.enableProximitySensor(true); //apds.disableProximitySensor(); apds.setProximityIntEnable(true); isr_flag = 0; attachInterrupt(1, proximity_int, FALLING); digitalWrite(LED_PIN, LOW); } else {while(true){}} } //*************************************** void gesture_int() { digitalWrite(LED_PIN, HIGH); detachInterrupt(1); isr_flag = 1; } void proximity_int() { digitalWrite(LED_PIN, HIGH); detachInterrupt(1); isr_flag = 2; }
The second part:
//******************Snow sparkles*********************************** void sparkle() // uint32_t getPixelColor(uint16_t n) { #define min_light 1 strip.setBrightness(255); for(uint8_t j=0; j<strip.numPixels(); j++) { strip.setPixelColor(j, strip.Color(min_light, min_light, min_light)); // minimum glow of the entire strip } while(true) { for(uint8_t i=0; i<NUM_PIX; i++) { uint8_t pixel = random(0,NUM_PIX); //random pixel position strip.setPixelColor(pixel, 255,255,255); //maximum possible brightness of the white pixel strip.show(); delay(random(20)); strip.setPixelColor(pixel, min_light,min_light,min_light); //maximal possible brightness of the white pixel strip.show(); delay(random(200, 500)); wdt_reset(); if ( isr_flag == 1) break; } if(check_int()) return; //if there was an interrupt, return } } //*****************Lamp*********************************** void lamp() { strip.setBrightness(vol); uint8_t fadestep=255/(strip.numPixels()/2); for(int i=0; i<=strip.numPixels()/2; i++) { strip.setPixelColor(i, strip.Color((i+1)*fadestep, (i+1)*fadestep, (i+1)*fadestep)); } for(int i=strip.numPixels()-1; i>=strip.numPixels()/2; i--) { strip.setPixelColor(i, strip.Color((strip.numPixels()-(i-1))*fadestep, (strip.numPixels()-(i-1))*fadestep, (strip.numPixels()-(i-1))*fadestep)) } strip.show(); } //*********************Rainbow************************************ void rainbow(int wait) { if(vol<10){ strip.setBrightness(10);}else{strip.setBrightness(vol);} while(true) { for(long firstPixelHue = 0; firstPixelHue < 5*65536; firstPixelHue += 256) { for(int i=0; i<strip.numPixels(); i++) { int pixelHue = firstPixelHue + (i * 65536L / strip.numPixels()); strip.setPixelColor(i, strip.gamma32(strip.ColorHSV(pixelHue)) } strip.show(); // Update strip with new contents delay(wait); // Pause for a moment // wdt_reset(); // if ( isr_flag == 1) break; if(check_int()) return; } } } //*******************Random colored pixels*************************************** void col_random() { byte i; set_color(0,0,0); if(vol<32){ strip.setBrightness(64);}else{strip.setBrightness(vol);} while(true) { i=random(0,NUM_PIX); byte r=random(2); if(r) r=255; byte g=random(2); if(g) g=255; byte b=random(2); if(b) b=255; strip.setPixelColor(i,r,g,b); strip.setPixelColor(i+1,0,0,0); strip.setPixelColor(random(0,NUM_PIX),0,0,0); strip.setPixelColor(i+2,0,0,0); strip.setPixelColor(random(0,NUM_PIX),0,0,0); strip.setPixelColor(i+3,0,0,0); strip.show(); delay(random(50,200)); if(check_int()) return; } } //*****************Glowing embers*********************************** void flame() { if(vol<8){ strip.setBrightness(8);}else{strip.setBrightness(vol);} while(true) { for(int i=0; i<strip.numPixels(); i++) { strip.setPixelColor(i, strip.Color(random(150, 254), random(0, 45), 0)) } strip.setBrightness(random(1,64)); strip.show(); delay(random(100,200)); if(check_int()) return; } } //****************Flame**************************************** void fire_new(int Cooling, uint8_t Sparking, uint8_t SpeedDelay) { if(vol<20){ strip.setBrightness(20);}else{strip.setBrightness(vol);} while(true) { static byte heat[NUM_PIX]; int cooldown; for( int i = 0; i < NUM_PIX; i++) { cooldown = random(0,(Cooling/NUM_PIX) + 2); if(cooldown>heat[i]) { heat[i]=0; } else { heat[i]=heat[i]-cooldown; } } for( int k= NUM_PIX - 1; k >= 2; k--) { heat[k] = (heat[k - 1] + heat[k - 2] + heat[k - 2]) / 3; } if( random(255) < Sparking ) { int y = random(7); heat[y] = heat[y] + random(160,255); //heat[y] = random(160,255); } for( int j = 0; j < NUM_PIX; j++) { setPixelHeatColor(j, heat[j] ); } strip.show(); delay(15); if(check_int()) return; } } void setPixelHeatColor (int Pixel, byte temperature) { byte t192 = round((temperature/255.0)*191); // Scale 'heat' down from 0-255 to 0-191 //calculate ramp up from byte heatramp = t192 & 0x3F; // 0..63 heatramp <<= 2; // scale up to 0..252 if( t192 > 0x80) // figure out which third of the spectrum we're in: { strip.setPixelColor(Pixel, 255, 255, heatramp);// hottest } else if( t192 > 0x40 ) { strip.setPixelColor(Pixel, 255, heatramp, 0); // middle } else { strip.setPixelColor(Pixel, heatramp, 0, 0);// coolest } } //********************Ball back and forth************************************* void CylonBounce(byte red, byte green, byte blue, int EyeSize, int SpeedDelay, int ReturnDelay) { while(true) { for(int i = 0; i < NUM_PIX-EyeSize-2; i++) { color_all_strip(0,0,0); strip.setPixelColor(i, red/10, green/10, blue/10); //side pixel for(int j = 1; j <= EyeSize; j++) { strip.setPixelColor(i+j, red, green, blue); //main pixels } strip.setPixelColor(i+EyeSize+1, red/10, green/10, blue/10); //side pixel strip.show(); delay(SpeedDelay); if(check_int()) return; } delay(ReturnDelay); for(int i = NUM_PIX-EyeSize-2; i > 0; i--) { color_all_strip(0,0,0); strip.setPixelColor(i, red/10, green/10, blue/10); for(int j = 1; j <= EyeSize; j++) { strip.setPixelColor(i+j, red, green, blue); } strip.setPixelColor(i+EyeSize+1, red/10, green/10, blue/10); strip.show(); delay(SpeedDelay); if(check_int()) return; } if(check_int()) return; } } //**************matrix effect********************* void matrix(byte red, byte green, byte blue, byte meteorSize, byte meteorTrailDecay, boolean meteorRandomDecay, int SpeedDelay) { if(vol<10){ strip.setBrightness(16);}else{strip.setBrightness(vol);} while(true) { for(int i = NUM_PIX-1; i >= -NUM_PIX; i--) { for(int j=NUM_PIX-1; j>=0; j--) { if( (!meteorRandomDecay) || (random(10)>5) ) { fadeToBlack(j, meteorTrailDecay ); } } for(int j = meteorSize-1; j >=0; j--) { if( ( i-j <NUM_PIX) && (i-j>=0) ) { strip.setPixelColor(i-j, red, green, blue); } } strip.show(); delay(SpeedDelay); if(check_int()) return; } //------------------------------- for(int i = 0; i < NUM_PIX; i++ ) { strip.setPixelColor(i, 0, 0, 0); } strip.show(); // delay(random(0,500)); } } void fadeToBlack(int ledNo, byte fadeValue) { uint32_t oldColor; uint8_t r, g, b; oldColor = strip.getPixelColor(ledNo); //gets the color of a given pixel r = (oldColor & 0x00ff0000UL) >> 16; //retrieving g = (oldColor & 0x0000ff00UL) >> 8; //3 color bytes b = (oldColor & 0x000000ffUL); //from the variable long r=(r<=10)? 0 : (int) r-(r*fadeValue/256); g=(g<=10)? 0 : (int) g-(g*fadeValue/256); b=(b<=10)? 0 : (int) b-(b*fadeValue/256); strip.setPixelColor(ledNo, r,g,b); } //***************Lava lamp************************ void bouncing_balls(int BallCount, byte colors[][3]) { if(vol<64){ strip.setBrightness(64);}else{strip.setBrightness(vol);} while(true) { float Gravity = -0.81; //-0.1; -9.81; int StartHeight = 1; float Height[BallCount]; float ImpactVelocityStart = sqrt( -2 * Gravity * StartHeight ); float ImpactVelocity[BallCount]; float TimeSinceLastBounce[BallCount]; int Position[BallCount]; long ClockTimeSinceLastBounce[BallCount]; float Dampening[BallCount]; for (int i = 0 ; i < BallCount ; i++) { ClockTimeSinceLastBounce[i] = millis(); Height[i] = StartHeight; Position[i] = 0; ImpactVelocity[i] = ImpactVelocityStart; TimeSinceLastBounce[i] = 0; Dampening[i] = 0.99 - float(i)/pow(BallCount,2);//0.90 } while (true) { for (int i = 0 ; i < BallCount ; i++) { TimeSinceLastBounce[i] = millis() - ClockTimeSinceLastBounce[i]; Height[i] = 0.5 * Gravity * pow( TimeSinceLastBounce[i]/1000 , 2.0 ) + ImpactVelocity[i] * TimeSinceLastBounce[i]/1000; if ( Height[i] < 0 ) { Height[i] = 0; ImpactVelocity[i] = Dampening[i] * ImpactVelocity[i]; ClockTimeSinceLastBounce[i] = millis(); if ( ImpactVelocity[i] < 0.01 ) { ImpactVelocity[i] = ImpactVelocityStart; } } Position[i] = round( Height[i] * (strip.numPixels() - 1) / StartHeight); } for (int i = 0 ; i < BallCount ; i++) { strip.setPixelColor(Position[i], strip.Color(colors[i][0],colors[i][1],colors[i][2])); } strip.show(); for(uint8_t j=0; j<strip.numPixels(); j++) { strip.setPixelColor(j, strip.Color(0, 0, 0)); //zeroing the tape } if(check_int()) return; } } if(check_int()) return; } //***************Support functions*********** //***************Installing any color************ void set_color(uint8_t R, uint8_t G, uint8_t B) { for(uint8_t i=0; i<vol; i++) { strip.setBrightness(i+1); //Change the brightness of the ribbon for(uint8_t j=0; j<strip.numPixels(); j++) { strip.setPixelColor(j, strip.Color(R, G, B)); } strip.show(); } } void color_all_strip(uint8_t R, uint8_t G, uint8_t B) { strip.setBrightness(vol); //Change the brightness of the ribbon for(uint8_t j=0; j<strip.numPixels(); j++) { strip.setPixelColor(j, strip.Color(R, G, B)); } strip.show(); } void light_on() { strip.setBrightness(vol); for(uint8_t j=0; j<strip.numPixels(); j++) { strip.setPixelColor(j, strip.Color(255, 255, 255)); } strip.show(); } void fade_off() { effect_stat=0; for(uint8_t i=vol; i>0; i--) { strip.setBrightness(i-1); for(uint8_t j=0; j<strip.numPixels(); j++) { strip.setPixelColor(j, strip.Color(255, 255, 255)); } strip.show(); } }
You can add any effect you like to my code but don’t forget to add a gesture exit from the looped effect. To do this, you need to add the following line if(check_int())
return at the end of the loop;
A description of the main functions in the sketch
ProcessingGesture()
processes interrupt from the gesture sensor.processingProximity()
processes interrupt from the proximity sensor.gesture_int()
– interrupt handler from the gesture sensor.proximity_int()
– proximity sensor interrupt handler.
Constants description
APDS9960_INT
– input for external interrupt. Arduino Nano and UNO have only two such inputs, 2 and 3.PIN
– Here, you need to specify the output to the LED strip. You can specify any digital pin to which the ws2812b strips are connected.NUM_PIX
– here you specify the number of addressable LEDs in the strip used.range
– error limit from 0 to 10. If you fix your hand over the sensor for 2 seconds when adjusting the brightness, the readings should be saved, but if the readings are not saved, then gradually increase the error limit.
Conclusion
Luminaire looks better live than on the video. Now, all who have seen it in my place, asking to build them the same. At the moment, I wouldn’t say I like the way the brightness control works, but I am working on it. No promises soon, but the next version of the light is already in development.
Thanks for reading this article to the end! If you still have questions, you can ask in the comments below the article. No registration is required.
As a DIY enthusiast, I recently decided to take on the challenge of controlling lights with an Arduino. After researching and reading up on the project, I was eager to get started.
Armed with my newfound knowledge, I headed down to the local electronics store and purchased the necessary components for my project. Gathering all of the parts was surprisingly simple and it only took me about half an hour to assemble everything together.
Once finished, I downloaded the Arduino software and began programming it with my desired commands. It took some time but eventually I had created a fully functional system that allowed me to control lights in my house through Arduino commands. The experience was thrilling as it gave me a sense of accomplishment that money can’t buy!
Hello, I also tried to do this project, unfortunately I did not succeed, the hardware I used is not wrong, the software should be wrong, please ask how your project successfully shared your experience, thank you