BoaCompra-Coffee: IoT that tells you if there’s coffee or not in a Slack channel using ESP-8266 and AWS Lambda

This post is separated into two parts, the first shows non-technical explanations of the problem and the second is just a technical explanation of how the problem was solved.

The main contributors in this project were:
– Gabriel Sabadin Facina
– Jeferson Capobianco da Silva
– Luiz Fernando Galdino Dubiela
– Marcelo Gimenes de Oliveira
– Rafael Dantas
– Vitor Gabriel Melon
– Welington Jafer Cruz
– Ricardo Rodrigues

TL;DR

We created a device, which lays next to the coffee thermal bottle, that is used to notify other people on a channel on Slack (Messenger like skype) when it ends or when a new coffee is brewed

Why?

Have you ever moved from your comfortable position to approach a bottle of coffee, using what was left of motivation, only to discover that the bottle was empty? If so, you know the type of frustration that I will refer to.

For those who have never been there (feel like a lucky person) I will try to explain it in another way: Do you know that scorching hot day, when you open the freezer and are faced with a wonderful and cold ice cream pot? How do you feel? Happy? So, in a hurry, you grab the pot (it looks brimful) and open it very close to your face waiting for the wonderful sweet smell of chocolate ice cream and when you look … it is BEAN! Do you know this feeling? Coffee is ten times worse and a thousand times more frequent (for me). Ps: This is a common thing in Brazilian homes.

This catastrophe, yes CATASTROPHE, of looking for coffee and finding only the empty bottle eventually happened at BoaCompra (the company I work at), but the company grew, and along with it came another kitchen on the other side of the building. It’s not that far (seriously) but worse than finding ONE empty bottle is crossing the entire company just to get into the kitchen and find ANOTHER EMPTY BOTTLE!

As everyone knows: “A programmer is a machine that transforms coffee into code”. Therefore, this type of catastrophe is very serious and can stop an entire software development area of a company. Thinking about it, we decided to create a group (in BoaCompra, we call it Guild) to develop this project and solve this problem.


What is it and how it works?

We needed a device that could notify a channel on Slack whether or not it had coffee in each one of the BoaCompra kitchens. In addition this device had to be cheap and connect to the internet via wifi (it would not be feasible to pass a network cable through the kitchen).

This device had to be as simple as possible, so we decided to use the ESP8266 microcontroller, which can be programmed as an Arduino and already has a Wifi interface. We built it with just two buttons (one green and one red), sothe user could inform if the coffee is over or if more coffee had been made, and an RGB LED that serves as feedback to the user through the colors green, red and blue.

Possible actions were as follows:

  • If the coffee runs out, someone presses the button RED
  • When someone makes coffee just press the button GREEN

The device can assume the following states:

  • Has coffee -> Green LED
  • Doesn’t have -> Red LED
  • Initializing / Sending -> Blue LED
  • First boot -> LED Off

A nice thing we thought about is that the device has to store its state so that, if it were turned off and then turned on again (as in a power outage), it could return to its last state. Although it seems obvious, this is not extremely trivial to do, as we normally use a volatile memory (which is lost with the absence of power, like your computer’s RAM) to store variables such as the state of the device. Taking advantage of the fact that it stores the state, we also programmed so the device only sends the notification if the state changes. Ex .: from “Has coffee” to “Doesn’t have coffee”, and this makes the use much easier (as we wanted).

Another interesting feature is the way it connects to wifi. Whenever the device is started (on) it tries to connect with a known wifi network and if it fails (as is the case of the first time it is turned on) it starts an AP (access point), that enables to use a cell phone (or computer) to connect to a wifi network that the device creates and from that an interface is offered to the user in order to select a network and inform the credentials. This way the device can connect on the internet. (As shown in the images below)

When the device is trying to connect to the wifi or when it is sending a notification, the LED is blue, and this is interesting because when the wifi is disconnected or has problems, it is very easy to identify since the LED that should be green or red turns blue. This situation (when the led turns blue for a long time) has happened more than once and when we checked we found out the problem was with the wifi router that had stopped responding. We restarted the router and everything went back to normal, because we programmed the device, in case of failure, trying to connect to the wifi every 3 minutes.

So it works like this: suppose there is no coffee (Red LED on) and then someone presses the green button, then the LED turns Blue, showing that it is sending a message, and when it sends successfully the LED turns Green, indicating that has coffee.

If any error occurs when sending the notification, either by a problem in the network or in the server, the device reboots and than tries to connect to the wifi. When it is connected successfully, the device does not try to send another notification. This was a decision made by the team since this “new” state might not be the current state anymore, as shown in the example bellow:

Example: there isn’t coffee (red light) and then someone presses the green button but for any reason the notification isn’t sent. So two hours later the network is restored, but the coffee has already run out again. If the device sends the notification in the Slack people would think that has coffee when in fact it hasn’t, which could pluck up hope going against the goal of this project. For this reason we created the state “stateless” and the LED stays off.

In the scenario where the message was sent successfully, the device sends it to a department of Amazon AWS (SNS and Lambda, I will explain it latter) which takes care of storing in a database (that can be use for metrics). Finally, a notification is sent to the channel #boacompre-café in the Slack that looks like this:

In this case, coffee was made in room 1006 at 11:06 whereas at 12:05 coffee was made at room 1007 when room 1006 ran out of coffee (must have been a difficult Monday).

This is the technical part

In the device’s hardware the following components were used:

  • 1 NodeMCU (ESP-8266)
  • 2 Buttons (green and red)
  • 2 Resistors of 10k
  • 1 LED RGB

The code that makes it works is the following:

// WIFI

#include <ESP8266WiFi.h>
#include <DNSServer.h>
#include <ESP8266WebServer.h>
#include <WiFiManager.h>
#include "ESP8266AWSImplementations.h"

Esp8266HttpClient httpClient;
Esp8266DateTimeProvider dateTimeProvider;

//EEPROM
#include <EEPROM.h>
const int EEPROM_ADDRESS = 0;

// AWS

#include <AmazonSNSClient.h>
#include <ESP8266AWSImplementations.h>

// LOGS 
const int LOGS_ENABLED = false; 

// BUTTONS
const int BUTTON_GREEN = 16;
const int BUTTON_RED = 5;

// LEDS
const int LED_GREEN = 12;
const int LED_RED = 13;
const int LED_BLUE = 4;

// POSSIBLE STATES (CAN CHANGE TO ENUM?)
const int STATE_HAVE = 0;
const int STATE_HAVENT = 1;
const int STATE_LOADING = 2;
const int STATE_LOADED = 3;
const int STATE_ERROR = 4;

// ACTUAL STATE (STARTED AS LOADING)
int state = 2;

// STATE MONITOR
int stateChanged = false;

void setup() {
  // INITIALIZE SERIAL
  Serial.begin(9600);

  // INITIALIZE EEPROM
  EEPROM.begin(4);
  Serial.print("EEPROM: ");
  Serial.println(EEPROM.read(0));

  // INITIALIZE I/O
  pinMode(LED_BUILTIN, OUTPUT);
  pinMode(BUTTON_GREEN,INPUT);
  pinMode(BUTTON_RED,INPUT);
  pinMode(LED_GREEN,OUTPUT);
  pinMode(LED_RED,OUTPUT);
  pinMode(LED_BLUE,OUTPUT);

  // UPDATE LED STATE
  updateLED();

  // CONNECT TO WIFI
  WiFiManager wifiManager;

  wifiManager.setTimeout(180);

  //fetches ssid and pass and tries to connect
  //if it does not connect it starts an access point
  //and goes into a blocking loop awaiting configuration
  if(!wifiManager.autoConnect()) {
    Serial.println("failed to connect and hit timeout");
    delay(3000);
    //reset and try again, or maybe put it to deep sleep
    ESP.reset();
    delay(5000);
  } 

  //if you get here you have connected to the WiFi
  Serial.println("connected...yeey :)");
  delay(10);
  
  // CHANGE STATE AFTER SUCCESSFUL CONNECTION
  state = initializeState();

  Serial.print("state: ");
  Serial.println(state);
  
  // UPDATE LED STATE
  updateLED();
}



void loop() {
  // INITIALIZE STATE MONITOR
  stateChanged = false;
  
  // READ BUTTON STATES
  int button_have_state = digitalRead(BUTTON_GREEN);
  if(button_have_state == HIGH){
    delay(300);
    button_have_state = digitalRead(BUTTON_GREEN);
  }
  
  int button_havent_state = digitalRead(BUTTON_RED);
  if(button_havent_state == HIGH){
    delay(300);
    button_havent_state = digitalRead(BUTTON_RED);
  }

  // LOGS
  if (LOGS_ENABLED) {
    Serial.print("Have: ");
    Serial.println(button_have_state);
    Serial.print("Haven't: ");
    Serial.println(button_havent_state);
  }

  // UPDATE STATE ON BUTTON PRESS
  if(button_have_state == HIGH &amp;&amp; state != STATE_HAVE)
  {
    state = STATE_HAVE;
    stateChanged = true;
  } else {
    if(button_havent_state == HIGH &amp;&amp; state != STATE_HAVENT)
    {
      state = STATE_HAVENT;
      stateChanged = true;
    }
  }

  if (stateChanged) {
   
    // UPDATE LED STATE
    setLEDBlue();
    
    // SEND MESSAGE TO SNS
    if(!sendMessage()){
      saveState(STATE_LOADED);
      ESP.reset();
    }
   
    saveState(state);
    // UPDATE LED STATE
    updateLED();
  }
}

// MESSAGING FUNCTIONS

bool sendMessage() {
  
  ActionError actionError;
  
  AmazonSNSClient snsClient;
  snsClient.setAWSRegion("sa-east-1");
  snsClient.setAWSEndpoint("amazonaws.com");
  snsClient.setAWSKeyID("");
  snsClient.setAWSSecretKey("");
  snsClient.setHttpClient(&amp;httpClient);
  snsClient.setDateTimeProvider(&amp;dateTimeProvider);
  

  PublishInput publishInput;
  publishInput.setTargetArn("arn:aws:sns:sa-east-1:371355836815:bc-coffe-notify");

  char location[10] = "Sala 1006";
  char hasCoffee[5];
  char message[256];

  if (state == STATE_HAVE) {
    strcpy(hasCoffee, "true");
  } else {
    strcpy(hasCoffee, "false");
  }

  snprintf(message, sizeof message, "{\"type\":\"BUTTON\",\"data\":{\"hasCoffee\":%s,\"location\":\"%s\"}}", hasCoffee, location);
  
  publishInput.setMessage(message);
  
  PublishOutput result = snsClient.publish(publishInput, actionError);
  
  
  Serial.print("Action error: ");
  Serial.println(actionError);
  Serial.print("Message ID: ");
  Serial.println(result.getMessageId().getCStr());
  Serial.print("Error type: ");
  Serial.println(result.getErrorType().getCStr());
  Serial.print("Error message: ");
  Serial.println(result.getErrorMessage().getCStr());

  if(actionError > 0){
    return false;
  }
  return true;
}

// LED FUNCTIONS

void updateLED() {
  switch (state) {
    case STATE_HAVE:
      setLEDGreen();
      break;
    case STATE_HAVENT:
      setLEDRed();
      break;
    case STATE_LOADING:
      setLEDBlue();
      break;
    case STATE_LOADED:
      setLEDOff();
      break;
    case STATE_ERROR:
      setLEDOff();
      break;
  }
}

void setLEDOff() {
  digitalWrite(LED_BLUE,LOW);
  digitalWrite(LED_GREEN,LOW);
  digitalWrite(LED_RED,LOW);
}

void setLEDRed() {
  digitalWrite(LED_BLUE,LOW);
  digitalWrite(LED_GREEN,LOW);
  digitalWrite(LED_RED,HIGH);
}

void setLEDBlue() {
  digitalWrite(LED_BLUE,HIGH);
  digitalWrite(LED_GREEN,LOW);
  digitalWrite(LED_RED,LOW);  
}

void setLEDGreen() {
  digitalWrite(LED_BLUE,LOW);
  digitalWrite(LED_GREEN,HIGH);
  digitalWrite(LED_RED,LOW);
}

void saveState(int state){
  EEPROM.write(EEPROM_ADDRESS, state);
  EEPROM.commit();
}

int getState(){
  return EEPROM.read(EEPROM_ADDRESS);
}

int initializeState(){
  int state = getState();
  if(state < 4 &amp;&amp; state >= 0){
    return state;
  }
  return STATE_LOADED;
}

The main libraries used were:

A WifiManager library (https://github.com/tzapu/WiFiManager ) was used to facilitate connection to the Wifi network creating an Access Point for the user to connect and indicate both network and credentials for esp8266 to connect.

To connect to AWS, we used AWS-ESP (https://github.com/internetofhomethings/AWS-ESP8266-API), however we had some problems to make it work. We had to change the line 17 of the file ESP8266AWSImplentations.cpp, adding the instruction sclient.setInsecure(); bellow of the instruction WiFiClientSecure sclient; so it could word properly.

The process of sending a notification goes from esp8266 to a SNS topic of AWS that “shoots” a Lambda (inscribed in this topic) to save this event ate DynamoDb and send a message in the Slack as shown in the diagram bellow

Diagrama

Conclusion

The project was successful and it is already helping people to find coffee, moreover we found out when we were running some tests and we had to turn off the device for about 2 hours. Lots of people came to talk to us saying that there were missing the coffee notifications.

Another indicative of this success is that areas of software development increase production around 37% (this is just a joke, actually we don’t have this kind of information).

This project could have been more simple, from the technical point of view, however we decided to use complex aspects in order to learn new technologies (as AWS SNS) and also to put into practice the one we already new.

At the time of writing this post, some features have already changed and I will write others posts showing what we did and why we did it.

From this project some other ideas came up and we will probably develop them soon

If you have any doubt or suggestion, please be welcome to leave a comment here, to send me an e-mail (vitormelon@gmail.com) or contact me through one of my social networks (listed next)

Thank you.

Leave a Reply

Your email address will not be published. Required fields are marked *