# SpokaPhoton

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

IntermediateFull instructions provided4 hours770

## Things used in this project

### Hardware components

 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

## 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
}

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
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()
{
}

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

6 projects • 32 followers

### Isabelle Lopez

1 project • 1 follower