Things used in this project

Hardware components:
Photon new
Particle Photon
×1
IKEA Spoka
There are two models. The big on (with blue ears is easier to transform)
×1
SparkFun Wall Adapter Power Supply 5v DC (at least 500mA)
Pay attention to IKEA Spoka DC plug. Two of my 40+ Spokas had a different plug (but still 5 VDC)
×1

Custom parts and enclosures

How to modify an IKEA Spoka - blue model
Step by step procedure
How to modify an IKEA Spoka - pink model
Step by step procedure
Explications détaillées (in French)
Une version en français pour ceux et celles qui préfèrent

Schematics

SpokaPhoton on a breadboard
spokaphoton_on_breadboard_FBgZLMPyGq.fzz

Code

SpokaPhoton scriptC/C++
/*

 This template can be used for both Spoka models (the small one and the big one) but there are some differences at HW level

 Small Spoka (with pink ears)
 - There is a push button on its head. The button is connected to 3.3v and to D4 pin. When the button is pressed, D4 goes HIGH
 - There are 9 single color LEDS, with a common anode (i.e they share the same positive input) and connected in 3 groups:
    - group 0 = blue = D1 (when group is turned On, LEDs 1, 4 and 7 will turn On)
    - group 1 = pink = D2 (when group is turned On, LEDs 2, 5 and 8 will turn On)
    - group 2 = orange = D3 (when group is turned On, LEDs 3, 6 and 9 will turn On)
    Obviously, you can mix colors by turning more than one group On at a time

 Big Spoka (with blue ears)
 - There is a push button on its head, identical to the one within Small Spoka. The button is connected to 3.3v and to D4 pin. When the button is pressed, D4 goes HIGH
 - There a 3 bicolor LEDs on this, connected all together. To keep it simple and to have a unified script for both Spoka modesl,
   I decided to create virtual groups
    - group 0 = blue = D2  (when group is turned On, ALL LEDs will turn On in blue)
    - group 1 = green = D3 (when group is turned On, ALL LEDs will turn On in green)
    - group 2 = white-blue = D2 and D3 (when group is turned On, ALL LEDs will turn On in white-blue)
    
 The way to figure out which Spoka model is to probe D5. 
 On Big Spoka, D5 is not connected to anything and has an internal pull-up
 On the Small Spoka, D5 and D6 are physically connected and D6 is set to LOW
 Therefore...  D5 will be HIGH for Big Spoka but LOW for Small Spoka     

*/

// Customization
String myName = "";  // UPPERCASE, alphanumeric, no space or special character
int mySequence = 1; // determine the LED sequence that SpokaPhoton will play once its head is pressed one time
String SPOKAPOKE = "SPOKAPOKE";
String SPOKAPARTY = "SPOKAPARTY";

// Model Pin , a jumper is installed between D5 and D6 on Big Spoka only
int modelJumperPin1 = D5;
int modelJumperPin2 = D6;
// For debug over USB
// SerialLogHandler logHandler;

// Button
int buttonPin = D4;
unsigned long smallDelay = 1000; // 1 sec to detect simple or double click
unsigned long bigDelay = 5000; // 10 sec to detect a long click
volatile bool btnState = LOW;
volatile bool lastBtnState = LOW;
volatile int cycles = 0;
volatile unsigned long beginOfScanPeriod = 0;
volatile unsigned long elapsedTime = 0;
volatile bool hasPressedOnce = false;
volatile bool hasPressedTwice = false;
volatile bool hasPressedDuring15sec = false;

// LEDs
int ledPin1 = D1;
int ledPin2 = D2;
int ledPin3 = D3;
int defaultBrightness = 122;  // i.e half brightness
int brightness[3] = {defaultBrightness, defaultBrightness, defaultBrightness};  // between 0 an 255
int defaultPeriodicity = 0; // no blinking
int periodicity[3] = {defaultPeriodicity, defaultPeriodicity, defaultPeriodicity}; // between 0 (no blinking) and 600 (600 by min, ie.e 10 by sec)
int defaultIdlePart = 50;   // how much time (in percentage) the LED will stay Off during a period of time
int idlePart[3] = {defaultIdlePart, defaultIdlePart, defaultIdlePart};

Timer group0Timer(defaultPeriodicity, onGroup0Tick);
Timer group1Timer(defaultPeriodicity, onGroup1Tick);
Timer group2Timer(defaultPeriodicity, onGroup2Tick);
Timer btnTimer(100, onBtnTick);

int identify(String s);

// First, let's setup our Photon

void setup() 
{
    // Log.info("Entering setup");
    
    // Detect Spoka Model and join the network
    pinMode(modelJumperPin1, INPUT_PULLUP);
    pinMode(modelJumperPin2, OUTPUT);
    digitalWrite(modelJumperPin2, LOW); 
    
    // Configure HW pins for LEDs 
    pinMode(ledPin1,OUTPUT);
    if (isBigSpoka())
    {
       if (System.deviceID() == "20003e000347353137323334") { ledPin2 = D0; }  // pin D2 was defective on this board 
       digitalWrite(ledPin1, HIGH);  // common positive for all LEDs on this model
    }
    else
    {
       analogWrite(ledPin1, 255);  
    }
    pinMode(ledPin2,OUTPUT);
    analogWrite(ledPin2, 255);
    pinMode(ledPin3,OUTPUT);
    analogWrite(ledPin3, 255);   
    // Let's introduce ourself to the Spoka Network
    Particle.publish("SPOKAALIVE", getSpokaName());   // no more that 63 ASCII characters, and no space
    // Button
    pinMode(buttonPin,INPUT_PULLDOWN);
    // Run a simple test to confirm all LEDs are ok
    runSelfTest();
    // Start observing user inputs
    enableButton();
    // Subscribe to a given topic on Particle Cloud. Each time another SpokaPhoton publish a message on that topic, we will be notified
    Particle.subscribe(SPOKAPOKE, onSpokaPoke);
	Particle.subscribe(SPOKAPARTY, onSpokaParty);
	Particle.function("Identify", identify);
    // Log.info("Leaving setup");
}

// This is the main loop. It will run forever

void loop()
{
    if (hasPressedOnce)  
    {
        playSequence(1);
        Particle.publish(SPOKAPOKE, getSpokaName());   // no more that 63 ASCII characters, and no space
        enableButton();
    } 
    if (hasPressedTwice)
    {
        playSequence(2);
        Particle.publish(SPOKAPARTY, getSpokaName());   // no more that 63 ASCII characters, and no space
        enableButton();
    }
    if (hasPressedDuring15sec) 
    {
        playSequence(3);
        resetWiFiConfig();
    }
    
    // Log.info("System version: %s", (const char*)System.version());
    // Log.info("Device ID: %s", (const char*)System.deviceID());
}

int identify(String s)
{
    playSequence(4);
}

// LED sequences
// Create your custom sequences here

void playSequence(int id)
{
    switch (id)
    {
        case 1:  //pressed ME Once
            playSimpleRepeatableSequence(300, 0, 0);  // group 0, 300 ms, one time
            break;
        case 2:  //pressed ME twice
            playSimpleRepeatableSequence(300, 1, 1);  // group 0, 300 ms, two times
            break;
        case 3:  //self disconnect wifi
            playSimpleRepeatableSequence(300, 0, 2);  // group 0, 300 ms, three times
            break;
        case 4:   //Self TEst
            playSimpleRepeatableSequence(300, 0, 0);  // group 0, 300 ms, one time
            playSimpleRepeatableSequence(300, 1, 0);  // group 1, 300 ms, one time
            playSimpleRepeatableSequence(300, 2, 0);  // group 2, 300 ms, one time
            break;
        case 5:  //SPOKAPOKE
            playSimpleRepeatableSequence(100, 2, 3);  // group 1, 300 ms, three times
            delay(500); 
            playSimpleRepeatableSequence(100, 2, 3);  // group 1, 300 ms, three times
            break;
	    case 6:  // do not change this //SPOKAPARTY
            playSimpleRepeatableSequence(80, 0, 0); 
            playSimpleRepeatableSequence(90, 2, 0); 
            playSimpleRepeatableSequence(60, 1, 0); 
            playSimpleRepeatableSequence(70, 2, 0); 
            playSimpleRepeatableSequence(80, 0, 0); 
            playSimpleRepeatableSequence(100, 1, 0); 
            playSimpleRepeatableSequence(80, 2, 0); 
            playSimpleRepeatableSequence(100, 0, 0); 
            playSimpleRepeatableSequence(80, 2, 0); 
            playSimpleRepeatableSequence(50, 0, 0); 
            playSimpleRepeatableSequence(80, 2, 0); 
            playSimpleRepeatableSequence(70, 1, 0); 
            playSimpleRepeatableSequence(100, 2, 0); 
            break;
        case 7:
           // TODO
            break;
    }
}

// A Simple repeatable and fixed duration sequence for a given group

void playSimpleRepeatableSequence(int duration, int groupId , int repeat)
{
    for (int i=-1; i<repeat; i++)
    {
       configureLEDGroup(groupId, 255, 0, 0); // group 0, On, full brightness
       applyConfig(groupId);
       delay(duration);
       configureLEDGroup(groupId, 0, 0, 0);  // group 0, Off
       applyConfig(groupId);
       delay(duration);
    }
}

// WiFi configuratiton

void resetWiFiConfig()
{
    Particle.publish("SPOKAWIFIRESET",myName);   // no more that 63 ASCII characters, and no space
    WiFi.disconnect();
    WiFi.clearCredentials();
    WiFi.connect(); // enter in listening mode. System LED on Photon board should blink now (blue)
}

// Configure a LED group

void configureLEDGroup(int groupId,      // 1, 2 or 3
                       int updatedBrightness,   // between 0 (off) and 255 (full brightness)
                       int updatedPeriodicity,  // between 0 (steady) and 600 (i.e 10 times per second approx)
                       int updatedIdlePart)     // how much time (in percentage) the LED will stay Off during a period of time
{
    // the first thing to configure is brightness, i.e how frequently PWM will fire to make LEDs looked dimmed
    configureBrightness(groupId, updatedBrightness);
    // the second thing to configure is blinking. This is managed by timers
    configureBlinking(groupId, updatedPeriodicity, updatedIdlePart);
}

void configureBrightness(int groupId, int updatedBrightness)
{
    if (updatedBrightness < 0) updatedBrightness = 0; // values our of range are trapped here, just in case
    if (updatedBrightness > 255) updatedBrightness = 255; // values our of range are trapped here, just in case
    brightness[groupId] = updatedBrightness;
    // Log.info("Brightness for group %d is now set to %d", groupId, brightness[groupId]);
}

void configureBlinking(int groupId, int updatedPeriodicity, int updatedIdlePart)
{
    if (updatedPeriodicity < 0) updatedPeriodicity = 0; // values our of range are trapped here, just in case
    if (updatedPeriodicity > 600) updatedPeriodicity = 600; // values our of range are trapped here, just in case
    periodicity[groupId] = updatedPeriodicity;
    // Log.info("Periodicity for group %d is now set to %d", groupId, periodicity[groupId]);
    
    if (updatedIdlePart < 0) updatedIdlePart = 0; // values our of range are trapped here, just in case
    if (updatedIdlePart > 100) updatedIdlePart = 100; // values our of range are trapped here, just in case
    idlePart[groupId] = updatedIdlePart;
    // Log.info("Idle time for group %d is now %d pct", groupId, idlePart[groupId]);
    
    // Since we change the group config, we do nt want to let any time alive 
    if (groupId == 0 && group0Timer.isActive() ) 
    {
        group0Timer.stop(); 
        // Log.info("Timer0 stopped");
    }
    if (groupId == 1 && group1Timer.isActive()) 
    {
        group1Timer.stop(); 
        // Log.info("Timer1 stopped");
    }
    if (groupId == 2 && group2Timer.isActive()) 
    {
        group2Timer.stop(); 
        // Log.info("Timer2 stopped");
    }
}

// Reset a LED group configuration with default values

void resetLEDGroupConfiguration(int groupId)
{
    // Log.info("Resetting group %d configuration to default values", groupId);
    configureLEDGroup(groupId, defaultBrightness, defaultPeriodicity, defaultIdlePart);
}

// Run a self-test on the LEDs

void runSelfTest()
{
    // Log.info("Running self test");
    playSequence(4);
}

// Apply a config to a LED group. 
// As a result, the LED group will go On or Off, steady or blinking, depending on configuration

void applyConfig (int groupId)
{
    if (brightness[groupId] == 0 )
    {
        applyConfigToPWMPins(groupId, 255); // 255 = HIGH => LED off since LED have common positive 
    }
    else
    {
        applyConfigToPWMPins(groupId, 255 - brightness[groupId] );  
        switch (groupId)
        {
            case 0:    
                if (periodicity[0] > 0) 
                {
                   group0Timer.changePeriod(computeCycleTime(0));
                   // Log.info("Timer0 starting");
                   group0Timer.start();
                }
                break;
            case 1:    
                if (periodicity[1] > 0) 
                {
                   group1Timer.changePeriod(computeCycleTime(1));
                   // Log.info("Timer1 starting");
                   group1Timer.start();
                }
                break;       
            case 2:    
                if (periodicity[2] > 0) 
                {
                   group2Timer.changePeriod(computeCycleTime(2));
                   // Log.info("Timer2 starting");
                   group2Timer.start();
                }
                break;
        }
    }
}

// Configure PWM pins depending on Spoka model 

void applyConfigToPWMPins(int groupId, int brightness)
{
    if (isSmallSpoka())
    {
        switch (groupId)     
        {
            case 0:
               analogWrite(ledPin1, brightness); // 255 = HIGH => LED off since LED have common positive 
               break;
            case 1:
               analogWrite(ledPin2, brightness); // 255 = HIGH => LED off since LED have common positive 
               break;
            case 2:
               analogWrite(ledPin3, brightness); // 255 = HIGH => LED off since LED have common positive 
               break;
        }
    }
    else
    {
        switch (groupId)     
        {
            case 0:
               analogWrite(ledPin2, brightness); // 255 = HIGH => LED off since LED have common positive 
               break;
            case 1:
               analogWrite(ledPin3, brightness); // 255 = HIGH => LED off since LED have common positive 
               break;
            case 2:
               analogWrite(ledPin2, brightness); // 255 = HIGH => LED off since LED have common positive 
               analogWrite(ledPin3, brightness); // 255 = HIGH => LED off since LED have common positive 
               break;
        }
    }
}

// Well .. the function name is clear, isn't it ?

void turnAllLEDGroupsOff()
{
    // Log.info("Turning alls LED groups off");
    configureLEDGroup(0, 0, 0, 0);  // group 0, Off
    applyConfig(0);
    configureLEDGroup(1, 0, 0, 0);  // group 0, Off
    applyConfig(1);
    configureLEDGroup(2, 0, 0, 0);  // group 0, Off
    applyConfig(2);
}

// React to LED timers. There is a timer associated to each LED group

void onGroup0Tick()
{
    onGroupTick(0);
}

void onGroup1Tick()
{
    onGroupTick(1);
}

void onGroup2Tick()
{
    onGroupTick(2);
}

//Reset pressed
void resetPressedStatus(){
    hasPressedOnce = false; 
    hasPressedTwice = false; 
    hasPressedDuring15sec = false; 
}

void onGroupTick( int groupId)
{
    // Log.info("At %s, timer ticked for group %d", (const char*) Time.timeStr(), groupId);
    applyConfigToPWMPins(groupId, 255); // 255 = HIGH => LED off since LED have common positive 
    delay(computeIdletime(groupId));
    applyConfigToPWMPins(groupId, 255 - brightness[groupId] );  
}

// React to timer attached to button

void onBtnTick()
{
    // Calculate the time elapsed since the start of the scan period
    elapsedTime = millis() - beginOfScanPeriod;   
    // Capture current button state and count on/off cycles if any
    btnState = digitalRead(buttonPin);
    if (btnState != lastBtnState)
    { 
        cycles++; // Start counting how many times he pressed/unpressed on the button
        // Log.info("Btn has changed at %d - cycles = %d", elapsedTime, cycles);
        lastBtnState = btnState;
    }
    // Determine user's intent
    if (elapsedTime > smallDelay && elapsedTime < bigDelay)  
    {
        switch (cycles)  // btn pressed once in a timeframe of 2 sec
        {
            case 0:  // Nothing happened) => let's reset scan period
                beginOfScanPeriod = millis();
                break;
            case 2: // i.e one press and one depress      
                disableButton();
                resetPressedStatus();
                hasPressedOnce = true; 
               break;
            case 4: // i.e button has been pressed and release two times
                disableButton();
                resetPressedStatus();
                hasPressedTwice = true; 
                break;
        }
    }
    else if (elapsedTime > bigDelay) 
    {
        switch (cycles)  
        {
            case 1:    // btn pressed and kept during at least 15 seconds   
                disableButton();
                resetPressedStatus();
                hasPressedDuring15sec = true; 
               break;
            default:  // To many things happened) => let's reset scan period
                beginOfScanPeriod = millis();
                break;  
       }
    }
}

// Start a timer that will evaluate button's state every 100 ms 
// This method helps to avoid bouncing

void enableButton()
{
    btnState = 0;
    lastBtnState = LOW;
    cycles = 0;
    hasPressedOnce = false;
    hasPressedTwice = false;
    hasPressedDuring15sec = false;
    beginOfScanPeriod = millis();  // let's start observing what the user is doing
    btnTimer.start();
}

// Stop evaluating button's state. It is typically called once a user intent has been detected 
// and we need time to perform the matching action. 

void disableButton()
{
    btnTimer.stop();
}

// Which Spoka (there are two models)

bool isSmallSpoka()
{
    return !isBigSpoka();
}

bool isBigSpoka()
{
    return digitalRead(modelJumperPin1);
}

// Determine the LED cycle time (in millis)

int computeCycleTime( int groupId)
{
    // Log.info("Periodicity for group %d is %d times per minute", groupId, periodicity[groupId]);
    int result = 60000 / periodicity[groupId];
    // Log.info("Computed cycle time for group %d is %d millis", groupId, result);
    return result;
}

// Determine the length of the idle time within a cycle (in millis )

int computeIdletime(int groupId)
{
    int cycle = computeCycleTime(groupId);
    // Log.info("Percentage of idle time for group %d is %d pct", groupId, idlePart[groupId]);
    int idleTime = cycle * idlePart[groupId] / 100;
    if (idleTime > 100) idleTime = idleTime - 100;
    // Log.info("Computed idle time for group %d is %d millis", groupId, idleTime);
    return idleTime;
}

// Get Spoka name (it could be a user friendly name or just the HW ID)

String getSpokaName()
{
    String result = myName;
    if (result == "" ) result = System.deviceID();  // the default name is the HW ID
    if ( isBigSpoka() )
    {
        result.concat("-BIGSPOKA");
    }
    else 
    {
        result.concat("-SMALLSPOKA");
    }
    return result;
}

// This function will be executed each time we get a notification on the topic we subscribed to

void onSpokaPoke(const char *event, const char *data)
{
    if (getSpokaName() == data) return; // Otherwise I get an echo of my own publications
   
    playSequence(5);
    // Log.info("%s poked m", data);
    
}

// This function will be executed each time we get a notification on the topic we subscribed to
// DO NOT CHANGE that part of the script, I will use it when all SpokaPhotons will be connected in the lobby

void onSpokaParty(const char *event, const char *data)
{
    if (getSpokaName() == data) return; // Otherwise I get an echo of my own publications
    playSequence(6);     
}

Credits

Replications

Did you replicate this project? Share it!

I made one

Love this project? Think it could be improved? Tell us what you think!

Give feedback

Comments

Similar projects you might like

ConnectTheDots with Particle Azure IoT Hub Integration
Intermediate
  • 598
  • 7

Protip

This project will allow you to connect your Particle device into an Azure IoT Hub for viewing data in real-time through an ASP.NET web app!

Other Clocks
Intermediate
  • 194
  • 3

Work in progress

A combination of 3 different clocks in one frame.

Christmas Gift Box
Intermediate
  • 3,604
  • 595

Full instructions

Christmas Gift Box plays music and sends an email when it is opened.

Weather Aware Sprinkler Controller
Intermediate
  • 1,321
  • 16

Full instructions

6 station Photon controller uses the Weather Underground API to prevent watering when windy, rainy, or too cold.

Simple Environmental Monitoring
Intermediate
  • 1,613
  • 13

Full instructions

Particle Photon circuit allowing the display of environmental conditions (light, temperature, humidity, pressure) using the Blynk app.

THDweeter
Intermediate
  • 253
  • 2

Protip

Yet another temperature-humidity sensor publishing to dweeter, with WiFi auto-disconnect and a push button to read daily max and min values.

Add projectSign up / Login