49713 Designing for the Internet of Things
· 16 members
A hands-on introductory course exploring the Internet of Things and connected product experiences.
Helping plants and humans to connect, communicate and ultimately foster a closer relationship through the magic of IoT
You know your plant is alive. Guess what … it probably has emotions too. It certainly feels on some level when it is too hot, too cold, thirsty or in need of some sunlight (just like you!). What if it could empathize with your plant, in the moment? What if your plant could communicate with you in an intuitive way. Introducing Pixie, the IoT weed that helps your plant to communicate with you.
Pixie monitors the light levels, temperature levels (and, coming in version 2: soil moisture levels) and is able to communicate what the plant needs using a single pixel. Pixie displays the emotions in the following ways:
Pixie allow a owner to be more attuned to the needs of to their plant. Ultimately this will mean a less nervous owner and a happier plant! It also does this in a natural and 'polite' way without shouting at the user through notifications.
The video below shows the plant exhibiting these emotions in demo mode. Can you catch them all (the breathing ones are subtle but have tested well for effectiveness)?
For this project, I aimed to focus on communicating emotion through the simplest of ambient displays - a simple RGB Led. It is an attempt to personify a plant without making the experience too obstrusive. It was also a technical challenge to be able to make the feelings of the plant seem intuitive to a human. It was also challenging to make the plant seem to 'breathe' without being distracting.
While the form of the device was not a requirement, I chose to explore it since I felt that it was important to communicate the idea appropriately. i.e. it was important to make the plant seem to communicate naturally.
In order to make the 'breath' animation seem more natural, I used a sine function rather than a straight-line approach. This was then tweaked to add pauses at the apex and trophs. A custom function allowed the timing to be adjusted to iteratively create the 'feel' of a human breathing. A similar approach will be used to create a 'shiver' (with a decreasing aptitude), however, a random brightness sequence was used to create a flicker):
A sine graph
The idea took flight gradually over several weeks. Once I had honed in on what I wanted to do, I sketched out the concept and planned the mechanics with pseudo code.
The process can be broadly outlined as follows:
1. Understand the brief by reading through it carefully
2. Ideate around the solutions that would be possible. Identify a solution that I am passionate in pursuing
3. Prototype electronics, this went through several versions and iterations. It took many hours to calibrate the precise timing
4. Ideate on the form of the prototype
5. Code up the final prototype. This part was challenging to get it working consistently. It is still a little unreliable
6. Build the 'looks like prototype.
7. Refine both of the above to ensure they work together (e.g. put the LED on a lead to combine them).
#include <math.h>
int clokfreq= 10; //Overall timer of the clock
float freq = 1/clokfreq;
int brightness = 255; // Full brightness for an CATHODE RGB LED is 0, OFF is 255
int redPin = D2; // RED pin of the LED to PWM pin **d2**
int greenPin = D3; // GREEN pin of the LED to PWM pin **d3**
int bluePin = D4; // BLUE pin of the LED to PWM pin **D4**
int ePins[] = {redPin, greenPin, bluePin}; //RGB
int eColor[] = {250,250,250};
int eNull[] = {0,0,0};
//Create Light Sensor related variables
int lightPin = A1; //LIghtsensor
int lightLevel = 0;
// Create Temparature sensor related variables
int tempPin = A0; //Temparature sensor
double temperature = 0.0;
double temperatureF = 0.0;
int movAverage[] = {20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20};
int reading = 0;
int avetemp = 0;
bool tempNormal = false;
int last_published = -1;
bool demomode = true;
int ndemo = 0;
void setup() {
//Initialise the pins
pinMode(redPin, OUTPUT);
pinMode(bluePin, OUTPUT);
pinMode(greenPin, OUTPUT);
pinMode(lightPin, INPUT);
//Create output monitoring variables to Console - RGB
Particle.variable("Red", eColor[0]);
Particle.variable("Green", eColor[1]);
Particle.variable("Blue", eColor[2]);
Particle.function("vRed", changeLED);
// Register a Particle variable here
Particle.variable("temperature", &temperature, DOUBLE);
Particle.variable("temperatureF", &temperatureF, DOUBLE);
// Connect the temperature sensor to A0 and configure it
// to be an input
pinMode(tempPin, INPUT);
testCycle();
}
int changeLED(String command){
// int colorValue = command.toInt();;
// outputt(colorValue, 0, 0);
// return 1;
}
void testCycle(){
eColor[0] = 0;
eColor[1] = 255;
eColor[2] = 0;
for (int i = 0; i < 3; i++){
outputt(eColor);
delay(500);
outputt(eNull);
delay(500);
}
}
//Output the passed vector Value to the LED
void outputt(int outCol[3]){
analogWrite(redPin, 255-outCol[0]);
analogWrite(greenPin, 255-outCol[1]);
analogWrite(bluePin, 255-outCol[2]);
}
//Pulse according to sine grap, nticks = number of clock cycles, tickval = distance in sine space
//amp = amplitude of sinegraph scalar; pticks = array of pause positions; pdurs = array of pause durations
void pulse(int outCol[3], int nticks, float tickval, float amp, int pticks[2], int pdurs[2]){
int noPauses = 2;
float scycle = 0; //track the x position of sine graph
float scal = 0; //the scalar -> y position of sine graph
int redvaltmp, bluevaltmp, greenvaltmp;
int tmpCol[3];
for (int lp = 0; lp < nticks; lp++){
scycle = scycle + tickval;
scal = (amp*sin(scycle)+1)/2; //convert to 0-1 space scalar for led out
for (int i = 0; i < 3; i++){
tmpCol[i] = round(outCol[i] * scal);
}
outputt(tmpCol);
for (int lp2 = 0; lp2 < noPauses; lp2++){
if (lp == pticks[lp2]){
delay(pdurs[lp2]);
}
}
delay(clokfreq);
}
}
//Repeat pulse n times, accept parameters for cycle length and number of cycles
void pulse_durxn(int outCol[3], int ncycles, float cyclelen){
int pticks[2] = {0,0};
int pdurs[2] = {0,0};
pulse(outCol, round(ncycles*cyclelen/clokfreq), 6.284*clokfreq/cyclelen, 1, pticks, pdurs);
}
//Use pulse and add pauses at top and bottom
void breath(int outCol[3], int ncycles, float in_out, float outhold, float inhold, float depth){
// set the breakpoints at the top and bottom of the sine graph 25% and 75% for sine graph that starts at 0
int pticks[2] = {round(0.25*2*in_out/clokfreq),round(0.75*2*in_out/clokfreq)};
//pass the hold delays through
int pdurs[2] = {inhold,outhold};
//in_out*2 is equal to one period of the grpah
//in_out is equal to a breath in cycle or breath our cycle
//2 x in_out/clokfreq = number of ticks for 1 period
//6.284/above term is the length of one tick in sine unit space to cover (6.284 = 2 x pi)
//0.95 to nto use the full range
for (int lp = 0; lp < ncycles; lp++){
pulse(outCol, round(2*in_out/clokfreq), 6.284*clokfreq/(2*in_out), depth, pticks, pdurs);
}
}
//Flicker a colour; nscale is the scaling of the clock
void flicker(int outCol[3], int nscale){
byte flicker[] = {180, 30, 89, 23, 255, 200, 90, 150, 60 , 230, 180, 45, 90};
int tmpCol[3];
for (int i = 0; i < 13; i++){
for (int i2 = 0; i2 < 3; i2++){
tmpCol[i2] = round(outCol[i2]*flicker[i]/255);
}
outputt(tmpCol);
delay(round(clokfreq*nscale));
}
}
void shiver(int outCol[3], int nscale){
byte flicker[] = {180, 180, 30, 180, 180, 30, 180, 180, 30, 30};
int tmpCol[3];
for (int i = 0; i < 10; i++){
for (int i2 = 0; i2 < 3; i2++){
tmpCol[i2] = round(outCol[i2]*flicker[i]/255);
}
outputt(tmpCol);
delay(round(clokfreq*nscale));
}
}
float updatetemp(float newtemp){
float agg = 0;
agg = newtemp;
float updateval = 0.0;
for (int i = 0; i < 49; i++){
movAverage[i] = movAverage[i+1];
agg = agg + movAverage[i+1];
}
movAverage[49] = newtemp;
updateval = agg/50;
return(updateval);
}
void to_spreadsheet( ){
//check if 1 minute has elapsed
if( last_published + 60000 < millis() ){
Particle.publish( "log_to_spreadsheet", String( lightLevel ) );
Particle.publish( "log_to_spreadsheet", String( avetemp ) );
last_published = millis();
}
}
void loop() {
//PROC: read in all sensors INPUT: Get from sensors OUTPUT: sensLightIn; senTempIn; sensSoilIn
//Read light level
lightLevel = analogRead(lightPin);
//Particle.publish("light_reading", String(lightLevel));
//Read temp
reading = analogRead(tempPin);
// The returned value from the device is going to be in the range from 0 to 4095
// Calculate the voltage from the sensor reading
double voltage = (reading * 3.3) / 4095.0;
// Calculate the temperature and update our static variable
temperature = (voltage - 0.5) * 100;
avetemp = updatetemp(temperature);
// Now convert to Farenheight
temperatureF = ((temperature * 9.0) / 5.0) + 32.0;
//Code for demo mode
int ncyc = 1;
if (demomode == true){
if (ndemo < ncyc){
//normal
lightLevel = 1500;
avetemp = 15;
}else{
if (ndemo < 2*ncyc){
//bright
lightLevel = 2500;
avetemp = 15;
}else{
if (ndemo < 3*ncyc){
lightLevel = 500;
avetemp = 15;
}else{
if (ndemo < 8*ncyc){
lightLevel = 1500;
avetemp = 30;
}else{
if (ndemo < 12*ncyc){
lightLevel = 1500;
avetemp = -2;
}
}
}
}
}
ndemo ++;
}
if (ndemo > 12*ncyc){
ndemo = 0;
}
//OUtput to console for monitoring
Particle.publish("temperature", String(temperature));
Particle.publish("avetemp", String(avetemp));
//PROC: emote - NOTE only two sensors implemented
//If both Agg < and Imm < - emote urgent;
//If Agg < and Imm > - emote gentle
//If Agg > then emote well
// Hierarchy - Temp, Light, Water
//Well - Green stable - Pulse breath DONE
//Too Hot - Flicker flame - Flicker flame DONE
//Too Cold - Shiver cold - SOLVED
//Too Dark - Green dim
//Too Bright - Green bright
//Thirsty - Sick looking blue - May be too difficult
//Swamped - Yellow blue - May be too difficult
if (avetemp < 10){
//shiver
eColor[0] = 0; eColor[1] = 0; eColor[2] = 255; //Red works better
shiver(eColor, 3); //set to 100ms 10x 10ms clock
tempNormal = false;
}else{
if (avetemp > 25){
//falme
//eColor[0] = 89; eColor[1] = 35; eColor[2] = 13; //Flame color - did not work
eColor[0] = 255; eColor[1] = 0; eColor[2] = 0; //Red works better
flicker(eColor, 10); //set to 100ms 10x 10ms clock
tempNormal = false;
}else{
tempNormal = true;
}
}
Particle.publish("TempNormal", String(tempNormal));
if (tempNormal == true){
if (lightLevel < 1000){
//Sleep
eColor[0] = 0; eColor[1] = 255; eColor[2] = 0;
breath(eColor, 4, 2000, 1000, 400, 0.8);
}else{
if (lightLevel > 2000){
//Pant
eColor[0] = 0; eColor[1] = 255; eColor[2] = 0;
breath(eColor, 8, 120, 20, 0, 0.9);
}else{
//Live
eColor[0] = 0; eColor[1] = 255; eColor[2] = 0;
breath(eColor, 6, 1000, 800, 200, 0.8);
}
}
}
//Output to IFFFT
to_spreadsheet();
//tick the clock over
delay(clokfreq);
}
Click to Expand
Connecting to IFFFT
Connecting to the IFFFT. A key benefit of a product such as this is the fact that it can log data and allow the user to understand what is going on with the plant. Ultimately, this could be used to make suggestions to the user on how better to take care of the plant.
In order to link this to and log the data, it uses the following IFFFT app:
I envision that the project could be taken further in the following ways:
It was overall an enjoyable project. I did however encounter some difficulties while executing it:
References:
This project is only accessible by signed in users. Be considerate and think twice before sharing.
A hands-on introductory course exploring the Internet of Things and connected product experiences.
Helping plants and humans to connect, communicate and ultimately foster a closer relationship through the magic of IoT
November 7th, 2019