Hardware components | ||||||
| × | 1 | ||||
| × | 1 | ||||
| × | 1 |
Note: si vous préférez lire en français, voir plus loin, il y a un document avec des explications détaillées.Overview
The journey began at brunch time last fall. I had a talk with my friend Isabelle. She was curious and excited to see real connected objects in my lab; I have some, well, too many actually.
She was also preparing a local event, Les Filles et les Sciences, and she was wondering how we could leverage connected objects to try to motivate 14 years old girls to choose science as a career path.
The week after, I Skyped her; a new connected object was sitting on my workbench, SpokaPhoton.
The SpokaPhotonSpoka is an IKEA night light. Nothing fancy there but so very cute. You push on its head and the light turns on. It is battery powered but you can plug it in, as well.
Particle Photon is a powerful STM32 ARM Cortex M3 microcontroller with a Cypress Wi-Fi chip that leverages cloud services over Particle Cloud. The only limit is what you can imagine!
With some "basic brain surgery", I created SpokaPhoton. It has the same nice look and feel, but the power of a connected object.
The GoalDuring the event in March 2017, up to 40 girls will discover what a connected object is, how it is designed (in very simple terms) and what they could achieve with a community of connected objects.
My intent here is to share the design in a way that almost everybody could replicate it. A full list a components is provided, along with schema. I tried to put many comments in my code, so you could follow and make it your own. I drafted some procedures (How-To's) in an attachment, as well.
In a nutshell, here is how a SpokaPhoton community works. Each SpokaPhoton can publish messages (each time you press on SpokaPhoton's head). All other SpokaPhotons are listening and they will react and blink.
[Coming soon: video with SpokaPhotons blinking]
/*
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);
}
Comments