Stephen Harrison
Published © CC BY

Jedi Light Switch

Replace your boring old wall light switch with this Photon based switch to control Lifx lighting with touch or the wave of a Jedi.

AdvancedFull instructions provided8 hours5,312
Jedi Light Switch

Things used in this project

Hardware components

Photon
Particle Photon
Headerless version
×1
APDS-9960
Jedi sensor, although apparently it's pronounced Gesture.
×1
HTU21D
Temperature and Humidity
×1
AT42QT1010
Touch sensor
×1
Blue LEDs (1206 - Reverse Mount)
Replace with another color or intensity if desired. Ensure reverse mount 1206 case
×4
Resistor - 1k (0805)
Change as needed for brightness of corner LEDs
×4
WS2812B
5050 Chip
×4
MCP73831
U6 - Battery Charger (V1.2)
×1
MBRA140
D9 - Battery Charger (V1.2)
×1
Resistor - 10k (0805)
R16 - Battery Charger (V1.2)
×1
Capacitor - 4k7 (0805)
C11/C12 - Battery Charger (V1.2)
×2
Resistor - 2k (0805)
R17 - Battery Charger (V1.2)
×1
2mm JST Socket
J1 - Battery Charger (V1.2)
×1
2000mAh LiPol Battery
V1.2 Only
×1
Resistor - 100k (0805)
R13/R14 - Battery Monitor (V1.2 Only)
×1
MAX17043G+U
Check package/footprint - link not correct. Battery Monitor (V1.2)
×1
Capacitor - 1.0uF (0805)
C14 - Battery Monitor (V1.2)
×1
Resistor - 1k (0805)
R22/R23- Battery Monitor (V1.2)
×2
BSS138
IR LED (V1.2)
×1
IR LEDs (!206)
IR LED (V1.2)
×2
Capacitor - 1.0uF (0805)
C13 - IR LED (V1.2)
×1
Resistor - 10k (0805)
R19 - IR LED (V1.2)
×1
Resistor - 10R (0805)
R18 - IR LED (V1.2)
×1
Resistor - 62R (0805)
R20/R21 - IR LED (V1.2)
×2
Capacitor - 0.1uF (0805)
NeoPixels (V1.2) and Touch (C6)
×5
Resistor - 470R (0805)
NeoPixels (V1.2)
×1
Resistor - 4k7 (0805)
I2C pull-ups and touch
×4
Capacitor - 22nF (0805)
C4 - Adjust as needed for board touch sensitivity. C5 is Do Not Populate - can be used to add capacitance if needed.
×1
Capacitor - 1uF (0805)
Gesture
×2
Capacitor - 100uF Tand (Case D)
Gesture
×1
Resistor - 0R (0805)
Because every circuit needs a 0R resistor.
×1
TEMT6000
V1,2 only analog light sensor.
×1
Resistor - 10k (0805)
R15 for TEMT6000 (V1.2)
×1

Software apps and online services

IFTTT Particle Channel
IFTTT Lifx Channel
Tinamous

Hand tools and fabrication machines

Soldering iron (generic)
Soldering iron (generic)
SMD Rework Station
Or reflow oven
Magnifying Glass
Preferably with light
Tweezers
Proper SMD ones so they let go of the components
3D Printer (generic)
3D Printer (generic)
I used an Ultimaker2+ (Form2 for the clear case) but that shouldn't make a difference.

Story

Read more

Custom parts and enclosures

Base - 16mm

Select an appropriate base for the depth of your wall switch. Base works for V1.1 and V1.2 PCBs. For V1.2 PCB if the JST connector is fitted this base will most likely be to small.

Base - 19mm

Base - 25mm

Base - 28mm

Minimal Cover - 16mm

Minimal Cover - 19mm

Minimal Cover - 25mm

Minimal Cover - 28mm

V1.1 Cover - 16mm

V1.1 Cover - 19mm

V1.1 Cover - 25mm

V1.1 Cover - 28mm

V1.2 Cover - 16mm

V1.2 Cover - 19mm

V1.2 Cover - 25mm

V1.2 Cover - 28mm

Schematics

Jedi LightSwitch Github

Base - 16mm

Select an appropriate base for the depth of your wall switch. Base works for V1.1 and V1.2 PCBs. For V1.2 PCB if the JST connector is fitted this base will most likely be to small.

V1.2 PDF Schematic

V1.1 Eagle Schematic

V1.2 PDF Board - All Layers

V1.2 PDF Board - Top Tracks

V1.2 PDF Board - Bottom tracks

V1.2 Eagle Schematic

V1.2 Eagle Board

V1.2 OSH Park PCB

Main PCB.

V1.1 PDF Schematic

V1.1 PDF Schematic

V1.1 PDF Board - All Layers

V1.1 PDF Board - Top tracks

V1.1 PDF Board - Bottom tracks

V1.1 Eagle Schematic

V1.1 Eagle Board

V1.1 OSH Park PCB

Base - 16mm

16mm deep base to go between the wall and the switch. If you have a taller switch use a bigger base. Base supports both V.11 and V1.2 boards

Base - 19mm

V1.2 Cover - 16mm

V1.2 Cover - 25mm

Code

JediLightSwitch.ino

Arduino
This is the main file for the Photon.
// This #include statement was automatically added by the Particle IDE.
#include "SenMLBuilder.h"

// This #include statement was automatically added by the Particle IDE.
#include "IrBlaster.h"

// This #include statement was automatically added by the Particle IDE.
#include "LightSensor.h"

// This #include statement was automatically added by the Particle IDE.
#include "LedHandler.h"

// This #include statement was automatically added by the Particle IDE.
#include "PowerManagement.h"

// This #include statement was automatically added by the Particle IDE.
#include "GestureSensor.h"

// This #include statement was automatically added by the Particle IDE.
#include "TouchSensors.h"

// This #include statement was automatically added by the Particle IDE.
#include "EnvironmentSensors.h"

// This isn't neaded but Partile build complains if it's not.
// It's already included in GestureSensor.h
#include "SparkFun_APDS9960.h"

///////////////////////////////////////////////////////////////////////////////////
// 0: V1.0 board (with logo on)
// 1: V1.1 board (2 vertical front sensors, LED in each corner)
// 2: V1.2 board (5 horizontal front sensors)
const int HARDWARE_REV = 2;
///////////////////////////////////////////////////////////////////////////////////

///////////////////////////////////////////////////////////////////////////////////
// Hardware abstraction.
LedHandler* leds;
EnvironmentSensors* environmentSensors;
TouchSensors* touchSensors;
GestureSensor* gestureSensor;
PowerManagement* powerManagement;
IrBlaster* irBlaster;
///////////////////////////////////////////////////////////////////////////////////

///////////////////////////////////////////////////////////////////////////////////
// Particle functions
// Flash the user facing LEDs
int flash(String command);
// Switch on the internal LED to make the box glow.
int glow(String command);
// Turn off User facing LEDs/Internal LED
int off(String command);
///////////////////////////////////////////////////////////////////////////////////

// Build a senml document for publishing
// NB: Max document size for publishing is 255 bytes.
SenMLBuilder senMLBuilder = SenMLBuilder();

// If the lightswitch things it is currently dark.
bool isDark = false;

// Loop counter 
int loopCounter = 0;
int publishMeasurementsCounter = 0;

// If measurements should be taken on the next run through loop.
bool timeToTakeMeasurements = false;

// Measurement timer. Every n seconds.
Timer takeMeasurementsTimer(20000, takeMeasurementsTick);

// ***********************************************************************
// Setup
// ***********************************************************************
void setup() {
    
    setupHAL();

    setupLeds();
    
    setupGestureSensor();
    
    setupTouch();
    
    setupEnvironmentSensors();
    
    setupIrBlaster();
    
    setupParticleFunctions();
    
    //attachInterrupt(D2, interruptHandler, FALLING);
    
    setupComplete();
    
    // Force and an initial measurement reading.
    //timeToTakeMeasurements = true;
}

void interruptHandler() {
    leds->debugLedOn();
}

// Setup the hardware abstraction layer classes.
void setupHAL() {
    if (HARDWARE_REV == 2) {
        leds = new LedHandlerV2();
        gestureSensor = new GestureSensorV2(leds);
        environmentSensors = new EnvironmentSensorsV2(leds);
        irBlaster = new IrBlasterV2();
        touchSensors = new TouchSensorsV2(leds);
        powerManagement = new PowerManagementV2();
    } else {
        leds = new LedHandlerV1();
        gestureSensor = new GestureSensorV1(leds);
        environmentSensors = new EnvironmentSensorsV1(leds);
        irBlaster = new IrBlasterV1();
        touchSensors = new TouchSensorsV1(leds);
        powerManagement = new PowerManagementV1();
    }
    
}

void setupLeds() {
    if (!leds->init()) {
        Particle.publish("Setup", "Leds setup error.");
        RGB.color(255, 0, 00);
    }
}

void setupGestureSensor() {
    if (!gestureSensor->init()) {
        Particle.publish("Setup", "APDS-9960 setup error.");
        RGB.color(255, 0, 00);
    } 
}

void setupEnvironmentSensors() {
    if (!environmentSensors->init()) {
        Particle.publish("Setup", "EnvironmentSensors setup failed.");
        RGB.color(255, 0, 00);
    }
}

void setupIrBlaster() {
    if (!irBlaster->init()) {
        Particle.publish("Setup", "IR Blaster setup failed.");
        RGB.color(255, 0, 00);
    }
}

void setupTouch() {
    if (!touchSensors->init()) {
        Particle.publish("Setup", "Touch sensor setup faield.");
        RGB.color(255, 0, 00);
    }
}

void setupPowerManagement() {
    if (!powerManagement->init()) {
        Particle.publish("Setup", "Power management setup failed.");
        RGB.color(255, 0, 00);
    }
}

// Set-up internet callable Particle functions
void setupParticleFunctions() {
    Particle.function("flash", flash);
    Particle.function("glow", glow);
    Particle.function("off", off);
}

void setupComplete() {
    // Complete setup.
    Particle.publish("Setup", "Jedi Light Switch Set-up complete. V1.2.15");
    
    // Short delay to allow the LEDs to be seen and
    // publishing to happen.
    delay(500);
    leds->ledsOff();
    
    // Start the measurements timer
    takeMeasurementsTimer.start();
}

// ***********************************************************************
// Main Loop
// ***********************************************************************
void loop() {
    
    gestureSensor->checkForGesture();
    touchSensors->checkIfTouched();
    
    if (timeToTakeMeasurements) {
        takeMeasurements();
    }
    
    // Check to see if the light level has changes
    // Uses Gesture sensor and/or environment sensors which are updated 
    // in the background.
    checkLightDarkLevel();

    loopCounter++;
    delay(250);
    leds->debugLedOff();
}

// Called every n seconds by the timer.
// to take light/temperature/humidity measurements and 
// publish them.
// Does not actually take the measurements as it publishes
// and this can cause delays which are undesirable
void takeMeasurementsTick() {
    timeToTakeMeasurements = true;
}

// Called every n seconds by the timer.
// to take light/temperature/humidity measurements and 
// publish them.
void takeMeasurements() {
    
    timeToTakeMeasurements = false;
    
    // Flash the LEDs when measuring to give feedback
    //allLedsOn(); // temp debug to show reading
    //leds->debugLedOn();
    
    // Read the light level every loop but 
    // only publish the update every n times around.
    gestureSensor->readLightLevel(false);
    
    //gestureSensor->readLightColors(publishMeasurements);
    environmentSensors->readTemperatureAndHumidity(false);
    
    environmentSensors->readAnalogLightLevel(false);
    
    powerManagement->readPowerState(false);
    
    // publish the measurements as one block if appropriate and reset the counters
    if (publishMeasurementsCounter > 0) {
        // Start a nice fresh senml document.
        senMLBuilder.begin();
        environmentSensors->appendSenML(&senMLBuilder);
        gestureSensor->appendSenML(&senMLBuilder);
        powerManagement->appendSenML(&senMLBuilder);
        senMLBuilder.publish();
        
        publishMeasurementsCounter = 0;
    }
    publishMeasurementsCounter++;
    
    // Delay to allow the leds to be seen to flash
    delay(250);
    //leds->debugLedOff();
}

// ***********************************************************************
// Check to see if it is light/dark.
// ***********************************************************************
void checkLightDarkLevel() {
    // TODO: Make ambientLightThreshold configurable via particle function.
    
    if (gestureSensor->isDark()) {
        if (!isDark) {
            // Below "Dark" level
            // Enable glowing.
            leds->glow();
        
            Particle.publish("Status", "It is now dark.");
            isDark = true;
        }
    }  else if (gestureSensor->isLight()) {
        if (isDark) {
            // Above "Light" level
            // Disable glowing.
            // TODO: But not if requested via the glow particle.function 
            leds->noglow();
        
            Particle.publish("Status", "It is now light.");
            isDark = false;
        }
        
        return;
    }
}

///////////////////////////////////////////
// Particle Function handles
///////////////////////////////////////////

// Flash the user facing LEDs
int flash(String command) {
    
    for (int i = 0; i<10; i++) {
        leds->ledsOff();
        leds->topLedsOn();
        delay(250);
        
        leds->ledsOff();
        leds->bottomLedsOn();
        delay(250);
    }
    
    leds->ledsOff();
    
    Particle.publish("Status", "Flashed LEDs");
    return 1;
}

// Switch on the internal LED to make the box glow.
int glow(String command) {
    if (command == "on") {
        leds->glow();
        Particle.publish("Status", "Glowing");
        return 1;
    } else if (command == "rainbow") {
        leds->rainbowGlow();
        Particle.publish("Status", "Rainbow Glowing");
        return 2;
    }
    else {
        leds->noglow();
        Particle.publish("Status", "Not Glowing");
        return 0;
    }
    
    Particle.publish("Status", "No command for glow :-(");
    return -1;
}

// Turn off User facing LEDs/Internal LED
int off(String command) {
    
    leds->ledsOff();
    leds->noglow();
    Particle.publish("Status", "LEDs Off");
    return 0;
}

EnvironmentSensors.cpp

C/C++
// Responsible for handling temperature and humidity measurements.

 #include "application.h"
 #include "EnvironmentSensors.h"
 
///////////////////////////////////////////////////////////////////////////////////
// Base class
///////////////////////////////////////////////////////////////////////////////////

// Ctor.
EnvironmentSensors::EnvironmentSensors(LedHandler* leds) {
     _leds = leds;
}

bool EnvironmentSensors::init() {
    // Setup I2C as master
    if ( !Wire.isEnabled() ) {
        Wire.setSpeed(CLOCK_SPEED_400KHZ);
        Wire.begin();
    }
    
        // Initialize Temperature and humidity
    if (!_humiditySensor.begin()) {
        Particle.publish("Setup", "Temperature/Humidity sensor setup failed.");
        delay(250);
        RGB.color(255, 0, 00);
        return false;
    }
    
    return true;
}

void EnvironmentSensors::readTemperatureAndHumidity(bool publish) {
    float h = _humiditySensor.readHumidity();
    if (h < 100) {
        _humidity = h;
    } else {
        // Invalid / error.
        RGB.color(255, 0, 0);
    }
    
    float t = _humiditySensor.readTemperature();
    if (t < 100) {
        _temperature = t;
    } else {
        // Invalid / error.
        RGB.color(255, 0, 0);
    }
    
    if (publish) {
        Particle.publish("senml", "{e:[{'n':'Temp','v':'" + String(_temperature) + "'},{'n':'RH','v':'" + String(_humidity) + "'}]}");
    }
}

void EnvironmentSensors::readAnalogLightLevel(bool publish) {
    // No action for V1 hardware.
}

void EnvironmentSensors::appendSenML(SenMLBuilder* builder) {
   // "{e:[{'n':'Temperature','v':'" + String(_temperature) + "'},{'n':'Humidity','v':'" + String(_humidity) + "'}]}"
   builder->add("T", _temperature);
   builder->add("RH", _humidity);
}

uint16_t EnvironmentSensors::getTemperature() {
    return _temperature;    
}

uint16_t EnvironmentSensors::getHumidity() {
    return _humidity;
}

int EnvironmentSensors::getAnalogLightLevel() {
    return _analogLightLevel;
}

///////////////////////////////////////////////////////////////////////////////////
// V1
///////////////////////////////////////////////////////////////////////////////////
// ctor
EnvironmentSensorsV1::EnvironmentSensorsV1(LedHandler* leds) : EnvironmentSensors(leds) {
}


///////////////////////////////////////////////////////////////////////////////////
// V2
///////////////////////////////////////////////////////////////////////////////////
// ctor
EnvironmentSensorsV2::EnvironmentSensorsV2(LedHandler* leds) : EnvironmentSensors(leds) {
}

bool EnvironmentSensorsV2::init() {
    // initialise the base class
    EnvironmentSensors::init();
    pinMode(_analogLightLevelPin, INPUT);
    return true;
}

// Override to read actual analog light level as it's supported on 
// V2 hardware.
void EnvironmentSensorsV2::readAnalogLightLevel(bool publish) {
    _analogLightLevel = analogRead(_analogLightLevelPin);
}

void EnvironmentSensorsV2::appendSenML(SenMLBuilder* builder) {
    EnvironmentSensors::appendSenML(builder);
    builder->add("LightAnalog", _analogLightLevel);
}

EnvironmentSensors.h

C/C++
// Responsible for handling temperature and humidity measurements.

#ifndef EnvironmentSensors_H
#define EnvironmentSensors_H

#include "HTU21D/HTU21D.h"
#include "LedHandler.h"
#include "SenMLBuilder.h"


///////////////////////////////////////////////////////////////////////////////////
// Base
///////////////////////////////////////////////////////////////////////////////////
class EnvironmentSensors {
public:
    EnvironmentSensors(LedHandler* leds);
    virtual bool init();
    virtual void readTemperatureAndHumidity(bool publish);
    virtual void readAnalogLightLevel(bool publish);
    
    // Append sensor measurements to SenML Builder
    // for publication.
    virtual void appendSenML(SenMLBuilder* builder);
    
    uint16_t getTemperature();
    uint16_t getHumidity();
    int getAnalogLightLevel();
    
protected:
    int _analogLightLevel = 0;

private:
    HTU21D _humiditySensor;
    LedHandler* _leds;
    uint16_t _humidity = 0;
    uint16_t _temperature = 0;
};

///////////////////////////////////////////////////////////////////////////////////
// V1
///////////////////////////////////////////////////////////////////////////////////
class EnvironmentSensorsV1 : public EnvironmentSensors {
public:
    EnvironmentSensorsV1(LedHandler* leds);
};

///////////////////////////////////////////////////////////////////////////////////
// V2
///////////////////////////////////////////////////////////////////////////////////
class EnvironmentSensorsV2 : public EnvironmentSensors {
public:
    EnvironmentSensorsV2(LedHandler* leds);
    bool init() override;
    void readAnalogLightLevel(bool publish) override;
    void appendSenML(SenMLBuilder* builder) override;
private:
    int _analogLightLevelPin = A1;
};

#endif

GestureSensor.cpp

C/C++
 #include "application.h"
 #include "GestureSensor.h"

///////////////////////////////////////////////////////////////////////////////////
// Base
///////////////////////////////////////////////////////////////////////////////////
// Ctor.
GestureSensor::GestureSensor(int gestureInterruptPin, LedHandler* leds) {
    _gestureInterruptPin = gestureInterruptPin;
    _leds = leds;
}

/**
 * @brief Destructor
 */
GestureSensor::~GestureSensor() {
}

bool GestureSensor::init() {
    
    // Already has 10k pull-up.
    pinMode(_gestureInterruptPin, INPUT); 
    
    if ( !Wire.isEnabled() ) {
        Wire.setSpeed(CLOCK_SPEED_400KHZ);
        Wire.begin();
    }

    if (!setupLightAndGesture()) {
        Particle.publish("Setup", "Setup Light and Gesture failed. APDS-9960 Error code: " + String(_errorCode));
        return false;
    }
    
     // Now everything is set-up attach the intterupts
    auto interruptHandler = std::bind(&GestureSensor::gestureInterrupt, this);
    attachInterrupt(_gestureInterruptPin, interruptHandler, FALLING);
    
    return true;
}

// Initialise Gesture/Light/Proximity Sensor
bool GestureSensor::setupLightAndGesture() {
    
    // Initialize APDS-9960 (configure I2C and initial values)
    int errorCode = _apds.init();
    if ( _errorCode < 0) {
        Particle.publish("Setup", "Error during APDS-9960 init. APDS-9960 Error code: " + String(errorCode));
        _errorCode = 1;
        RGB.color(255, 0, 00);
        return false;
    }
    
    // Enable light sensing
    if ( !_apds.enableLightSensor(false) ) {
        RGB.color(255, 0, 00);
        _errorCode = 3;
        Particle.publish("Setup", "Error enabling light sensor");
        return false;
    }
    
    // Interrup when light below the low  threshold to say it's dark
    //_apds.setLightIntLowThreshold(_ambientLightLowThreshold);
    // Interrup when light above the high threshold to say it's now light
    //_apds.setLightIntHighThreshold(_ambientLightHighThreshold);
    
    ///
    // Set-up proximity
    // ---
    // Adjust the Proximity sensor gain
    if ( !_apds.setProximityGain(PGAIN_4X) ) { 
        RGB.color(255, 0, 00);
        _errorCode = 4;
        Particle.publish("Setup", "Error  trying to set PGAIN");
        return false;
    } 

    // Start running the APDS-9960 gesture sensor engine
    if (!_apds.enableGestureSensor(true) ) {
        RGB.color(255, 0, 00);
        _errorCode = 8;
        Particle.publish("Setup", "Enable Gesture setup failed.");
        return false;
    }
    
    _apds.clearAmbientLightInt();
    _apds.clearProximityInt();
    
    
    Particle.publish("Setup", "Gesture set-up complete");
    
    return true;
}

void GestureSensor::checkForGesture() {
        // If gesture/proximity/light interrupt flag.
    if (_gestureIsrFlag) {
        //_leds->debugLedOn();
        handleGesture();
    } else if (_apds.isGestureAvailable()) {
        //_leds->debugLedOn();
    }
    
        _apds.clearAmbientLightInt();
    _apds.clearProximityInt();
    
    //processGesture();
    //delay(150);
    //_leds->debugLedOff();
}

void GestureSensor::handleGesture() {
    
    detachInterrupt(_gestureInterruptPin);
    
    // Check the light level as that will cause an interrupt.
    // leave it up to the main loop to determine if light/dard.
    readLightLevel(false);

    // now check for a gesture...
    //readProximity();
    //checkProximity();
    processGesture();
    
    _apds.clearAmbientLightInt();
    _apds.clearProximityInt();
    
    // Pause to allow user to see LEDs
    delay(500);
    _gestureIsrFlag = 0;
    _leds->ledsOff();
    
    auto interruptHandler = std::bind(&GestureSensor::gestureInterrupt, this);
    attachInterrupt(_gestureInterruptPin, interruptHandler, FALLING);
}

void GestureSensor::readLightLevel(bool publish) {
    
    // Read light levels
    readAmbientLight();
    
    // Publish details
    if (publish) {
        publishLightLevel();
    }
}

void GestureSensor::publishLightLevel() {
    Particle.publish("senml", "{e:[{'n':'LightLevel','v':'" + String(_ambientLight) + "'}]}");
}

void GestureSensor::readLightColors(bool publish) {
    
    readRedLight();
    
    readGreenLight();
    
    readBlueLight();

    // Publish details
    if (publish) {
        publishColorLevels();
    }
}

void GestureSensor::publishColorLevels() {
    Particle.publish("senml", "{e:[{'n':'LightLevel','v':'" + String(_ambientLight) + "'},{'n':'Red','v':'" + String(_redLevel) + "'},{'n':'Green','v':'" + String(_greenLevel) + "'},{'n':'Blue','v':'" + String(_blueLevel) + "'},{'n':'Proximity','v':'" + String(_proximity) + "'}]}");    
}

void GestureSensor::readAmbientLight() {
    
    uint16_t value = 0;
    if (_apds.readAmbientLight(value)) {
        _ambientLight = value;
    } else {
        _errorCode = 100;
        RGB.color(255, 0, 0);
        // Particle.publish("Status", "Error reading ambient light");
    }
}

void GestureSensor::readRedLight() {
    uint16_t value = 0;
    if (_apds.readRedLight(value)) {
        _redLevel = value;
    } else {
        _errorCode = 101;
        RGB.color(255, 0, 0);
    }
}

void GestureSensor::readGreenLight() {
    uint16_t value = 0;
    if (_apds.readGreenLight(value)) {
        _greenLevel = value;
    } else {
        _errorCode = 102;
        RGB.color(255, 0, 0);
    }
}

void GestureSensor::readBlueLight() {
    uint16_t value = 0;
    if (_apds.readBlueLight(value)) {
        _blueLevel = value;
    } else {
        _errorCode = 103;
        RGB.color(255, 0, 0);
    }
}

void GestureSensor::readProximity() {
    if ( !_apds.clearProximityInt() ) {
        RGB.color(255, 0, 0);
    }
    
    // Check proximity.
    uint8_t value = 0;
    if ( _apds.readProximity(value) ) {
        _proximity = value;
    } 
}

void GestureSensor::processGesture() {
    if (_apds.isGestureAvailable()) {
        _leds->ledsOff();
        
        int gestureValue = _apds.readGesture();
        //Particle.publish("Status", String(gestureValue));

        switch (gestureValue) {
            case DIR_NONE: // 0
                //Particle.publish("Status", "Swipe None");
                // Skip the delay.
                return;
            case DIR_LEFT: // 1
                _leds->leftLedsOn();
                Particle.publish("Status", "Swipe Left");
                break;
            case DIR_RIGHT: // 2
                _leds->rightLedsOn();
                Particle.publish("Status", "Swipe Right");
                break;
            case DIR_UP: // 3
                _leds->topLedsOn();
                Particle.publish("Status", "Swipe Up");
                break;
            case DIR_DOWN: // 4
                _leds->bottomLedsOn();
                Particle.publish("Status", "Swipe Down");
                break;
            case DIR_NEAR: // 5
                _leds->allLedsOn();
                Particle.publish("Status", "Swipe Near");
                break;
            case DIR_FAR: // 6
                _leds->allLedsOn();
                Particle.publish("Status", "Swipe Far");
                break;
            case DIR_ALL: // 7
                Particle.publish("Status", "Swipe ALL!");
                break;
         // default:
            //Serial.println("NONE");
        }
    } 
}

// Interrupt from gesture sensor.
void GestureSensor::gestureInterrupt() {
    _gestureIsrFlag = true;
}

int GestureSensor::getAmbientLightLevel() {
    return _ambientLight;
}

bool GestureSensor::isDark() {
    // Unknown. Not measured
    if (_ambientLight < 0) {
        return false;
    }
    
    return _ambientLight < _ambientLightLowThreshold;
}

bool GestureSensor::isLight() {
    // Unknown. Not measured
    if (_ambientLight < 0) {
        return false;
    }
    
    return _ambientLight > _ambientLightHighThreshold;
}

void  GestureSensor::appendSenML(SenMLBuilder* builder) {
    // "{e:[{'n':'LightLevel','v':'" + String(_ambientLight) + "'}]}"
    builder->add("Light", _ambientLight);
}

///////////////////////////////////////////////////////////////////////////////////
// V1
///////////////////////////////////////////////////////////////////////////////////
// Constructor for V1 handler which sets up the base class
// with the known IO pins or the V1.1 board.
GestureSensorV1::GestureSensorV1(LedHandler* leds) : GestureSensor(D2, leds) {
}

///////////////////////////////////////////////////////////////////////////////////
// V2
///////////////////////////////////////////////////////////////////////////////////
// Constructor for V2  handler which sets up the base class
// with the known IO pins or the V1.2 board.
GestureSensorV2::GestureSensorV2(LedHandler* leds) : GestureSensor(D2, leds) {
}

GestureSensor.h

C/C++
// Responsible for handling gasture and gesture sensor light levels.

#ifndef GestureSensor_H
#define GestureSensor_H

#include "LedHandler.h"
#include "SparkFun_APDS9960.h"
#include "SenMLBuilder.h"

///////////////////////////////////////////////////////////////////////////////////
// Base
///////////////////////////////////////////////////////////////////////////////////
class GestureSensor {
public:
    GestureSensor(int gestureInterruptPin, LedHandler* leds);
    ~GestureSensor();
    virtual bool init();
    void checkForGesture();
    void readLightLevel(bool publish);
    int getAmbientLightLevel();
    bool isDark();
    bool isLight();
    
    // Append sensor measurements to SenML Builder
    // for publication.
    virtual void appendSenML(SenMLBuilder* builder);
private:
    bool setupLightAndGesture();
    void handleGesture();
    
    void publishLightLevel();
    void readLightColors(bool publish);
    void publishColorLevels();
    void readAmbientLight();
    void readRedLight();
    void readGreenLight();
    void readBlueLight();
    void readProximity();
    
    void processGesture();
    
    // Light level thresholds
    int _ambientLightLowThreshold = 10;
    int _ambientLightHighThreshold = 15;

    // APDS-9960 variables for light/proximity and gesture sensing.
    SparkFun_APDS9960 _apds = SparkFun_APDS9960();
    
    LedHandler* _leds;
    
    int _errorCode = 0;
    // Set to -1 to indicate the level has not been measured.
    int _ambientLight = -1;
    int _redLevel = -1;
    int _greenLevel = -1;
    int _blueLevel = -1;
    int _proximity = -1;
    
    // Interupt
    void gestureInterrupt();
    int _gestureInterruptPin;
    volatile bool _gestureIsrFlag = false;
};

///////////////////////////////////////////////////////////////////////////////////
// V1
///////////////////////////////////////////////////////////////////////////////////
class GestureSensorV1 : public GestureSensor {
public:
    GestureSensorV1(LedHandler* leds);
};

///////////////////////////////////////////////////////////////////////////////////
// V2
///////////////////////////////////////////////////////////////////////////////////
class GestureSensorV2 : public GestureSensor {
public:
    GestureSensorV2(LedHandler* leds);
};

#endif

IrBlaster.cpp

C/C++
#include "application.h"
#include "IrBlaster.h"

///////////////////////////////////////////////////////////////////////////////////
// Base
///////////////////////////////////////////////////////////////////////////////////
// Ctor.
IrBlaster::IrBlaster() {
}


/**
 * @brief Destructor
 */
IrBlaster::~IrBlaster() {
}

/**
 * @brief Base initialiser.
 *
 * @return True if initialized successfully. False otherwise.
 */
bool IrBlaster::init() {
    return true;
}

void IrBlaster::send() {
    // No action by default. Not common to all versions of hardware.
}

///////////////////////////////////////////////////////////////////////////////////
// V1
///////////////////////////////////////////////////////////////////////////////////
// Constructor for V1. No IR LEDs on V1.1 board
// with the known IO pins or the V1.1 board.
IrBlasterV1::IrBlasterV1() : IrBlaster() {
    
}

///////////////////////////////////////////////////////////////////////////////////
// V2
///////////////////////////////////////////////////////////////////////////////////
// Constructor for V2. Two IR LEDs on V2 board
IrBlasterV2::IrBlasterV2() : IrBlaster() {
}

bool IrBlasterV2::init() {
    pinMode(_irLedPin, OUTPUT);
    return true;
}

void IrBlasterV2::send() {
    // No action by default. Not common to all versions of hardware.
    // TODO: Waggle IR LED pin...
}

IrBlaster.h

C/C++
Note that the IR Blaster isn't actually implemented yet. These are just place holder class definitions/files for work in progress. Keep an eye on the repository for updates.
#ifndef IrBlaster_H
#define IrBlaster_H

///////////////////////////////////////////////////////////////////////////////////
// Base
///////////////////////////////////////////////////////////////////////////////////
// Send IR Pulses (hopefully becoming a remote control....)
class IrBlaster {
public:
    IrBlaster();
    ~IrBlaster();
    
    virtual bool init();
    
    // TODO: Figure out what to do!
    virtual void send();
};

///////////////////////////////////////////////////////////////////////////////////
// V1
///////////////////////////////////////////////////////////////////////////////////
class IrBlasterV1 : public IrBlaster {
public:
    IrBlasterV1();
};

///////////////////////////////////////////////////////////////////////////////////
// V2
///////////////////////////////////////////////////////////////////////////////////
class IrBlasterV2 : public IrBlaster {
public:
    IrBlasterV2();
    bool init() override;
    void send() override;
private:
    int _irLedPin = A0;
};

#endif

LedHandler.cpp

C/C++
// Responsible for handling the interface with the 4 UI LEDs in the corners of the board.

#include "application.h"
#include "LedHandler.h"

///////////////////////////////////////////////////////////////////////////////////
// Base
///////////////////////////////////////////////////////////////////////////////////
// Ctor.
LedHandler::LedHandler(int topLeftLed, int topRightLed, int bottomLeftLed, int bottomRightLed, int debugLed) {
     _topLeftLed = topLeftLed;
     _topRightLed =  topRightLed;
     _bottomLeftLed = bottomLeftLed;
     _bottomRightLed = bottomRightLed;
     _debugLed = debugLed;  
}


/**
 * @brief Destructor
 */
LedHandler::~LedHandler() {
}

/**
 * @brief Configures I2C communications and initializes registers to defaults
 *
 * @return True if initialized successfully. False otherwise.
 */
bool LedHandler::init() {
        // Take control of the Photons LED.
    RGB.control(true);
    RGB.brightness(100);
    RGB.color(0, 0, 0);
    
        // Set output mode for D7 LED.
    pinMode(_debugLed, OUTPUT);
    
    // User facing LEDs
    pinMode(_topLeftLed, OUTPUT);
    pinMode(_topRightLed, OUTPUT);
    pinMode(_bottomLeftLed, OUTPUT);
    pinMode(_bottomRightLed, OUTPUT);
    
    // Set default logic levels for the IO pins.
    // Switch all the user facing LEDs on during setup
    digitalWrite(_topLeftLed, HIGH);
    digitalWrite(_topRightLed, HIGH);
    digitalWrite(_bottomLeftLed, HIGH);
    digitalWrite(_bottomRightLed, HIGH);
    
    return true;
}

void LedHandler::debugLedOn() {
    // NB: This clashes with bottom right
    digitalWrite(_debugLed, HIGH);
}

void LedHandler::debugLedOff() {
    // NB: This clashes with bottom right
    digitalWrite(_debugLed, LOW);
}

void LedHandler::ledOn(int led) {
    switch (led) {
        case 1:
            digitalWrite(_topLeftLed, HIGH);
            break;
        case 2:
            digitalWrite(_topRightLed, HIGH);
            break;
        case 3:
            digitalWrite(_bottomRightLed, HIGH);
            break;
        case 4:
            digitalWrite(_bottomLeftLed, HIGH);
            break;
    }
}

void LedHandler::ledOff(int led) {
    switch (led) {
        case 1:
            digitalWrite(_topLeftLed, LOW);
            break;
        case 2:
            digitalWrite(_topRightLed, LOW);
            break;
        case 3:
            digitalWrite(_bottomRightLed, LOW);
            break;
        case 4:
            digitalWrite(_bottomLeftLed, LOW);
            break;
    }
}

void LedHandler::allLedsOn() {
    digitalWrite(_topLeftLed, HIGH);
    digitalWrite(_topRightLed, HIGH);
    digitalWrite(_bottomLeftLed, HIGH);
    digitalWrite(_bottomRightLed, HIGH);
}

void LedHandler::topLedsOn() {
    digitalWrite(_topLeftLed, HIGH);
    digitalWrite(_topRightLed, HIGH);
}

void LedHandler::bottomLedsOn() {
    digitalWrite(_bottomLeftLed, HIGH);
    digitalWrite(_bottomRightLed, HIGH);
}

void LedHandler::leftLedsOn() {
    digitalWrite(_topLeftLed, HIGH);
    digitalWrite(_bottomLeftLed, HIGH);
}

void LedHandler::rightLedsOn() {
    digitalWrite(_topRightLed, HIGH);
    digitalWrite(_bottomRightLed, HIGH);
}

void LedHandler::ledsOff() {
    digitalWrite(_topLeftLed, LOW);
    digitalWrite(_topRightLed, LOW);
    digitalWrite(_bottomLeftLed, LOW);
    digitalWrite(_bottomRightLed, LOW);
    digitalWrite(_debugLed, LOW);
}

void LedHandler::glow() {
        RGB.brightness(100);
        // Glow colour (Green).
        RGB.color(0, 255, 0);
}

void LedHandler::noglow() {
        RGB.brightness(100);
        RGB.color(0, 0, 0);
}

void LedHandler::rainbowGlow() {
        RGB.brightness(100);
        // TODO: Rainbow the RGB LED...
        RGB.color(0, 255, 255);
}

///////////////////////////////////////////////////////////////////////////////////
// V1
///////////////////////////////////////////////////////////////////////////////////
// Constructor for V1 LED handler which sets up the base class
// with the known IO pins or the V1.1 board.
LedHandlerV1::LedHandlerV1() : LedHandler(A0, D3, A5, D7, D7) {
}

///////////////////////////////////////////////////////////////////////////////////
// V2
///////////////////////////////////////////////////////////////////////////////////
// Constructor for V2 LED handler which sets up the base class
// with the known IO pins or the V1.2 board.
// NB: V2 hardware supports Neopixels
LedHandlerV2::LedHandlerV2() : LedHandler(A2, D3, A5, D7, D7) {
}

bool LedHandlerV2::init() {
    LedHandler::init();
    
    _strip.begin();
     // Initialize all pixels to 'off'
    _strip.show();
    
    return true;
}

 void LedHandlerV2::glow() {
    // V2 hardware, don't use the tri-color lED on the photon as we have NeoPixels!
    // Set to use a dimish green
    uint32_t color = this->_strip.Color(0, 0, 90);
    for(uint16_t i=0; i<_strip.numPixels(); i++) {
      this->_strip.setPixelColor(i, color);
    }
    this->_strip.show();
}

void LedHandlerV2::noglow() {
    uint32_t color = this->_strip.Color(0, 0, 0);
    for(uint16_t i=0; i<_strip.numPixels(); i++) {
      this->_strip.setPixelColor(i, color);
    }
    this->_strip.show();
}

void LedHandlerV2::rainbowGlow() {
    LedHandlerV2::rainbow(20);
}

void LedHandlerV2::rainbow(uint8_t wait) {
  uint16_t i, j;

  for(j=0; j<256; j++) {
    for(i=0; i<_strip.numPixels(); i++) {
      this->_strip.setPixelColor(i, Wheel((i+j) & 255));
    }
    this->_strip.show();
    delay(wait);
  }
}

// Input a value 0 to 255 to get a color value.
// The colours are a transition r - g - b - back to r.
uint32_t LedHandlerV2::Wheel(byte WheelPos) {
  if(WheelPos < 85) {
   return this->_strip.Color(WheelPos * 3, 255 - WheelPos * 3, 0);
  } else if(WheelPos < 170) {
   WheelPos -= 85;
   return this->_strip.Color(255 - WheelPos * 3, 0, WheelPos * 3);
  } else {
   WheelPos -= 170;
   return this->_strip.Color(0, WheelPos * 3, 255 - WheelPos * 3);
  }
}

LedHandler.h

C/C++
// Responsible for handling the interface with the 4 UI LEDs in the corners of the board.

 
#ifndef LedHandler_H
#define LedHandler_H

#include "neopixel/neopixel.h"

///////////////////////////////////////////////////////////////////////////////////
// Base
///////////////////////////////////////////////////////////////////////////////////
class LedHandler {
public:
    LedHandler();
    ~LedHandler();
    
    virtual bool init();
    void debugLedOn();
    void debugLedOff();
    void ledOn(int led);
    void ledOff(int led);
    void allLedsOn();
    void topLedsOn();
    void bottomLedsOn();
    void leftLedsOn();
    void rightLedsOn();
    void ledsOff();
    
    virtual void glow();
    virtual void rainbowGlow();
    virtual void noglow();
    
protected:
    LedHandler(int topLeftLed, int topRightLed, int bottomLeftLed, int bottomRightLed, int debugLed);
    

private:
    int _topLeftLed;
    int _topRightLed;
    int _bottomLeftLed;
    int _bottomRightLed;
    int _debugLed;     
};

///////////////////////////////////////////////////////////////////////////////////
// V1
///////////////////////////////////////////////////////////////////////////////////
class LedHandlerV1 : public LedHandler {
public:
    LedHandlerV1();
};

///////////////////////////////////////////////////////////////////////////////////
// V2
///////////////////////////////////////////////////////////////////////////////////
class LedHandlerV2 : public LedHandler {
public:
    LedHandlerV2();
    bool init() override;
    void glow() override;
    void noglow() override;
    void rainbowGlow() override;
private:
    // V2 hardware supports NeoPixels on the back of the PCB
    void rainbow(uint8_t wait);
    uint32_t Wheel(byte WheelPos);
    Adafruit_NeoPixel _strip = Adafruit_NeoPixel(4, A7, WS2812B);
};

#endif

PowerManagement.cpp

C/C++
#include "application.h"
#include "PowerManagement.h"

///////////////////////////////////////////////////////////////////////////////////
// Base
///////////////////////////////////////////////////////////////////////////////////
// Ctor.
PowerManagement::PowerManagement() {
}

/**
 * @brief Destructor
 */
PowerManagement::~PowerManagement() {
}

/**
 * @brief Initializes power management
 *
 * @return True if initialized successfully. False otherwise.
 */
bool PowerManagement::init() {
}

void PowerManagement::appendSenML(SenMLBuilder* builder) {
    // No action for base & V1 classes.
}

///////////////////////////////////////////////////////////////////////////////////
// V1
///////////////////////////////////////////////////////////////////////////////////
PowerManagementV1::PowerManagementV1() {
}

void PowerManagementV1::readPowerState(bool publish) {
    // Nothing to implement.
}

///////////////////////////////////////////////////////////////////////////////////
// V2
///////////////////////////////////////////////////////////////////////////////////
PowerManagementV2::PowerManagementV2() {
}

bool PowerManagementV2::init() {
    pinMode(_batteryMonitorAnalogPin, INPUT);
    pinMode(_chargeStatusPin, INPUT);
    
    _batteryMonitor.reset();
    _batteryMonitor.quickStart();
        
    return true;
}

void PowerManagementV2::readPowerState(bool publish) {
    _cellVoltage = _batteryMonitor.getVCell();
    _stateOfCharge = _batteryMonitor.getSoC();
    
    // Battery charger status
    _chargeState = digitalRead(_chargeStatusPin);
    
    // Battery ADC value
    _batteryAdc = analogRead(_batteryMonitorAnalogPin);
    
    // Convert ADC value (0.8mv per ADC, 1/2 voltage divider, millivolrs -> volts)
    _batteryVoltageAnalog = ( (_batteryAdc * 0.8F) * 2.0F / 1000);
    
    if (publish) {
        Particle.publish("senml", "{e:[{'n':'ps-voltage','v':'" + String(_cellVoltage) + "'},{'n':'ps-soc','v':'" + String(_stateOfCharge) + "'},{'n':'charge-status','v':'" + String(_chargeState) + "'},{'n':'battery-adc','v':'" + String(_batteryAdc) + "'} ]}", 60, PRIVATE);
    }
}


void PowerManagementV2::appendSenML(SenMLBuilder* builder) {
    //Particle.publish("senml", "{e:[{'n':'ps-voltage','v':'" + String(_cellVoltage) + "'},{'n':'ps-soc','v':'" + String(_stateOfCharge) + "'},{'n':'charge-status','v':'" + String(_chargeState) + "'},{'n':'battery-adc','v':'" + String(_batteryAdc) + "'} ]}", 60, PRIVATE);
    
    builder->add("ps-volts", _cellVoltage);
    builder->add("ps-soc", _stateOfCharge);
    builder->add("charging", _chargeState);
    builder->add("batt-adc", _batteryAdc);
    builder->add("batt-volts", _batteryVoltageAnalog);
}

PowerManagement.h

C/C++
#ifndef PowerManagement_H
#define PowerManagement_H

#include "PowerShield/PowerShield.h"
#include "SenMLBuilder.h"

///////////////////////////////////////////////////////////////////////////////////
// Base
///////////////////////////////////////////////////////////////////////////////////
// Responsible for reading the LiPol battery voltage and charge.
class PowerManagement {
public:
    PowerManagement();
    ~PowerManagement();
    
    virtual bool init();
    // Pure virtual - must be overridden
    virtual void readPowerState(bool publish) = 0;
    
    // Append sensor measurements to SenML Builder
    // for publication.
    virtual void appendSenML(SenMLBuilder* builder);

protected:

private:
    
};

///////////////////////////////////////////////////////////////////////////////////
// V1
///////////////////////////////////////////////////////////////////////////////////
// V1.1 board does not support power management
class PowerManagementV1 : public PowerManagement {
public:
    PowerManagementV1();
    void readPowerState(bool publish) override;
};

///////////////////////////////////////////////////////////////////////////////////
// V2
///////////////////////////////////////////////////////////////////////////////////
// V1.2 board has battery/charger and monitor.
class PowerManagementV2 : public PowerManagement {
public:
    PowerManagementV2();
    bool init() override;
    void readPowerState(bool publish) override;
    void appendSenML(SenMLBuilder* builder) override;
private:
    PowerShield _batteryMonitor;
    // Battery is connected to ADC via potential divider on pin...
    int _batteryMonitorAnalogPin = A4;
    int _chargeStatusPin = A3;
    float _cellVoltage;
    float _stateOfCharge;
    int _chargeState;
    // Battery voltage as measured via ADC
    int _batteryAdc;
    float _batteryVoltageAnalog;
};

#endif

SenMLBuilder.cpp

C/C++
#include "application.h"
#include "SenMLBuilder.h"

// Ctor.
SenMLBuilder::SenMLBuilder() {
    // Maximum size of a published message is 255 characters.
    _senml.reserve(255);
}

/**
 * @brief Destructor
 */
SenMLBuilder::~SenMLBuilder() {
}

/**
 * @brief Start a new SenML document.
 *
 */
void SenMLBuilder::begin() {
    // Clear out the old string buffer
    _senml = "{e:[";
    _first = true;
    _terminated = false;
}

void SenMLBuilder::prependComma() {
    // Prepend a comma to anything but the first measurement
    // to seperate the json objects in the senml.e array.
    if (!_first) {
        _senml += ",";
    }
    _first = false;
}

void SenMLBuilder::add(String fieldName, String value) {
    // {'n':'Temperature','v':'22'}
    prependComma();
    _senml += "{'n':'" + fieldName + "','v':'" + value + "'},";
    
}

void SenMLBuilder::add(String fieldName, int value) {
    // {'n':'Temperature','v':'22'}
    prependComma();
    _senml += "{'n':'" + fieldName + "','v':'" + String(value) + "'}";
}

void SenMLBuilder::add(String fieldName, float value) {
    prependComma();
    _senml += "{'n':'" + fieldName + "','v':'" + String(value) + "'}";
}

void SenMLBuilder::add(String fieldName, uint16_t value) {
    prependComma();
    _senml += "{'n':'" + fieldName + "','v':'" + String(value) + "'}";
}

// Complete the document and convert it to a string
// This method may be called repeatedly.
String SenMLBuilder::toString() {
    // Add each or the recorded sensor measurements
    // {'n':'Temperature','v':'" + String(environmentSensors->getTemperature()) + "'}
    //
    if (!_terminated) {
        _senml += "]}";
        _terminated = true;
    }
    
    return _senml;
}

// Publish the senml document
// private. 60s ttl, name="senml"
// This method may be called repeatedly in case of failure.
bool SenMLBuilder::publish() {
    return Particle.publish("senml", toString(), 60, PRIVATE);
}

SenMLBuilder.h

C/C++
 
#ifndef SenMLBuilder_H
#define SenMLBuilder_H

// Build a SenML json document
// Note: Max document length is 255 characters
// as set by the max data size for Particle.Publish()
// see https://docs.particle.io/reference/firmware/photon/#particle-publish-
// For senml format see http://tools.ietf.org/html/draft-jennings-senml-10
class SenMLBuilder {
public:
    SenMLBuilder();
    ~SenMLBuilder();
    
    
    // Start building the SenML string
    void begin();
    
    void add(String fieldName, String value);
    void add(String fieldName, int value);
    void add(String fieldName, float value);
    void add(String fieldName, uint16_t value);
    
    // Create the SenML document
    String toString();
    
    // Publish the senml document
    bool publish();
private:
    String _senml;
    bool _first = true;
    bool _terminated = false;
    void prependComma();
};

#endif

SparkFun_APDS9960.cpp

C/C++
Author: Shawn Hymel (SparkFun Electronics) - I owe you a beer!
/**
 * @file    SparkFun_APDS-9960.cpp
 * @brief   Library for the SparkFun APDS-9960 breakout board
 * @author  Shawn Hymel (SparkFun Electronics)
 *
 * @copyright	This code is public domain but you buy me a beer if you use
 * this and we meet someday (Beerware license).
 *
 * This library interfaces the Avago APDS-9960 to Arduino over I2C. The library
 * relies on the Arduino Wire (I2C) library. to use the library, instantiate an
 * APDS9960 object, call init(), and call the appropriate functions.
 *
 * APDS-9960 current draw tests (default parameters):
 *   Off:                   1mA
 *   Waiting for gesture:   14mA
 *   Gesture in progress:   35mA
 * 
 * SH: Modified library to work with Photon and added 100uS delay 
 * to wireReadDataByte to improve stability.
 */
 
 #include "application.h"
 #include "SparkFun_APDS9960.h"
 
SparkFun_APDS9960::SparkFun_APDS9960()
{
    gesture_ud_delta_ = 0;
    gesture_lr_delta_ = 0;
    
    gesture_ud_count_ = 0;
    gesture_lr_count_ = 0;
    
    gesture_near_count_ = 0;
    gesture_far_count_ = 0;
    
    gesture_state_ = 0;
    gesture_motion_ = DIR_NONE;
}

/**
 * @brief Destructor
 */
SparkFun_APDS9960::~SparkFun_APDS9960()
{

}

/**
 * @brief Configures I2C communications and initializes registers to defaults
 *
 * @return True if initialized successfully. False otherwise.
 */
int SparkFun_APDS9960::init()
{
    uint8_t id;

    /* Initialize I2C */
    Wire.begin();
    
    /* Read ID register and check against known values for APDS-9960 */
    if( !wireReadDataByte(APDS9960_ID, id) ) {
        return -1;
    }
    
    // SH: My sensor does not register either of these two.
    if( !(id == APDS9960_ID_1 || id == APDS9960_ID_2) ) {
        //Spark.publish("Setup", "Unexpected APDS Id: " + String(id) );
        return -2;
    }
    
     /* Set ENABLE register to 0 (disable all features) */
    if( !setMode(ALL, OFF) ) {
        return -3;
        //return false;
    }
    
     /* Set default values for ambient light and proximity registers */
    if( !wireWriteDataByte(APDS9960_ATIME, DEFAULT_ATIME) ) {
        return -4;
    }
    if( !wireWriteDataByte(APDS9960_WTIME, DEFAULT_WTIME) ) {
        return -5;
    }
    if( !wireWriteDataByte(APDS9960_PPULSE, DEFAULT_PROX_PPULSE) ) {
        return -6;
    }
    if( !wireWriteDataByte(APDS9960_POFFSET_UR, DEFAULT_POFFSET_UR) ) {
        return -7;
    }
    if( !wireWriteDataByte(APDS9960_POFFSET_DL, DEFAULT_POFFSET_DL) ) {
        return -8;
    }
    if( !wireWriteDataByte(APDS9960_CONFIG1, DEFAULT_CONFIG1) ) {
        return -9;
    }
    
    if( !setLEDDrive(DEFAULT_LDRIVE) ) {
        return -10;
    }
    
    if( !setProximityGain(DEFAULT_PGAIN) ) {
        return -11;
    }
    if( !setAmbientLightGain(DEFAULT_AGAIN) ) {
        return -12;
    }
    if( !setProxIntLowThresh(DEFAULT_PILT) ) {
        return -13;
    }
    if( !setProxIntHighThresh(DEFAULT_PIHT) ) {
        return -14;
    }
    if( !setLightIntLowThreshold(DEFAULT_AILT) ) {
        return -15;
    }
    if( !setLightIntHighThreshold(DEFAULT_AIHT) ) {
        return -16;
    }
    if( !wireWriteDataByte(APDS9960_PERS, DEFAULT_PERS) ) {
        return -17;
    }
    if( !wireWriteDataByte(APDS9960_CONFIG2, DEFAULT_CONFIG2) ) {
        return -18;
    }
    if( !wireWriteDataByte(APDS9960_CONFIG3, DEFAULT_CONFIG3) ) {
        return -19;
    }
    
    /* Set default values for gesture sense registers */
    if( !setGestureEnterThresh(DEFAULT_GPENTH) ) {
        return -20;
    }
    if( !setGestureExitThresh(DEFAULT_GEXTH) ) {
        return -21;
    }
    if( !wireWriteDataByte(APDS9960_GCONF1, DEFAULT_GCONF1) ) {
        return -22;
    }
    if( !setGestureGain(DEFAULT_GGAIN) ) {
        return -23;
    }
    if( !setGestureLEDDrive(DEFAULT_GLDRIVE) ) {
        return -24;
    }
    if( !setGestureWaitTime(DEFAULT_GWTIME) ) {
        return -25;
    }
    if( !wireWriteDataByte(APDS9960_GOFFSET_U, DEFAULT_GOFFSET) ) {
        return -26;
    }
    if( !wireWriteDataByte(APDS9960_GOFFSET_D, DEFAULT_GOFFSET) ) {
        return -27;
    }
    if( !wireWriteDataByte(APDS9960_GOFFSET_L, DEFAULT_GOFFSET) ) {
        return -28;
    }
    if( !wireWriteDataByte(APDS9960_GOFFSET_R, DEFAULT_GOFFSET) ) {
        return -29;
    }
    if( !wireWriteDataByte(APDS9960_GPULSE, DEFAULT_GPULSE) ) {
        return -30;
    }
    if( !wireWriteDataByte(APDS9960_GCONF3, DEFAULT_GCONF3) ) {
        return -31;
    }
    if( !setGestureIntEnable(DEFAULT_GIEN) ) {
        return -32;
    }
    
    #if 0
    /* Gesture config register dump */
    uint8_t reg;
    uint8_t val;
  
    for(reg = 0x80; reg <= 0xAF; reg++) {
        if( (reg != 0x82) && \
            (reg != 0x8A) && \
            (reg != 0x91) && \
            (reg != 0xA8) && \
            (reg != 0xAC) && \
            (reg != 0xAD) )
        {
            wireReadDataByte(reg, val);
            // Serial.print(reg, HEX);
            // Serial.print(": 0x");
            // Serial.println(val, HEX);
        }
    }

    for(reg = 0xE4; reg <= 0xE7; reg++) {
        wireReadDataByte(reg, val);
        // Serial.print(reg, HEX);
        // Serial.print(": 0x");
        // Serial.println(val, HEX);
    }
#endif
     
    return 0;
}

/*******************************************************************************
 * Public methods for controlling the APDS-9960
 ******************************************************************************/

/**
 * @brief Reads and returns the contents of the ENABLE register
 *
 * @return Contents of the ENABLE register. 0xFF if error.
 */
uint8_t SparkFun_APDS9960::getMode()
{
    uint8_t enable_value;
    
    /* Read current ENABLE register */
    if( !wireReadDataByte(APDS9960_ENABLE, enable_value) ) {
        return ERROR;
    }
    
    return enable_value;
}

/**
 * @brief Enables or disables a feature in the APDS-9960
 *
 * @param[in] mode which feature to enable
 * @param[in] enable ON (1) or OFF (0)
 * @return True if operation success. False otherwise.
 */
bool SparkFun_APDS9960::setMode(uint8_t mode, uint8_t enable)
{
    uint8_t reg_val;

    /* Read current ENABLE register */
    reg_val = getMode();
    if( reg_val == ERROR ) {
        return false;
    }
    
    /* Change bit(s) in ENABLE register */
    enable = enable & 0x01;
    if( mode >= 0 && mode <= 6 ) {
        if (enable) {
            reg_val |= (1 << mode);
        } else {
            reg_val &= ~(1 << mode);
        }
    } else if( mode == ALL ) {
        if (enable) {
            reg_val = 0x7F;
        } else {
            reg_val = 0x00;
        }
    }
        
    /* Write value back to ENABLE register */
    if( !wireWriteDataByte(APDS9960_ENABLE, reg_val) ) {
        return false;
    }
        
    return true;
}


/**
 * @brief Starts the light (R/G/B/Ambient) sensor on the APDS-9960
 *
 * @param[in] interrupts true to enable hardware interrupt on high or low light
 * @return True if sensor enabled correctly. False on error.
 */
bool SparkFun_APDS9960::enableLightSensor(bool interrupts)
{
    
    /* Set default gain, interrupts, enable power, and enable sensor */
    if( !setAmbientLightGain(DEFAULT_AGAIN) ) {
        return false;
    }
    if( interrupts ) {
        if( !setAmbientLightIntEnable(1) ) {
            return false;
        }
    } else {
        if( !setAmbientLightIntEnable(0) ) {
            return false;
        }
    }
    if( !enablePower() ){
        return false;
    }
    if( !setMode(AMBIENT_LIGHT, 1) ) {
        return false;
    }
    
    return true;

}

/**
 * @brief Ends the light sensor on the APDS-9960
 *
 * @return True if sensor disabled correctly. False on error.
 */
bool SparkFun_APDS9960::disableLightSensor()
{
    if( !setAmbientLightIntEnable(0) ) {
        return false;
    }
    if( !setMode(AMBIENT_LIGHT, 0) ) {
        return false;
    }
    
    return true;
}


/**
 * @brief Starts the proximity sensor on the APDS-9960
 *
 * @param[in] interrupts true to enable hardware external interrupt on proximity
 * @return True if sensor enabled correctly. False on error.
 */
bool SparkFun_APDS9960::enableProximitySensor(bool interrupts)
{
    /* Set default gain, LED, interrupts, enable power, and enable sensor */
    if( !setProximityGain(DEFAULT_PGAIN) ) {
        return false;
    }
    if( !setLEDDrive(DEFAULT_LDRIVE) ) {
        return false;
    }
    if( interrupts ) {
        if( !setProximityIntEnable(1) ) {
            return false;
        }
    } else {
        if( !setProximityIntEnable(0) ) {
            return false;
        }
    }
    if( !enablePower() ){
        return false;
    }
    if( !setMode(PROXIMITY, 1) ) {
        return false;
    }
    
    return true;
}

/**
 * @brief Ends the proximity sensor on the APDS-9960
 *
 * @return True if sensor disabled correctly. False on error.
 */
bool SparkFun_APDS9960::disableProximitySensor()
{
	if( !setProximityIntEnable(0) ) {
		return false;
	}
	if( !setMode(PROXIMITY, 0) ) {
		return false;
	}

	return true;
}


/**
 * @brief Starts the gesture recognition engine on the APDS-9960
 *
 * @param[in] interrupts true to enable hardware external interrupt on gesture
 * @return True if engine enabled correctly. False on error.
 */
bool SparkFun_APDS9960::enableGestureSensor(bool interrupts)
{
    
    /* Enable gesture mode
       Set ENABLE to 0 (power off)
       Set WTIME to 0xFF
       Set AUX to LED_BOOST_300
       Enable PON, WEN, PEN, GEN in ENABLE 
    */
    resetGestureParameters();
    if( !wireWriteDataByte(APDS9960_WTIME, 0xFF) ) {
        return false;
    }
    if( !wireWriteDataByte(APDS9960_PPULSE, DEFAULT_GESTURE_PPULSE) ) {
        return false;
    }
    
    if( !setLEDBoost(LED_BOOST_100) ) {
        return false;
    }
    if( interrupts ) {
        if( !setGestureIntEnable(1) ) {
            return false;
        }
    } else {
        if( !setGestureIntEnable(0) ) {
            return false;
        }
    }
    if( !setGestureMode(1) ) {
        return false;
    }
    if( !enablePower() ){
        return false;
    }
    if( !setMode(WAIT, 1) ) {
        return false;
    }
    if( !setMode(PROXIMITY, 1) ) {
        return false;
    }
    if( !setMode(GESTURE, 1) ) {
        return false;
    }
    
    return true;
}

/**
 * @brief Ends the gesture recognition engine on the APDS-9960
 *
 * @return True if engine disabled correctly. False on error.
 */
bool SparkFun_APDS9960::disableGestureSensor()
{
    resetGestureParameters();
    if( !setGestureIntEnable(0) ) {
        return false;
    }
    if( !setGestureMode(0) ) {
        return false;
    }
    if( !setMode(GESTURE, 0) ) {
        return false;
    }
    
    return true;
}

/**
 * @brief Determines if there is a gesture available for reading
 *
 * @return True if gesture available. False otherwise.
 */
bool SparkFun_APDS9960::isGestureAvailable()
{
    uint8_t val;
    
    /* Read value from GSTATUS register */
    if( !wireReadDataByte(APDS9960_GSTATUS, val) ) {
        return ERROR;
    }
    
    /* Shift and mask out GVALID bit */
    val &= APDS9960_GVALID;
    
    /* Return true/false based on GVALID bit */
    if( val == 1) {
        return true;
    } else {
        return false;
    }
}


/**
 * @brief Processes a gesture event and returns best guessed gesture
 *
 * @return Number corresponding to gesture. -1 on error.
 */
int SparkFun_APDS9960::readGesture()
{
    uint8_t fifo_level = 0;
    uint8_t bytes_read = 0;
    uint8_t fifo_data[128];
    uint8_t gstatus;
    int motion;
    int i;
    
    /* Make sure that power and gesture is on and data is valid */
    if( !isGestureAvailable() || !(getMode() & 0b01000001) ) {
        return DIR_NONE;
    }
    
    /* Keep looping as long as gesture data is valid */
    while(1) {
    
        /* Wait some time to collect next batch of FIFO data */
        delay(FIFO_PAUSE_TIME);
        
        /* Get the contents of the STATUS register. Is data still valid? */
        if( !wireReadDataByte(APDS9960_GSTATUS, gstatus) ) {
            return ERROR;
        }
        
        /* If we have valid data, read in FIFO */
        if( (gstatus & APDS9960_GVALID) == APDS9960_GVALID ) {
        
            /* Read the current FIFO level */
            if( !wireReadDataByte(APDS9960_GFLVL, fifo_level) ) {
                return ERROR;
            }

#if DEBUG
            Serial.print("FIFO Level: ");
            Serial.println(fifo_level);
#endif

            /* If there's stuff in the FIFO, read it into our data block */
            if( fifo_level > 0) {
                bytes_read = wireReadDataBlock(  APDS9960_GFIFO_U, 
                                                (uint8_t*)fifo_data, 
                                                (fifo_level * 4) );
                if( bytes_read == -1 ) {
                    return ERROR;
                }
#if DEBUG
                Serial.print("FIFO Dump: ");
                for ( i = 0; i < bytes_read; i++ ) {
                    Serial.print(fifo_data[i]);
                    Serial.print(" ");
                }
                Serial.println();
#endif

                /* If at least 1 set of data, sort the data into U/D/L/R */
                if( bytes_read >= 4 ) {
                    for( i = 0; i < bytes_read; i += 4 ) {
                        gesture_data_.u_data[gesture_data_.index] = \
                                                            fifo_data[i + 0];
                        gesture_data_.d_data[gesture_data_.index] = \
                                                            fifo_data[i + 1];
                        gesture_data_.l_data[gesture_data_.index] = \
                                                            fifo_data[i + 2];
                        gesture_data_.r_data[gesture_data_.index] = \
                                                            fifo_data[i + 3];
                        gesture_data_.index++;
                        gesture_data_.total_gestures++;
                    }
                    
#if DEBUG
                Serial.print("Up Data: ");
                for ( i = 0; i < gesture_data_.total_gestures; i++ ) {
                    Serial.print(gesture_data_.u_data[i]);
                    Serial.print(" ");
                }
                Serial.println();
#endif

                    /* Filter and process gesture data. Decode near/far state */
                    if( processGestureData() ) {
                        if( decodeGesture() ) {
                            //***TODO: U-Turn Gestures
#if DEBUG
                            //Serial.println(gesture_motion_);
#endif
                        }
                    }
                    
                    /* Reset data */
                    gesture_data_.index = 0;
                    gesture_data_.total_gestures = 0;
                }
            }
        } else {
    
            /* Determine best guessed gesture and clean up */
            delay(FIFO_PAUSE_TIME);
            decodeGesture();
            motion = gesture_motion_;
#if DEBUG
            Serial.print("END: ");
            Serial.println(gesture_motion_);
#endif
            resetGestureParameters();
            return motion;
        }
    }
}


/**
 * Turn the APDS-9960 on
 *
 * @return True if operation successful. False otherwise.
 */
bool SparkFun_APDS9960::enablePower()
{
    if( !setMode(POWER, 1) ) {
        return false;
    }
    
    return true;
}

/**
 * Turn the APDS-9960 off
 *
 * @return True if operation successful. False otherwise.
 */
bool SparkFun_APDS9960::disablePower()
{
    if( !setMode(POWER, 0) ) {
        return false;
    }
    
    return true;
}

/*******************************************************************************
 * Ambient light and color sensor controls
 ******************************************************************************/

/**
 * @brief Reads the ambient (clear) light level as a 16-bit value
 *
 * @param[out] val value of the light sensor.
 * @return True if operation successful. False otherwise.
 */
bool SparkFun_APDS9960::readAmbientLight(uint16_t &val)
{
    uint8_t val_byte;
    val = 0;

    /* Read value from clear channel, low byte register */
    if( !wireReadDataByte(APDS9960_CDATAL, val_byte) ) {
        return false;
    }
    val = val_byte;
    
    /* Read value from clear channel, high byte register */
    if( !wireReadDataByte(APDS9960_CDATAH, val_byte) ) {
        return false;
    }
    
    val = val + ((uint16_t)val_byte << 8);
    return true;
}

/**
 * @brief Reads the red light level as a 16-bit value
 *
 * @param[out] val value of the light sensor.
 * @return True if operation successful. False otherwise.
 */
bool SparkFun_APDS9960::readRedLight(uint16_t &val)
{
    uint8_t val_byte;
    val = 0;

    /* Read value from clear channel, low byte register */
    if( !wireReadDataByte(APDS9960_RDATAL, val_byte) ) {
        return false;
    }
    val = val_byte;
    
    /* Read value from clear channel, high byte register */
    if( !wireReadDataByte(APDS9960_RDATAH, val_byte) ) {
        return false;
    }
    val = val + ((uint16_t)val_byte << 8);
    
    return true;
}
 
/**
 * @brief Reads the green light level as a 16-bit value
 *
 * @param[out] val value of the light sensor.
 * @return True if operation successful. False otherwise.
 */
bool SparkFun_APDS9960::readGreenLight(uint16_t &val)
{
    uint8_t val_byte;
    val = 0;

    /* Read value from clear channel, low byte register */
    if( !wireReadDataByte(APDS9960_GDATAL, val_byte) ) {
        return false;
    }
    val = val_byte;
    
    /* Read value from clear channel, high byte register */
    if( !wireReadDataByte(APDS9960_GDATAH, val_byte) ) {
        return false;
    }
    val = val + ((uint16_t)val_byte << 8);
    
    return true;
}

/**
 * @brief Reads the red light level as a 16-bit value
 *
 * @param[out] val value of the light sensor.
 * @return True if operation successful. False otherwise.
 */
bool SparkFun_APDS9960::readBlueLight(uint16_t &val)
{
    uint8_t val_byte;
    val = 0;

    /* Read value from clear channel, low byte register */
    if( !wireReadDataByte(APDS9960_BDATAL, val_byte) ) {
        return false;
    }
    val = val_byte;
    
    /* Read value from clear channel, high byte register */
    if( !wireReadDataByte(APDS9960_BDATAH, val_byte) ) {
        return false;
    }
    val = val + ((uint16_t)val_byte << 8);

    return true;
}


/*******************************************************************************
 * Proximity sensor controls
 ******************************************************************************/

/**
 * @brief Reads the proximity level as an 8-bit value
 *
 * @param[out] val value of the proximity sensor.
 * @return True if operation successful. False otherwise.
 */
bool SparkFun_APDS9960::readProximity(uint8_t &val)
{
    val = 0;
    
    /* Read value from proximity data register */
    if( !wireReadDataByte(APDS9960_PDATA, val) ) {
        return false;
    }
    
    return true;
}

/*******************************************************************************
 * High-level gesture controls
 ******************************************************************************/

/**
 * @brief Resets all the parameters in the gesture data member
 */
void SparkFun_APDS9960::resetGestureParameters()
{
    gesture_data_.index = 0;
    gesture_data_.total_gestures = 0;
    
    gesture_ud_delta_ = 0;
    gesture_lr_delta_ = 0;
    
    gesture_ud_count_ = 0;
    gesture_lr_count_ = 0;
    
    gesture_near_count_ = 0;
    gesture_far_count_ = 0;
    
    gesture_state_ = 0;
    gesture_motion_ = DIR_NONE;
}

/**
 * @brief Processes the raw gesture data to determine swipe direction
 *
 * @return True if near or far state seen. False otherwise.
 */
bool SparkFun_APDS9960::processGestureData()
{
    uint8_t u_first = 0;
    uint8_t d_first = 0;
    uint8_t l_first = 0;
    uint8_t r_first = 0;
    uint8_t u_last = 0;
    uint8_t d_last = 0;
    uint8_t l_last = 0;
    uint8_t r_last = 0;
    int ud_ratio_first;
    int lr_ratio_first;
    int ud_ratio_last;
    int lr_ratio_last;
    int ud_delta;
    int lr_delta;
    int i;

    /* If we have less than 4 total gestures, that's not enough */
    if( gesture_data_.total_gestures <= 4 ) {
        return false;
    }
    
    /* Check to make sure our data isn't out of bounds */
    if( (gesture_data_.total_gestures <= 32) && \
        (gesture_data_.total_gestures > 0) ) {
        
        /* Find the first value in U/D/L/R above the threshold */
        for( i = 0; i < gesture_data_.total_gestures; i++ ) {
            if( (gesture_data_.u_data[i] > GESTURE_THRESHOLD_OUT) &&
                (gesture_data_.d_data[i] > GESTURE_THRESHOLD_OUT) &&
                (gesture_data_.l_data[i] > GESTURE_THRESHOLD_OUT) &&
                (gesture_data_.r_data[i] > GESTURE_THRESHOLD_OUT) ) {
                
                u_first = gesture_data_.u_data[i];
                d_first = gesture_data_.d_data[i];
                l_first = gesture_data_.l_data[i];
                r_first = gesture_data_.r_data[i];
                break;
            }
        }
        
        /* If one of the _first values is 0, then there is no good data */
        if( (u_first == 0) || (d_first == 0) || \
            (l_first == 0) || (r_first == 0) ) {
            
            return false;
        }
        /* Find the last value in U/D/L/R above the threshold */
        for( i = gesture_data_.total_gestures - 1; i >= 0; i-- ) {
#if DEBUG
            Serial.print(F("Finding last: "));
            Serial.print(F("U:"));
            Serial.print(gesture_data_.u_data[i]);
            Serial.print(F(" D:"));
            Serial.print(gesture_data_.d_data[i]);
            Serial.print(F(" L:"));
            Serial.print(gesture_data_.l_data[i]);
            Serial.print(F(" R:"));
            Serial.println(gesture_data_.r_data[i]);
#endif
            if( (gesture_data_.u_data[i] > GESTURE_THRESHOLD_OUT) &&
                (gesture_data_.d_data[i] > GESTURE_THRESHOLD_OUT) &&
                (gesture_data_.l_data[i] > GESTURE_THRESHOLD_OUT) &&
                (gesture_data_.r_data[i] > GESTURE_THRESHOLD_OUT) ) {
                
                u_last = gesture_data_.u_data[i];
                d_last = gesture_data_.d_data[i];
                l_last = gesture_data_.l_data[i];
                r_last = gesture_data_.r_data[i];
                break;
            }
        }
    }
    
    /* Calculate the first vs. last ratio of up/down and left/right */
    ud_ratio_first = ((u_first - d_first) * 100) / (u_first + d_first);
    lr_ratio_first = ((l_first - r_first) * 100) / (l_first + r_first);
    ud_ratio_last = ((u_last - d_last) * 100) / (u_last + d_last);
    lr_ratio_last = ((l_last - r_last) * 100) / (l_last + r_last);
       
#if DEBUG
    Serial.print(F("Last Values: "));
    Serial.print(F("U:"));
    Serial.print(u_last);
    Serial.print(F(" D:"));
    Serial.print(d_last);
    Serial.print(F(" L:"));
    Serial.print(l_last);
    Serial.print(F(" R:"));
    Serial.println(r_last);

    Serial.print(F("Ratios: "));
    Serial.print(F("UD Fi: "));
    Serial.print(ud_ratio_first);
    Serial.print(F(" UD La: "));
    Serial.print(ud_ratio_last);
    Serial.print(F(" LR Fi: "));
    Serial.print(lr_ratio_first);
    Serial.print(F(" LR La: "));
    Serial.println(lr_ratio_last);
#endif
       
    /* Determine the difference between the first and last ratios */
    ud_delta = ud_ratio_last - ud_ratio_first;
    lr_delta = lr_ratio_last - lr_ratio_first;
    
#if DEBUG
    Serial.print("Deltas: ");
    Serial.print("UD: ");
    Serial.print(ud_delta);
    Serial.print(" LR: ");
    Serial.println(lr_delta);
#endif

    /* Accumulate the UD and LR delta values */
    gesture_ud_delta_ += ud_delta;
    gesture_lr_delta_ += lr_delta;
    
#if DEBUG
    Serial.print("Accumulations: ");
    Serial.print("UD: ");
    Serial.print(gesture_ud_delta_);
    Serial.print(" LR: ");
    Serial.println(gesture_lr_delta_);
#endif
    
    /* Determine U/D gesture */
    if( gesture_ud_delta_ >= GESTURE_SENSITIVITY_1 ) {
        gesture_ud_count_ = 1;
    } else if( gesture_ud_delta_ <= -GESTURE_SENSITIVITY_1 ) {
        gesture_ud_count_ = -1;
    } else {
        gesture_ud_count_ = 0;
    }
    
    /* Determine L/R gesture */
    if( gesture_lr_delta_ >= GESTURE_SENSITIVITY_1 ) {
        gesture_lr_count_ = 1;
    } else if( gesture_lr_delta_ <= -GESTURE_SENSITIVITY_1 ) {
        gesture_lr_count_ = -1;
    } else {
        gesture_lr_count_ = 0;
    }
    
    /* Determine Near/Far gesture */
    if( (gesture_ud_count_ == 0) && (gesture_lr_count_ == 0) ) {
        if( (abs(ud_delta) < GESTURE_SENSITIVITY_2) && \
            (abs(lr_delta) < GESTURE_SENSITIVITY_2) ) {
            
            if( (ud_delta == 0) && (lr_delta == 0) ) {
                gesture_near_count_++;
            } else if( (ud_delta != 0) || (lr_delta != 0) ) {
                gesture_far_count_++;
            }
            
            if( (gesture_near_count_ >= 10) && (gesture_far_count_ >= 2) ) {
                if( (ud_delta == 0) && (lr_delta == 0) ) {
                    gesture_state_ = NEAR_STATE;
                } else if( (ud_delta != 0) && (lr_delta != 0) ) {
                    gesture_state_ = FAR_STATE;
                }
                return true;
            }
        }
    } else {
        if( (abs(ud_delta) < GESTURE_SENSITIVITY_2) && \
            (abs(lr_delta) < GESTURE_SENSITIVITY_2) ) {
                
            if( (ud_delta == 0) && (lr_delta == 0) ) {
                gesture_near_count_++;
            }
            
            if( gesture_near_count_ >= 10 ) {
                gesture_ud_count_ = 0;
                gesture_lr_count_ = 0;
                gesture_ud_delta_ = 0;
                gesture_lr_delta_ = 0;
            }
        }
    }
    
#if DEBUG
    Serial.print("UD_CT: ");
    Serial.print(gesture_ud_count_);
    Serial.print(" LR_CT: ");
    Serial.print(gesture_lr_count_);
    Serial.print(" NEAR_CT: ");
    Serial.print(gesture_near_count_);
    Serial.print(" FAR_CT: ");
    Serial.println(gesture_far_count_);
    Serial.println("----------");
#endif
    
    return false;
}

/**
 * @brief Determines swipe direction or near/far state
 *
 * @return True if near/far event. False otherwise.
 */
bool SparkFun_APDS9960::decodeGesture()
{
    /* Return if near or far event is detected */
    if( gesture_state_ == NEAR_STATE ) {
        gesture_motion_ = DIR_NEAR;
        return true;
    } else if ( gesture_state_ == FAR_STATE ) {
        gesture_motion_ = DIR_FAR;
        return true;
    }
    
    /* Determine swipe direction */
    if( (gesture_ud_count_ == -1) && (gesture_lr_count_ == 0) ) {
        gesture_motion_ = DIR_UP;
    } else if( (gesture_ud_count_ == 1) && (gesture_lr_count_ == 0) ) {
        gesture_motion_ = DIR_DOWN;
    } else if( (gesture_ud_count_ == 0) && (gesture_lr_count_ == 1) ) {
        gesture_motion_ = DIR_RIGHT;
    } else if( (gesture_ud_count_ == 0) && (gesture_lr_count_ == -1) ) {
        gesture_motion_ = DIR_LEFT;
    } else if( (gesture_ud_count_ == -1) && (gesture_lr_count_ == 1) ) {
        if( abs(gesture_ud_delta_) > abs(gesture_lr_delta_) ) {
            gesture_motion_ = DIR_UP;
        } else {
            gesture_motion_ = DIR_RIGHT;
        }
...

This file has been truncated, please download it to see its full contents.

SparkFun_APDS9960.h

C/C++
Author: Shawn Hymel (SparkFun Electronics)
/**
 * @file    SparkFun_APDS-9960.h
 * @brief   Library for the SparkFun APDS-9960 breakout board
 * @author  Shawn Hymel (SparkFun Electronics)
 *
 * @copyright	This code is public domain but you buy me a beer if you use
 * this and we meet someday (Beerware license).
 *
 * This library interfaces the Avago APDS-9960 to Arduino over I2C. The library
 * relies on the Arduino Wire (I2C) library. to use the library, instantiate an
 * APDS9960 object, call init(), and call the appropriate functions.
 */
 
#ifndef SparkFun_APDS9960_H
#define SparkFun_APDS9960_H

/* Debug */
#define DEBUG                   0

/* APDS-9960 I2C address */
#define APDS9960_I2C_ADDR       0x39

/* Gesture parameters */
#define GESTURE_THRESHOLD_OUT   10
#define GESTURE_SENSITIVITY_1   50
#define GESTURE_SENSITIVITY_2   20

/* Error code for returned values */
#define ERROR                   0xFF

/* Acceptable device IDs */
#define APDS9960_ID_1           0xAB
#define APDS9960_ID_2           0x9C 

/* Misc parameters */
#define FIFO_PAUSE_TIME         30      // Wait period (ms) between FIFO reads

/* APDS-9960 register addresses */
#define APDS9960_ENABLE         0x80
#define APDS9960_ATIME          0x81
#define APDS9960_WTIME          0x83
#define APDS9960_AILTL          0x84
#define APDS9960_AILTH          0x85
#define APDS9960_AIHTL          0x86
#define APDS9960_AIHTH          0x87
#define APDS9960_PILT           0x89
#define APDS9960_PIHT           0x8B
#define APDS9960_PERS           0x8C
#define APDS9960_CONFIG1        0x8D
#define APDS9960_PPULSE         0x8E
#define APDS9960_CONTROL        0x8F
#define APDS9960_CONFIG2        0x90
#define APDS9960_ID             0x92
#define APDS9960_STATUS         0x93
#define APDS9960_CDATAL         0x94
#define APDS9960_CDATAH         0x95
#define APDS9960_RDATAL         0x96
#define APDS9960_RDATAH         0x97
#define APDS9960_GDATAL         0x98
#define APDS9960_GDATAH         0x99
#define APDS9960_BDATAL         0x9A
#define APDS9960_BDATAH         0x9B
#define APDS9960_PDATA          0x9C
#define APDS9960_POFFSET_UR     0x9D
#define APDS9960_POFFSET_DL     0x9E
#define APDS9960_CONFIG3        0x9F
#define APDS9960_GPENTH         0xA0
#define APDS9960_GEXTH          0xA1
#define APDS9960_GCONF1         0xA2
#define APDS9960_GCONF2         0xA3
#define APDS9960_GOFFSET_U      0xA4
#define APDS9960_GOFFSET_D      0xA5
#define APDS9960_GOFFSET_L      0xA7
#define APDS9960_GOFFSET_R      0xA9
#define APDS9960_GPULSE         0xA6
#define APDS9960_GCONF3         0xAA
#define APDS9960_GCONF4         0xAB
#define APDS9960_GFLVL          0xAE
#define APDS9960_GSTATUS        0xAF
#define APDS9960_IFORCE         0xE4
#define APDS9960_PICLEAR        0xE5
#define APDS9960_CICLEAR        0xE6
#define APDS9960_AICLEAR        0xE7
#define APDS9960_GFIFO_U        0xFC
#define APDS9960_GFIFO_D        0xFD
#define APDS9960_GFIFO_L        0xFE
#define APDS9960_GFIFO_R        0xFF

/* Bit fields */
#define APDS9960_PON            0b00000001
#define APDS9960_AEN            0b00000010
#define APDS9960_PEN            0b00000100
#define APDS9960_WEN            0b00001000
#define APSD9960_AIEN           0b00010000
#define APDS9960_PIEN           0b00100000
#define APDS9960_GEN            0b01000000
#define APDS9960_GVALID         0b00000001

/* On/Off definitions */
#define OFF                     0
#define ON                      1

/* Acceptable parameters for setMode */
#define POWER                   0
#define AMBIENT_LIGHT           1
#define PROXIMITY               2
#define WAIT                    3
#define AMBIENT_LIGHT_INT       4
#define PROXIMITY_INT           5
#define GESTURE                 6
#define ALL                     7

/* LED Drive values */
#define LED_DRIVE_100MA         0
#define LED_DRIVE_50MA          1
#define LED_DRIVE_25MA          2
#define LED_DRIVE_12_5MA        3

/* Proximity Gain (PGAIN) values */
#define PGAIN_1X                0
#define PGAIN_2X                1
#define PGAIN_4X                2
#define PGAIN_8X                3

/* ALS Gain (AGAIN) values */
#define AGAIN_1X                0
#define AGAIN_4X                1
#define AGAIN_16X               2
#define AGAIN_64X               3

/* Gesture Gain (GGAIN) values */
#define GGAIN_1X                0
#define GGAIN_2X                1
#define GGAIN_4X                2
#define GGAIN_8X                3

/* LED Boost values */
#define LED_BOOST_100           0
#define LED_BOOST_150           1
#define LED_BOOST_200           2
#define LED_BOOST_300           3    

/* Gesture wait time values */
#define GWTIME_0MS              0
#define GWTIME_2_8MS            1
#define GWTIME_5_6MS            2
#define GWTIME_8_4MS            3
#define GWTIME_14_0MS           4
#define GWTIME_22_4MS           5
#define GWTIME_30_8MS           6
#define GWTIME_39_2MS           7

/* Default values */
#define DEFAULT_ATIME           219     // 103ms
#define DEFAULT_WTIME           246     // 27ms
#define DEFAULT_PROX_PPULSE     0x87    // 16us, 8 pulses
#define DEFAULT_GESTURE_PPULSE  0x89    // 16us, 10 pulses
#define DEFAULT_POFFSET_UR      0       // 0 offset
#define DEFAULT_POFFSET_DL      0       // 0 offset      
#define DEFAULT_CONFIG1         0x60    // No 12x wait (WTIME) factor
#define DEFAULT_LDRIVE          LED_DRIVE_100MA
#define DEFAULT_PGAIN           PGAIN_4X
#define DEFAULT_AGAIN           AGAIN_4X
#define DEFAULT_PILT            0       // Low proximity threshold
#define DEFAULT_PIHT            50      // High proximity threshold
#define DEFAULT_AILT            0xFFFF  // Force interrupt for calibration
#define DEFAULT_AIHT            0
#define DEFAULT_PERS            0x11    // 2 consecutive prox or ALS for int.
#define DEFAULT_CONFIG2         0x01    // No saturation interrupts or LED boost  
#define DEFAULT_CONFIG3         0       // Enable all photodiodes, no SAI
#define DEFAULT_GPENTH          40      // Threshold for entering gesture mode
#define DEFAULT_GEXTH           30      // Threshold for exiting gesture mode    
#define DEFAULT_GCONF1          0x40    // 4 gesture events for int., 1 for exit
#define DEFAULT_GGAIN           GGAIN_2X 
#define DEFAULT_GLDRIVE         LED_DRIVE_100MA
#define DEFAULT_GWTIME          GWTIME_2_8MS
#define DEFAULT_GOFFSET         0       // No offset scaling for gesture mode
#define DEFAULT_GPULSE          0xC9    // 32us, 10 pulses
#define DEFAULT_GCONF3          0       // All photodiodes active during gesture
#define DEFAULT_GIEN            0       // Disable gesture interrupts

/* Direction definitions */
enum {
  DIR_NONE,
  DIR_LEFT,
  DIR_RIGHT,
  DIR_UP,
  DIR_DOWN,
  DIR_NEAR,
  DIR_FAR,
  DIR_ALL
};

/* State definitions */
enum {
  NA_STATE,
  NEAR_STATE,
  FAR_STATE,
  ALL_STATE
};

/* Container for gesture data */
typedef struct gesture_data_type {
    uint8_t u_data[32];
    uint8_t d_data[32];
    uint8_t l_data[32];
    uint8_t r_data[32];
    uint8_t index;
    uint8_t total_gestures;
    uint8_t in_threshold;
    uint8_t out_threshold;
} gesture_data_type;

/* APDS9960 Class */
class SparkFun_APDS9960 {
public:

    /* Initialization methods */
    SparkFun_APDS9960();
    ~SparkFun_APDS9960();
    int init();
    uint8_t getMode();
    bool setMode(uint8_t mode, uint8_t enable);
    
    /* Turn the APDS-9960 on and off */
    bool enablePower();
    bool disablePower();
    
    /* Enable or disable specific sensors */
    bool enableLightSensor(bool interrupts = false);
    bool disableLightSensor();
    bool enableProximitySensor(bool interrupts = false);
    bool disableProximitySensor();
    bool enableGestureSensor(bool interrupts = true);
    bool disableGestureSensor();
    
    /* LED drive strength control */
    uint8_t getLEDDrive();
    bool setLEDDrive(uint8_t drive);
    uint8_t getGestureLEDDrive();
    bool setGestureLEDDrive(uint8_t drive);
    
    /* Gain control */
    uint8_t getAmbientLightGain();
    bool setAmbientLightGain(uint8_t gain);
    uint8_t getProximityGain();
    bool setProximityGain(uint8_t gain);
    uint8_t getGestureGain();
    bool setGestureGain(uint8_t gain);
    
    /* Get and set light interrupt thresholds */
    bool getLightIntLowThreshold(uint16_t &threshold);
    bool setLightIntLowThreshold(uint16_t threshold);
    bool getLightIntHighThreshold(uint16_t &threshold);
    bool setLightIntHighThreshold(uint16_t threshold);
    
    /* Get and set proximity interrupt thresholds */
    bool getProximityIntLowThreshold(uint8_t &threshold);
    bool setProximityIntLowThreshold(uint8_t threshold);
    bool getProximityIntHighThreshold(uint8_t &threshold);
    bool setProximityIntHighThreshold(uint8_t threshold);
    
    /* Get and set interrupt enables */
    uint8_t getAmbientLightIntEnable();
    bool setAmbientLightIntEnable(uint8_t enable);
    uint8_t getProximityIntEnable();
    bool setProximityIntEnable(uint8_t enable);
    uint8_t getGestureIntEnable();
    bool setGestureIntEnable(uint8_t enable);
    
    /* Clear interrupts */
    bool clearAmbientLightInt();
    bool clearProximityInt();
    
    /* Ambient light methods */
    bool readAmbientLight(uint16_t &val);
    bool readRedLight(uint16_t &val);
    bool readGreenLight(uint16_t &val);
    bool readBlueLight(uint16_t &val);
    
    /* Proximity methods */
    bool readProximity(uint8_t &val);
    
    /* Gesture methods */
    bool isGestureAvailable();
    int readGesture();
    
private:

    /* Gesture processing */
    void resetGestureParameters();
    bool processGestureData();
    bool decodeGesture();

    /* Proximity Interrupt Threshold */
    uint8_t getProxIntLowThresh();
    bool setProxIntLowThresh(uint8_t threshold);
    uint8_t getProxIntHighThresh();
    bool setProxIntHighThresh(uint8_t threshold);
    
    /* LED Boost Control */
    uint8_t getLEDBoost();
    bool setLEDBoost(uint8_t boost);
    
    /* Proximity photodiode select */
    uint8_t getProxGainCompEnable();
    bool setProxGainCompEnable(uint8_t enable);
    uint8_t getProxPhotoMask();
    bool setProxPhotoMask(uint8_t mask);
    
    /* Gesture threshold control */
    uint8_t getGestureEnterThresh();
    bool setGestureEnterThresh(uint8_t threshold);
    uint8_t getGestureExitThresh();
    bool setGestureExitThresh(uint8_t threshold);
    
    /* Gesture LED, gain, and time control */
    uint8_t getGestureWaitTime();
    bool setGestureWaitTime(uint8_t time);
    
    /* Gesture mode */
    uint8_t getGestureMode();
    bool setGestureMode(uint8_t mode);

    /* Raw I2C Commands */
    bool wireWriteByte(uint8_t val);
    bool wireWriteDataByte(uint8_t reg, uint8_t val);
    bool wireWriteDataBlock(uint8_t reg, uint8_t *val, unsigned int len);
    bool wireReadDataByte(uint8_t reg, uint8_t &val);
    int wireReadDataBlock(uint8_t reg, uint8_t *val, unsigned int len);

    /* Members */
    gesture_data_type gesture_data_;
    int gesture_ud_delta_;
    int gesture_lr_delta_;
    int gesture_ud_count_;
    int gesture_lr_count_;
    int gesture_near_count_;
    int gesture_far_count_;
    int gesture_state_;
    int gesture_motion_;
};

#endif

TouchSensors.cpp

C/C++
// Responsible for handling temperature and humidity measurements.

 #include "application.h"
 #include "TouchSensors.h"

///////////////////////////////////////////////////////////////////////////////////
// Base
///////////////////////////////////////////////////////////////////////////////////
// Ctor.
TouchSensors::TouchSensors(int touchInput, int touchVdd, LedHandler* leds) {
    _touchInput = touchInput;
    _touchVdd =  touchVdd;
     _leds = leds;
}

bool TouchSensors::init() {
   
   // IO Pin setup for touch in setupIO.
   // Touch
    pinMode(_touchInput, INPUT);
    pinMode(_touchVdd, OUTPUT);
    
    // Enable touch controller (1.1+ hardware)
    digitalWrite(_touchVdd, HIGH);
    
    // Attach interrupt to touchInput...
    auto interruptHandler = std::bind(&TouchSensors::touchInterrupt, this);
    attachInterrupt(_touchInput, interruptHandler, RISING);

    return true;
}

void TouchSensors::checkIfTouched() {
    if (_touched) {
        handleTouch();
    }
}

void TouchSensors::handleTouch() {
    noInterrupts();
        
    // Initial indiation that touch has been sensed.
    _leds->allLedsOn();
    
    int touchedCount = getTouchedCount();
    
    processTouch(touchedCount);
    
    delay(200);
    _leds->ledsOff();
    
    _touched = false;
    interrupts();
}

int TouchSensors::getTouchedCount() {
    int touchedCount = 0;
    bool touched;
    
    do {
        touched = digitalRead(_touchInput);
        
        if (touched) {
            touchedCount++;
            delay(250);
            
            // If long touch then flash left/right
            // leds to show long touch accepted
            if (touchedCount > _veryLongTouchThreshold) {
                _leds->ledsOff();
                if (touchedCount % 2 == 0) {
                    _leds->topLedsOn();
                } else {
                    _leds->bottomLedsOn();
                }
            } else if (touchedCount > _longTouchThreshold) {
                _leds->ledsOff();
                if (touchedCount % 2 == 0) {
                    _leds->leftLedsOn();
                } else {
                    _leds->rightLedsOn();
                }
            }
        }
    }
    while(touched);
    
    return touchedCount;
}

void TouchSensors::processTouch(int touchedCount) {
    if (touchedCount > _veryLongTouchThreshold) {
        doVeryLongTouchAction();
    } else if (touchedCount > _longTouchThreshold) {
        doLongTouchAction();
    } else {
        doTouchedAction();
    }
}

void TouchSensors::doTouchedAction() {
    Particle.publish("Status", "Touched");
}

void TouchSensors::doLongTouchAction() {
    Particle.publish("Status", "Long touch");
}

void TouchSensors::doVeryLongTouchAction() {
    Particle.publish("Status", "VERY Long touch");
}

void TouchSensors::touchInterrupt() {
    _touched = true;
}


///////////////////////////////////////////////////////////////////////////////////
// V1
///////////////////////////////////////////////////////////////////////////////////
TouchSensorsV1::TouchSensorsV1(LedHandler* leds) : TouchSensors(D5, D6, leds) {

}

///////////////////////////////////////////////////////////////////////////////////
// V2
///////////////////////////////////////////////////////////////////////////////////s
TouchSensorsV2::TouchSensorsV2(LedHandler* leds) : TouchSensors(D5, D6, leds) {

};

TouchSensors.h

C/C++
// Responsible for handling temperature and humidity measurements.

#ifndef TouchSensors_H
#define TouchSensors_H

#include "LedHandler.h"

///////////////////////////////////////////////////////////////////////////////////
// Base
///////////////////////////////////////////////////////////////////////////////////
class TouchSensors {
public:
    TouchSensors(int touchInput, int touchVdd, LedHandler* leds);
    bool init();
    void checkIfTouched();
private:
    void touchInterrupt();
    void handleTouch();
    int getTouchedCount();
    void processTouch(int touchedCount);
    void doTouchedAction();
    void doLongTouchAction();
    void doVeryLongTouchAction();
    LedHandler* _leds;
    int _touchInput;
    int _touchVdd;
    volatile int _touched = false;
    // Number of counts of touched (with a 250ms delay for each)
    int _longTouchThreshold = 4;
    int _veryLongTouchThreshold = 20;
};

///////////////////////////////////////////////////////////////////////////////////
// V1
///////////////////////////////////////////////////////////////////////////////////
class TouchSensorsV1 : public TouchSensors {
public:
    TouchSensorsV1(LedHandler* leds);

};

///////////////////////////////////////////////////////////////////////////////////
// V2
///////////////////////////////////////////////////////////////////////////////////
class TouchSensorsV2 : public TouchSensors {
public:
    TouchSensorsV2(LedHandler* leds);
};

#endif

Enclosure design

SCAD
Load into OpenSCAD
// -----------------------------------------------------------------
// Jedi Light Switch Enclosure.
// Designed to be an enclosure for the Jedi Light Switch PCB
// and fit over a UK (Possibly EU) Lightswitch.
// -----------------------------------------------------------------

// Define the switch type to be covered.
// This only effects the switch modeled to help visualise the case
// It does not effect the base or cover stl's.
// New style Single switch = 0
// New style Double switch = 1 - not implemented.
// Old style Single switch = 2
// Old style Double switch = 3
switchType  = 0; // Update depth also

// How deep the base & cover should be.
// For new style switch:
// depth = 16; // 19mm Gives plenty of room in new switches.
// For old style
depth = 19; // 28mm gives plenty
// Super skinny - helpful for test print to check sizes.
// depth = 5;

// Set to true to a minalist cover, goes a few mm over the PCB
// leaving most of the PCB exposed.
minimalCover = false;

// Which version of the PCB is to be used.
// The effects which cutouts are required
// at the top of the cover.
pcbVersion = 2;

// How deep to make the USB connector cutout.
// For ease of use this should be the same as, or deeper than 
// the depth of the cover so the cover can be fitted/removed 
// without disconnecting the USB connector.
usbConnectorCutoutDepth = depth;

// The gap to leave between the cover and the base when pushed together.
// Adjust this to give a tighter (smaller number) or looser (larger number) fit between
// the base and the cover. This will depend on your printer.
coverToBasegap = 0.2;

// Change these to show the various main components.
// They are modified through command line calls when generating STL files.
// e.g To generate the base stl file set showBase to true and everything else
// to false.
showCover = false;
showBase = false;
showPcb = true;
showSwitch = false;


// ------------------------------------------------------


            
// Size of the outer bezel that surounds the PCB
bezelSize = 2;

// Thickness of the cover wall.
// PCB overlap onto support = 2
// then add the bezel size to allow for an overall wall thickness.
wallThickness = 2 + bezelSize; // top wall thickness.
sideWallThickness = 2 + bezelSize;

// -----------------------------------------------------------------
// Jedi lightswitch PCB details
// -----------------------------------------------------------------
// PCB size we are trying to fit.
// PCB is 100, allow a small bodge
pcbHeight = 100.25;
pcbWidth = 100.25;
// How thick (deep) the front panel is (1.6mm FR4 PCB)
pcbThickness = 1.6;
// -----------------------------------------------------------------

// -----------------------------------------------------------------
// Compute the actual overall size required 
// for the outer enclosure.
// -----------------------------------------------------------------
height = pcbHeight + (2*bezelSize);
width = pcbWidth + (2*bezelSize);
// -----------------------------------------------------------------

// -----------------------------------------------------------------
// Wall Switch size (physical size of the light switch being covered)
// -----------------------------------------------------------------
switchHeight = pcbHeight-15; //88;
// switchWidth = 88;  // Original value.
switchWidth = 85; // Newer switches.
// How deep the switch is at the deepest part (e.g. to the top of the switches switch).
switchDepth = 22;
// How much the switch body should overlap onto the base
switchOverlap = 3;

middleDepth = 10;

// How deep the inside of the box is.
baseInnerThickness = 1;

// -----------------------------------------
// -----------------------------------------
module GenericBase(xDistance, yDistance, zHeight) {
	// roundedBox([xDistance, yDistance, zHeight], 2, true);

	// Create a rectangluar base to work from that
	// is xDistance by yDistance and zHeight height.

	// This is effectivly a cube with rounded corners

	// extend the base out by 3.5 from holes by using minkowski
	// which gives rounded corners to the board in the process
	// matching the Gadgeteer design
	
	$fn=50;
	radius = 5; //bezelSize;


	translate([radius,radius,0]) {
		minkowski()
		{
			// 3D Minkowski sum all dimensions will be the sum of the two object's dimensions
			cube([xDistance-(radius*2), yDistance-(radius*2), zHeight /2]);
			cylinder(r=radius,h=zHeight/2);
		}
	}
}



// -----------------------------------------
// Base for the switch, goes around existing lightswitch.
// -----------------------------------------
module LightSwitchBase() {
	
	difference() {
		union() 
		{
			// Outer base wall
			OuterWall();
		}		
		union() 
		{
			SwitchCutout();
			PcbCutout();
            CutOff();
		}
	}
}

// -----------------------------------------
// Main base outer wall
// -----------------------------------------
module OuterWall() {

    innerCutoutOffset = wallThickness; // bezelSize + x

	difference() {
		union() {
			GenericBase(width, height, depth);
		}
		union() {
			// Cut out the bulk of the inside of the box.
			translate([sideWallThickness, wallThickness, baseInnerThickness]) {
                
                // Rounded interial
				//GenericBase(width - (wallThickness* 2), 
				//					height - (innerCutoutOffset *2), 
				//					(depth - baseInnerThickness) + 1);
                
                // Square interia
                cube([width - (sideWallThickness * 2), 
                    height - (wallThickness *2), 
					(depth - baseInnerThickness) + 1]);
			}
		}
	}
}

// -----------------------------------------
// Cut out an area in the base for the switch allowing
// for an overlap to go under the switch which holds the case to the wall.
// -----------------------------------------
module SwitchCutout() {

	// Cutout smaller than the actual switch to allow for the overlap all around
	cutoutWidth = switchWidth - (switchOverlap*2);
	cutoutHeight = switchHeight - (switchOverlap*2);	

	// Padding either side of the cutout.
	paddingWidth = (width - cutoutWidth) / 2;
	paddingHeight = -10; // Fixed padding from top.

	// Switch cutout.
	// Cut out a area less wide than the switch so it sits on it 
	// keeping the box against the wall
	// -1 z to ensure it goes all the way through
	translate([paddingWidth, paddingHeight, -1]) {
		cube([cutoutWidth, height, 4]);
	}

	// Switch body
	// Create a block to show how the switch body sits
	// in the base.
	switchOuterPaddingWidth = (width - switchWidth) /2;
	switchOuterPaddingHeight = paddingHeight - switchOverlap;

	translate([switchOuterPaddingWidth , switchOuterPaddingHeight,1]) {
		color( [1, 0, 0, 0.90] ) {
            cube([switchWidth, switchHeight, switchDepth]);
		}
	}
}

// -----------------------------------------
// Cut out an area in the base for the PCB.
// -----------------------------------------
module PcbCutout() {
	// Move to a slight offset to allow for an outer bezel.
	// and position so the top of the pcb is at the top of the base box.
	translate([bezelSize, bezelSize, depth - (pcbThickness - 0.1)]) {
		Pcb();        
	}
}

// -----------------------------------------
// Generate a model of the PCB.
// -----------------------------------------
module Pcb() {
    photonWidth = 24;
    photonHeight = 37;
    photonDepth = 3.5;
        
    // PCB.
    color( "purple" ) {
        GenericBase(pcbWidth, pcbHeight, pcbThickness);
    }
        
    // Photon
    translate([(pcbWidth/2) - (photonWidth /2),0 , -(photonDepth)]) {
        cube([photonWidth, photonHeight, photonDepth]);
    }
        
    // LEDs
    ledOffsetFromSide = 3;
    ledOffsetFromTopBottom = 1.5;
    ledWidth = 3; // Include the resistor and LED and a small fudge for the oversized PCB.
    ledHeight = 7;
    ledDepth = 1;
    
    color( "blue" )  {
        // Top left LED
        // 0,0,0 bottom left.
        translate([
                    0 + ledOffsetFromSide,
                    pcbHeight - ledOffsetFromTopBottom - ledHeight,
                    -ledDepth]) {
            cube ([ledWidth, ledHeight, ledDepth]);
        }
       
        
        // top right LED
        translate([
                    pcbWidth - ledOffsetFromSide - ledWidth,
                    pcbHeight - ledOffsetFromTopBottom - ledHeight,-ledDepth]) {
            cube ([ledWidth, ledHeight, ledDepth]);
        }
        
        // Bottom Left LED.
        translate([
                0 + ledOffsetFromSide,
                ledOffsetFromTopBottom,
                -ledDepth]) {
            cube ([ledWidth, ledHeight, ledDepth]);
        }
        
        // Bottom Right LED.
        translate([
                pcbWidth - ledOffsetFromSide - ledWidth,
                ledOffsetFromTopBottom,
                -ledDepth]) {
            cube ([ledWidth, ledHeight, ledDepth]);
        }
        
        // Show LED holes with LEDs shining through
        // LED hole
        translate([
                    0 + 5,
                    pcbHeight - 5,
                    -0.1]) {
            cylinder(d=2.5, h=3, $fn=10);
        }
        
        translate([
                    pcbWidth - 5,
                    pcbHeight - 5,
                    -0.1]) {
            cylinder(d=2.5, h=3, $fn=10);
        }
        
        translate([
                    0 + 5,
                    5,
                    -0.1]) {
            cylinder(d=2.5, h=3, $fn=10);
        }
        
        translate([pcbWidth - 5,
                    5,
                    -0.1]) {
            cylinder(d=2.5, h=3, $fn=10);
        }
    }
    
    // components for sensors on the base of the board.
    // V1.1 this is not such as issue as they are 4 mm from edge.
    translate([(pcbWidth/2) -15 ,pcbHeight - 25, -pcbThickness]) {
        cube([44, 20, ledDepth + pcbThickness]);
    }
    
    // Show different model of PCB sensors
    // for the second version.
    // TODO: Include battery connector and Neopixels 
    // on the base as well.
    if (pcbVersion == 1) {
        // Humidity and gesture components on the board top.
        // 2.5mm offset from board top.
        // 10mm heihg
        // 6mm wide
        // actually 2mm hight but project 5mm to make it stand out.
        translate([(pcbWidth/2) -3 ,pcbHeight - 10 - 2.5, pcbThickness]) {
            color( "black" ) cube([6, 10, 2]);
        }
        
        // Small raised area to indicade the touch pad.
        // touch pad is 19mm from top.
        // 40mm height
        // 85mm wide
        // 8mm from left/right edge.
        touchPad(19, pcbHeight, pcbThickness );
    } else if (pcbVersion == 2) {
        // Single row of 5 sensors.
        translate([(pcbWidth/2) - 32.5 ,pcbHeight - 8, pcbThickness]) {
            color( "black" ) {
                cube([65, 6, 2]);
            }
        }
        
        // Small raised area to indicade the touch pad.
        // touch pad is 17.5mm from top.
        touchPad(17.5, pcbHeight, pcbThickness);
        
        // Include the JST Connector for V2 as this
        // can hit the switch.
        JSTConnector();
        NeoPixels();
    }
}

module JSTConnector() {
    // 8mm hight
    // 64mm down from top
    // 5.5mm high
    translate([21.5,pcbHeight-8-64, -5.5]) {
        color( "black" ) {
            cube([8, 8, 5.5]);
        }
    }
}

module NeoPixels() {
    NeoPixel(16.5,9.5);
    NeoPixel(81,9.5);
    NeoPixel(13.8,73);
    NeoPixel(89,72);
    
}

module NeoPixel(x,y) {
    neoPixelWidth = 5.0;
    neoPixelHeight = 5.0;
    neoPixelDepth = 1.5;
    
    translate([x,pcbHeight-neoPixelHeight-y, -neoPixelDepth]) {
        color( "yellow" ) {
            cube([neoPixelWidth, neoPixelHeight, neoPixelDepth]);
        }
    }
}

// -----------------------------------------
// Show the touch area on the PCB.
// -----------------------------------------
module touchPad(yPosition, pcbHeight, pcbThickness ) {
    height = 40; 
    width = 85;
    // 8mm from left/right edge.
    translate([8,pcbHeight-height-yPosition, 0]) {
        color( "SteelBlue" )  {
            cube([width , height , pcbThickness + 1]);
        }
    }
}

// -----------------------------------------
// Cutoff the end of the switch to allow it to be pushed over the lightswitch 
// and slid down. The outer cover will hide this missing piece.
// -----------------------------------------
module CutOff() {     
    // +2mm on switch width to allow for a small tolerance.
    cutoutOffset = (width - (switchWidth+2)) / 2;
    cutoutWidth = width - (wallThickness*2) - (switchOverlap*2) + 2*2;
    
    translate([cutoutOffset,0,0]) {
        cube([switchWidth+2,wallThickness  ,depth+2]);
    }
}

// -----------------------------------------
// This is the outer cover that goes over the base and
// holds the PCB in place.
// -----------------------------------------
module Cover() {
    coverWallThickness = 2;
    
    coverWidth = width + coverWallThickness*2;
    coverHeight = height + coverWallThickness*2;
        
    // Cover depth to match the overall depth of the base
    // plus the additional cover top thickness
    // then allow 1mm gap for wall edge variances so it's a snug-ish fit
    // ensuring cover goes fully onto base.
    coverWallOffset = 1;
    coverDepth = depth + coverWallThickness - coverWallOffset;
    coverTopThickness = 1;
       
    usbHeight = 10; // Height here is in the z axid.
    usbWidth = 13;
    
    // How much over the top (sensors) the cover should cover.
    coverTopOverlap = 21;
        
    includeBottomVentHoles = true;
    ventWidth = width - 40;
    ventHeight = 8;
    
    difference() {
		union() {
			GenericBase(coverWidth, coverHeight, coverDepth);
		}
		union() {
            // Hollow out to fit around the base.
            translate([coverWallThickness - coverToBasegap, 
                            coverWallThickness - coverToBasegap, 
                            -coverTopThickness]) {
                    GenericBase(width + (coverToBasegap*2), height + (coverToBasegap*2), coverDepth);
            }
            
            // Cutout for the USB connector...
            translate([(coverWidth/2)-(usbWidth/2),
                        coverHeight - coverWallThickness - 1,
                        depth-(coverTopThickness + usbConnectorCutoutDepth)]) {
                cube([usbWidth,coverWallThickness+2,usbConnectorCutoutDepth]);
                // TODO: Put cylinders on ends to make it rounded.
            }
            
            // Add some vent holes in the bottom to allow air
            // circulation and for light to escape when using "Glow"
            if (includeBottomVentHoles) {
                translate([(coverWidth/2)-(ventWidth/2),
                        coverHeight - coverWallThickness - 1,
                        0]) {
                    #cube([ventWidth,coverWallThickness+2,ventHeight]);
                }
            }
                
            if (minimalCover) {
                // Allow for cover thickness, the base bexel size.
                // base has 2mm PCB overlap
                overlap = coverWallThickness + bezelSize + 2;
                doubleOverlap = overlap * 2;
                
                // Cut out the main PCB area
                translate([overlap,overlap,+2]) {
                    GenericBase(coverWidth-doubleOverlap, coverHeight- doubleOverlap, coverDepth);
                }
            } else {
                if (pcbVersion == 1) {
                    coverCutoutsV1(coverWidth, 
                                coverHeight, 
                                coverWallThickness, 
                                coverTopThickness,
                                coverDepth, 
                                coverTopOverlap, 
                                coverWallOffset);
                } else if (pcbVersion == 2) {
                    coverCutoutsV2(coverWidth, 
                                coverHeight, 
                                coverWallThickness, 
                                coverTopThickness,
                                coverDepth, 
                                coverTopOverlap, 
                                coverWallOffset);
                }
            }
        }
	}
}

// -----------------------------------------
// Defines the parts of the cover to be cutout for the V1 board.
// -----------------------------------------
module coverCutoutsV1(coverWidth, 
                    coverHeight, 
                    coverWallThickness, 
                    coverTopThickness,
                    coverDepth, 
                    coverTopOverlap, 
                    coverWallOffset) {

    // How far down from the top of the cover the opening for the sensor set should be.
    sensorSetTopOffset = 3.5;
    sensorSetHeight = 12;
    sensorSetWidth = 8;
        
    touchCoverCutouts(coverWidth,
                    coverHeight, 
                    coverWallThickness, 
                    coverTopThickness,
                    coverDepth, 
                    coverTopOverlap, 
                    coverWallOffset);
                
    // Cut out the space for the sensors.
    // move down coverWallThickness + 2mm 
    // from top so some PCB is covered.           
    translate([(coverWidth/2),
                coverWallThickness + sensorSetTopOffset + (sensorSetWidth/2)-1,
                depth-coverTopThickness + coverWallOffset]) {
        #cylinder(d=sensorSetWidth, h=coverTopThickness, $fn=20);
    }
                
    translate([(coverWidth/2),
                coverWallThickness + sensorSetTopOffset + sensorSetHeight - (sensorSetWidth/2) + 1,
                depth-coverTopThickness + coverWallOffset]) {
        #cylinder(d=sensorSetWidth, h=coverTopThickness, $fn=20);
    }
                
    // Join the two cylinders to make the cutout oblong
    translate([(coverWidth/2)-(sensorSetWidth/2),
                coverWallThickness + sensorSetTopOffset + 3,
                depth-coverTopThickness + coverWallOffset]) {
        #cube([sensorSetWidth,sensorSetHeight-6,coverTopThickness]);
    }
    
    // Don't include LED holes as material is most likely thin enough
    // for the LEDs to shine through without needing extra holes.
}

// -----------------------------------------
// Defines the parts of the cover to be cutout for the V1 board.
// -----------------------------------------
module coverCutoutsV2(coverWidth, 
                    coverHeight, 
                    coverWallThickness, 
                    coverTopThickness,
                    coverDepth, 
                    coverTopOverlap, 
                    coverWallOffset) {

    // How far down from the top of the cover the opening for the sensor set should be.
    sensorSetTopOffset = 3;
    sensorSetHeight = 8;
    sensorSetWidth = 65;
                                                  
    touchCoverCutouts(coverWidth,
                    coverHeight, 
                    coverWallThickness, 
                    coverTopThickness,
                    coverDepth, 
                    coverTopOverlap-1.5, 
                    coverWallOffset);
   
    // Cut out the space for the sensors.
    // move down coverWallThickness + n mm 
    // from top so some PCB is covered.            
    translate([(coverWidth/2) - (sensorSetWidth/2),
                coverWallThickness + sensorSetTopOffset + (sensorSetHeight/2),
                depth-coverTopThickness + coverWallOffset]) {
        #cylinder(d=sensorSetHeight, h=coverTopThickness, $fn=20);
    }
                
    translate([(coverWidth/2) + (sensorSetWidth/2),
                coverWallThickness + sensorSetTopOffset + (sensorSetHeight/2),
                depth-coverTopThickness + coverWallOffset]) {
        #cylinder(d=sensorSetHeight, h=coverTopThickness, $fn=20);
    }
                
    // Join the two cylinders to make the cutout oblong
    translate([(coverWidth/2)-(sensorSetWidth/2),
                coverWallThickness + sensorSetTopOffset,
                depth-coverTopThickness + coverWallOffset]) {
        #cube([sensorSetWidth,sensorSetHeight,coverTopThickness]);
    }
                                
    // Don't include LED holes as material is most likely thin enough
    // for the LEDs to shine through without needing extra holes.
}

// -----------------------------------------
// Defines the parts of the cover to be cutout for the touch sensor
// -----------------------------------------
module touchCoverCutouts(coverWidth, 
                    coverHeight, 
                    coverWallThickness, 
                    coverTopThickness,
                    coverDepth, 
                    coverTopOverlap, 
                    coverWallOffset) {
    
    // How much over the bottom the cover should cover.
    // this hides the non-touchable zone (where the Photon would mess
    // with tuch sensor.
    coverBottomOverlap = coverHeight - (coverTopOverlap + 40 + 4);
                            
    // Cut out the main touch switch area.
    // Gives an overlap of 10mm on the PCB left and right
    // and a larger overlap at the top to cover the sensor area.
    // and matching overlap at the bottom to give it some sym.
    translate([10,coverTopOverlap,+2]) {
        GenericBase(coverWidth-20, coverHeight- (coverTopOverlap + coverBottomOverlap ), coverDepth);
    }
}


// -----------------------------------------
// Show the model of the switch.
// NB: Do not include when generating stl's
// -----------------------------------------
module Switch(xOffset, yOffset, zOffset) {
    translate([xOffset, yOffset, zOffset]) {
        color( [0, 0, 1, 1] ) {
                       
            if(switchType == 0) {            
                // New style switches
                switchBodyDepth = 8.5;
                switchSwitchDepth = 4; // on top of the body.
                
                // Square switch base
                // Switch depth = 8.5mm.
                cube([switchWidth, switchHeight, switchBodyDepth]);
                
                // Put the actual switch switch on the base.
                translate([(switchWidth /2) - 8, (switchHeight/2) - 13, switchBodyDepth]) {
                    cube([16, 26, switchSwitchDepth]);
                }
            } else if(switchType == 2) {
                // Old style lever switch
                
                // Square switch base
                cube([switchWidth, switchHeight, 16]);
                
                // Put the actual switch switch on the base
                // Show switch on the on position only as it would be expected tobe.
                // Make switch y=22mm for full range.
                translate([(switchWidth /2) - 4, (switchHeight/2)-11, 16]) {
                    cube([8, 11, 8]);
                }
            } else if(switchType == 3) {
                // Old style lever switch
                
                // Square switch base
                cube([switchWidth, switchHeight, 16]);
                
                // Put the actual switch switch on the base
                // Show switch for On & Off as either switch
                // may be in that state as this is typically
                // a landing/hall switch and has a second switch
                translate([(switchWidth /2) - 13, (switchHeight/2)-11, 16]) {
                    cube([26, 22, 8]);
                }
            }
		}
    }
}





// -----------------------------------------
// MAIN
// -----------------------------------------

if (showBase) {
    LightSwitchBase();
}

if (showCover) {
    // Case top rotated 180° (well openings swapped around.
    // so rotate and adjust position os they sit ontop.
    rotate(180) {
        
        // Adjust x offset to move cover up/down onto base.
        translate([-width -2.2,-height -2.2,0]) {
            color( [0, 1, 0, 1] ) {
               Cover();
            }
        }
    }
}

// Optional models to help visualise the enclosure.

if (showSwitch) {
    // Show the actual light switch we are covering
    // comment out when generating STLs.
    switchX = (width - switchWidth) /2;
    Switch(switchX,14,1);
}

if (showPcb) {
    // Show the PCB (in purple)
    // comment out when generating STLs.
    translate([bezelSize, bezelSize, depth - (pcbThickness - 0.1)]) {
        Pcb();        
    }
}

GitHub JediLightSwitch Repository

Software is in V1.1 folder, works for both V1.1 and 1.2, just set the HARDWARE_REV = 1 for V1.1 and HARDWARE_REV = 2 for V1.2

Credits

Stephen Harrison

Stephen Harrison

18 projects • 51 followers
Founder of Tinamous.com, software developer, hardware tinkerer, dyslexic. @TinamousSteve
Thanks to SparkFun and Particle.

Comments