Este post está separado em basicamente duas partes, a primeira são explicações não técnicas para o problema e a segunda é justamente a explicação técnica de como foi resolvido o problema.
Os principais envolvidos neste projeto foram:
– 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 (Resumo)
Criamos um dispositivo, que fica ao lado da garrafa de café que é utilizado para notificar outras pessoas em um canal no Slack (Mensageiro tipo o skype) quando acaba ou quando um novo café é preparado
Por que?
Algum dia você já se moveu de sua confortável posição para se aproximar de uma garrafa de café, utilizando o que tinha sobrado de motivação, apenas para descobrir que a garrafa se encontrava vazia? Se sim, você sabe o tipo de frustração que eu vou me referir.
Pra quem nunca passou por isso (sinta-se uma pessoa afortunada) vou tentar explicar de outra forma: Sabe naquele dia de calor infernal, quando você abre o congelador e se depara com um maravilhoso e gelado pote de sorvete? Como se sente? Feliz? Então você mais do que de pressa agarra o pote (ele está muito cheio) e abre muito perto do rosto esperando o belíssimo cheiro doce de sorvete de chocolate e quando olha… é FEIJÃO! Sabe esta sensação? A do café é dez vezes pior e mil vezes mais frequente (para mim).
Esta catástrofe, sim CATÁSTROFE, de procurar por café e encontrar apenas a garrafa vazia acontecia eventualmente no BoaCompra, porém a empresa cresceu, e junto com ela veio uma outra cozinha do outro lado do prédio. Não é tão longe assim (sério) mas pior do que encontrar uma garrafa vazia, é atravessar a empresa inteira só para chegar na cozinha e encontrar OUTRA GARRAFA VAZIA!
Como todos sabem: “O programador é uma máquina que transforma café em código”. Logo, esse tipo catástrofe é seríssima e pode parar toda a área de desenvolvimento de software da uma empresa. Pensando nisso resolvemos criar um grupo (no BoaCompra, a gente chama de Guild) para desenvolver este projeto e solucionar este problema.
O que é e como funciona?
Precisávamos de um dispositivo que pudesse notificar um canal no Slack se tinha ou não café em cada uma das cozinhas do BoaCompra, além disso este dispositivo tinha que ser barato e se conectar a internet por meio de wifi (não seria viável passar um cabo de rede até a cozinha).
Este dispositivo tinha que o mais simples possível, Então resolvemos utilizar o microcontrolador ESP8266, que pode ser programado como um Arduino e já possui uma interface Wifi. Construímos com apenas dois botões (um verde e um vermelho), para o usuário informar se acabou ou se foi feito mais café, e um LED RGB que serve de feedback para o usuário através das cores verde, vermelho e azul.
As ações possíveis ficaram as seguintes:
- Se o café acabar, alguém pressiona o botão VERMELHO
- Quando alguém fizer café é só pressionar o botão VERDE
O dispositivo pode assumir os seguintes estados:
- tem café -> LED Verde
- não tem ->LED Vermelho
- Inicializando/Enviando -> LED Azul
- primeira inicialização -> LED Apagado
Uma coisa legal em que pensamos, é que o dispositivo tem que armazenar o seu estado para que, caso fosse desligado e depois ligado novamente (como numa queda de energia), ele pudesse retornar ao seu último estado. Apesar de parecer algo óbvio, isso não é extremamente trivial de se fazer, pois normalmente utilizamos a uma memória volátil (que se perde com a ausência de energia, tipo a RAM do seu computador) para armazenarmos variáveis como a do estado do dispositivo. Aproveitando que ele armazena o estado, programamos também para que o dispositivo só envie a notificação caso o estado mude. Ex.: de tem café para acabou o café, e isso facilita muito o uso (como queríamos)
Outra característica interessante é a forma com que ele conecta no wifi, Sempre que o dispositivo é iniciado (ligado) ele tenta se conectar com uma rede wifi conhecida e se ele não conseguir (como é o caso da primeira vez que ele é ligado) ele inicia um AP (access point), isso significa que é possível utilizar um celular (ou computador) para conectar numa rede wifi que o dispositivo cria e a partir disso ele oferece uma interface para que seja selecionada uma rede e informadas as credenciais dela para o dispositivo se conectar. (Como mostra as imagens a baixo)
Quando o dispositivo está tentando se conectar no wifi, ou quando ele está enviando uma notificação o LED fica azul, e o legal disso é que quando o wifi cai, ou apresenta problemas é bem fácil de identificar, pois o LED que deveria estar verde ou vermelho fica azul. Já aconteceu mais de uma vez a situação que o led ficou azul por bastante tempo e quando fomos verificar, o problema estava no roteador wifi que tinha parado de responder. Reiniciamos o roteador e tudo voltou ao normal, pois programamos o dispositivo, para no caso de falha, ficar tentando se conectar no wifi de 3 em 3 minutos .
Então o fluxo fica assim: suponha que não tenha café (LED Vermelho aceso) e alguém pressione o botão verde, então o LED fica Azul, dizendo que está enviando uma mensagem, e quando o envio é concluído com sucesso o LED fica Verde, indicando que tem café.
Se acontecer algum tipo de erro na hora de enviar a notificação, seja por problemas na rede ou por algum problema no servidor, o dispositivo se reinicia e em seguida tenta se conectar no wifi. Quando a conexão ocorre com sucesso, o dispositivo não tenta enviar a notificação novamente, isso foi uma decisão da equipe, pois esse “novo” estado pode não ser mais o estado atual, como no exemplo a baixo:
Exemplo: Não tinha café (luz vermelha) e quando foi feito alguém apertou o botão verde, mas por algum problema na rede a notificação não é enviada. Então 2h depois a rede é reestabelecida, mas o café ja acabou novamente. Se o dispositivo enviasse uma notificação no Slack as pessoas poderiam pensar que tem café mas não tem, e isso arrancaria as esperanças delas (o que vai justamente contra o objetivo deste projeto). Por este motivo que criamos o estado “sem estado”onde o LED fica apagado.
No cenário onde a mensagem é enviada com sucesso, o dispositivo a envia para um serviço da Amazon AWS (SNS e Lambda, vou explicar melhor depois) e este se encarrega de armazenar em um banco de dados (que pode ser utilizado para tirarmos métricas depois), por fim é enviada uma notificação no canal #boacompra-café do Slack que fica assim:
Neste caso foi feito café na sala 1006 às 11:06 e às 12:05 tanto foi feito café na sala 1007 quando acabou o café da 1006 (devia ser uma segunda-feira daquelas rs).
Aqui começa a parte técnica da coisa
No hardware do dispositivo foram utilizados seguintes componentes:
- 1 NodeMCU (ESP-8266)
- 2 Botões (verde e vermelho)
- 2 Resistores de 10k
- 1 LED RGB
O código que faz esta belezura funcionar é o seguinte:
// 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 && state != STATE_HAVE)
{
state = STATE_HAVE;
stateChanged = true;
} else {
if(button_havent_state == HIGH && 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(&httpClient);
snsClient.setDateTimeProvider(&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 && state >= 0){
return state;
}
return STATE_LOADED;
}
As principais bibliotecas utilizadas foram:
A biblioteca WifiManager (https://github.com/tzapu/WiFiManager) foi utilizada para facilitar a conexão com a rede Wifi disponibilizando um Access Point para o usuário se conectar e lá indicar tanto a rede quanto as credencias para o esp8266 se conectar
Para a conexão com a AWS, utilizamos a AWS-ESP8266-API (https://github.com/internetofhomethings/AWS-ESP8266-API), porém tivemos alguns probleminhas para faze-la funcionar. Tivemos que alterar a linha 17 do arquivo ESP8266AWSImplentations.cpp, adinionando a intrução sclient.setInsecure(); abaixo da instrução WiFiClientSecure sclient; para ela funcionar corretamente.
O processo de envio de uma notificação, parte do esp8266 para um tópico SNS da AWS que “dispara” um Lambda (inscrito neste tópico) para salvar este evento no DynamoDb e enviar uma mensagem no Slack como mostra o diagrama abaixo
Conclusão
O projeto foi um sucesso e já está ajudando as pessoas encontrarem o santo café de cada dia, inclusive descobrimos isso por que um dia estávamos fazendo alguns testes e tivemos que desligar tudo por mais umas 2 horas. Várias pessoas vieram falar conosco, perguntando o que tinha acontecido e dizendo que estavam sentindo falta da notificação, avisando que o café acabou de ser preparado.
Outro indicativo é que a área de desenvolvimento de software aumentou a produção de código em cerca de 37% (brinks rsrs, não temos este tipo de métrica).
Este projeto poderia ser muito mais simples, do ponto de vista técnico, porém decidimos utilizar algumas complexidades tanto para aprendermos novas tecnologias (como o AWS SNS), quanto pra treinarmos nas que já conhecíamos.
No momento que escrevi este post, algumas coisas já mudaram neste fluxo e vou fazer outras postagens mostrando o que, e por que fizemos isso.
A partir deste projeto, algumas outras idéias apareceram e provavelmente vamos desenvolve-las em breve.
Se você tiver alguma dúvida ou sugestão, por favor, deixe um comentário aqui neste post, me envie um e-mail (vitormelon@gmail.com) ou me contate por uma de minhas redes sociais (listadas ao lado)
Obrigado.
Supimpa cara and keep the hard working!
Maneiro pra caramba!
Good Job!!!!!