Philippe LibioulleIsabelle Lopez
Published © GPL3+

SpokaPhoton

Let's re-invent Spoka as a connected object and build the SpokaPhoton community!

IntermediateFull instructions provided4 hours1,730
SpokaPhoton

Things used in this project

Hardware components

Photon
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

Story

Read more

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

Code

SpokaPhoton script

C/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

Philippe Libioulle

Philippe Libioulle

7 projects • 49 followers
Isabelle Lopez

Isabelle Lopez

1 project • 2 followers

Comments