Manual

do

Maker

.

com

ESP8266 - Como configurar atualização OTA por MQTT (2 de 2)

ESP8266 - Como configurar atualização OTA por MQTT (2 de 2)

Atualização OTA

Na parte 1 de 2 expliquei em detalhes as configurações de todos os serviços necessários para criar sua estrutura de gerenciamento IoT sem depender de um broker público ou da contratação de um serviço (parte 1 sobre atualização OTA). O broker nesse caso que estou criando, é um Raspberry Pi 2. Estar com o broker dentro da mesma rede que os dispositivos tem seus prós e contras. O ideal é sim ter um broker na nuvem e utilizando SSL no canal de comunicação. Aqui estou fazendo uma configuração doméstica para exemplo de  e também para meus propósitos.

A necessidade de ter um broker na nuvem é principalmente pelo fato de disponibilidade. Se ficar sem internet em casa, nem broker, nem nada.

Se você seguiu o exemplo de configuração do broker com as ACLs especificadas aqui, basta seguir adiante. Em outro caso, volte ao post anterior e verifique se não esqueceu algum detalhe.

O propósito agora é interagir com um ESP8266 - no caso, o Wemos mesmo - e isso, utilizando as ACLs definidas no meu artigo sobre configuração do broker. Nesse post utilizaremos apenas o tópico '/mcu', mas não remova o tópico 'casa/' se pretende implementar os controles domésticos que iniciarei tão logo haja tempo (não era pra contar, era surpresa...).

Só pra finalizar, eu sei que estamos em uma verdadeira pandemia com o coquetel de doenças oferecido pelo Aedes Aegypti e falar de mosquito nesse momento pode causar um certo desconforto (já me cocei umas 3 vezes só nesse parágrafo), mas tenha bons pensamentos, esse mosquitto vai te deixar feliz!

Programação do ESP8266

O MQTT é apenas um carteiro; ele recebe a mensagem e entrega ao destinatário (quando o destinatário vier checar se tem algo pra ele). Nesse caso, devemos nos ater na implementação do código no ESP8266, que deverá reagir a cada mensagem lida. Vamos criar duas condições diferentes, depois explico o motivo.

Controle de um LED

Vamos implementar um controle básico de um LED. Eu sei que é um verdadeiro sacão usar sempre LEDs em provas de conceito, mas se você consegue ligar um LED, consegue acionar um relê. Cada pino de GPIO pode suprir até 20mA, dá pra usar LED de 5mm tranquilamente, mas vou ficar com o de 3mm que já estava jogado na mesa quando tive a ideia.

O jeito mais simples de programar é planejar previamente e, se não ficar adequado, reescrever. Se não quiser pensar, é só pegar o código e compilar, depois quando entregar curriculo, torça pra que ninguém te ponha à prova quanto aos seus conhecimentos de programação .

Planejamento

O propósito é informar via MQTT ao ESP8266 que há atualização para ele. O bom em utilizar MQTT é que você pode mandar o nome do arquivo que ele deve pegar. Pensando desse modo, só precisamos de 2 estados da informação; vamos chamar a ausência de atualização de "NULL" e a disponibilidade de atualização de "firmware-123.bin".  Não sei se você já reparou, mas utilizar MQTT é tão flexivel que permitirá a você despejar montes de firmwares no mesmo diretório e depois informar a cada dispositivo qual lhe pertence. Isso também permite fazer um rollback caso a nova implementação precise ser recuada.

O LED está aí para mostrar  a alternância entre as tarefas, mas também pra amadurecer a ideia do conceito.

  • Enviar comando para o ESP8266 manipular o LED e receber seu status.
  • Informar o ESP8266 a respeito de atualizações.

São duas tarefas distintas e para ambas, os pontos extremos não se conhecem; não há interação direta entre os dispositivos que se comunicam com o broker e o client que faz consultas.

Em relação à atualização OTA, o ESP8266  mantém a informação no tópico /mcu/fw_update  e escreve a mudança de estado para /mcu/fw_version. As tarefas se dividem então em:

LED

/mcu/LED - ON/OFF, enviando 1 para ON e 0 para OFF

O client manda ON/OFF para para o tópico /mcu/LED quando desejar ligar ou desligar o LED.

LED_status

Aqui fica guardado o status real atual do LED, exibindo "ON" ou "OFF".

/mcu/fw_version

Exibe a informação do firmware atual. Por exemplo, você pode mandar um update e a MCU resetar antes de fazer a atualização por algum bug. Pouco provável? - Não, não. Infelizmente não estou conseguindo fazer o update desse modo porque o WDT reinicia a MCU mesmo quando não encontra um firmware. Já tentei o sketch puro, mas nessa board Wemos não funciona, não tem jeito. Até o firmware eu compilei o meu próprio (você verá no próximo post). Enfim, dá pra ver no video quando o objeto de atualização é chamado e a mensagem de despedida do firmware velho é exibida. Bem bonitinho.

OTA

/mcu/fw_update - nome_do_firmware.bin

A atualização via OTA nesse caso é executada pela MCU, porém ela só vai buscar atualização se receber um nome no tópico e esse nome for diferente da versão já instalada. Aqui será necessário implementar um pequeno conjunto de regras. O firmware deverá ter um define com o nome do firmware. Por exemplo:

#define FW_VERSION "wemos-0.1"

Do lado client poderá ser implementada qualquer lógica desejada em relação ao informe da atualização com sucesso. Mas e se durante a atualização do firmware o dispositivo não voltar? - Nesse caso, pareceria que até o momento o dispositivo não viu que existia atualização para ser feita. Essa condição precisa obrigatoriamente ser tratada, mas nessa prova de conceito não me darei ao trabalho.

/mcu/reconnection

Esse tópico guarda o timestamp da reconexão. Pode ser bom para saber os momentos que acontecer a desconexão, já que a reconexão é automática.

Client MQTT

Será necessário um client que possa se comunicar com o broker. Por aí o comum é utilizar um web service para interação do usuário, mas para debug e deploy não há nada melhor que um aplicativo que permita interagir sem muito compromisso. Eu utilizo (e recomendo) o MyMQTT. A configuração é simples, vou mostrar no video como configurar, subescrever e publicar no broker com ele. Só tem uma questão que poderá impedi-lo de participar dessa configuração; esse app é para Android. Se você tem outra coisa que não seja Android, procure algum app para a tarefa.

Codificando

Para fazer a verificação periódica consultando via web, eu escrevi esse post. Nele, utilizo também um recurso da API do ESP8266, que é o timer, de forma que a verificação é assincrona. Podemos fazer o intervalo de duas maneiras; uma é através de millis(), que funciona bem se você não tem nenhum compromisso com precisão. Cito isso porque se houver algum delay no loop, certamente haverá diferença de tempo também na comunicação com o broker. Por outro lado, se estiver acontecendo uma tarefa de maior prioridade que a comunicação com o broker, uma interrupção pode ser prejudicial. Tentei usar o timer, mas não funcionou bem em conjunção com as demais bibliotecas, acabei usando algo parecido com o millis(), mas da própria API do ESP8266. O millis também deu vários WDT. Delay, nem pensar, só um yield muito humilde no final do loop, senão, de novo reset pelo WDT. Gostaria de acrescentar que a função de timer utilizado está disponível através da API do ESP8266 e a estrutura do método contempla a chamada de uma função de callback, mas além de não ter sido possível usar o timer, o MQTT tem um callback, ia virar uma zona. E por falar nisso, estou fortemente inclinado a escrever um post só sobre a API do ESP8266, mas por enquanto, vamos fazer "o mais Arduino possível".

PubSubClient MQTT

Você encontrará várias bibliotecas para comunicação MQTT procurando pouco. Eu optei por essa, que você encontra no Library Manager da IDE do seu Arduino. Nesse caso, não poderei abstrair porque o comportamento da biblioteca certamente varia das demais. A documentação completa da API você encontra aqui.  A biblioteca oferece vários exemplos, mas eu estava na dúvida de como funcionava o tratamento da resposta de um subscribe, por isso procurei pela documentação. A documentação discorre que se o client se subescrever para tópicos no broker, obrigatoriamente será necessário declarar uma função callback() no seguinte formato:

void callback(const char[] topic, byte* payload, unsigned int length)

Não gosto de descrever métodos e funções, mas nesse caso será realmente necessário fazê-lo para não ter que explicar isso durante a exibição do código.

Parâmetros

  • topic - O tópico à qual a mensagem será entregue.
  • payload - payload da mensagem (dã).
  • lenght - o comprimento do payload da mensagem.

A documentação descreve que internamente o client utiliza o mesmo buffer para mensagem de entrada e de saída. Quando do retorno da função de callback (ou se uma chamada publish/subscribe ocorrer) , topic e payloadpassados para a função serão sobrescritos. Por isso, é necessário que façamos uma cópia destes valores na aplicação, caso a interação com o retorno seja desejado (e obviamente, o é).

Enfim, o código ficou grande, mas o plugin do Wordpress permite copiar facilmente o código, ele tem uma barrinha no começo, toque com o mouse a primeira linha do código, então copie para um novo sketch. Eu não colocaria um código tão grande em um post, mas o código tem comentários das funcionalidades. Nesse caso, acho que o código por aqui será necessário.

#include <ESP8266WiFi.h>
#include <WiFiClient.h>
#include <PubSubClient.h>

#include <Arduino.h>
#include <ESP8266WiFiMulti.h>
#include <ESP8266HTTPClient.h>
#include <ESP8266httpUpdate.h>

ESP8266WiFiMulti WiFiMulti;
HTTPClient http;

#include "/home/djames/senhas.h"

/* Versao do firmware. Pode receber um monte de parsing, mas
    nessa prova de conceito simplesmente mudarei o nome
*/
#define FW_VERSION "wemos-0.1"

//ligado, desligado e pino do LED. 4 corresponde ao 14 do ESP
#define ON  1
#define OFF 0
#define LED 4

//estrutura dos dados de login para nao precisar fazer smoothing no video
login userInfo;

#define ID   userInfo.WEMOS
#define USER userInfo.dobitaobyte
#define PASS userInfo.PASS_BROKER

//API de comunicacao com o ESP
extern "C" {
#include "user_interface.h"
}

/*Veja o post de timer com ESP8266:
  http://www.manualdomaker.com/article/timer-com-esp8266-na-ide-do-arduino/ */
os_timer_t mTimer;

const char* mSSID  = userInfo.SSID;
const char* mPASS  = userInfo.PASS_WIFI;
const char* BROKER = "ns1.dobitaobyte.lan";

unsigned long timestamp = 0;

unsigned long tOld = 0;
unsigned long tNow = 0;
unsigned long tSum = 0;

char msg[15]  = {0};
char temp[15] = {0};
char stat[4]  = {0};

bool timeout   = false;
bool led_is_on = false;

WiFiClient wificlient;

/*Funcao de callback da classe PubSubClient. Se o client se subescreve
  para topicos, eh necessario ler o callback
*/
void callback(char* topic, byte* payload, unsigned int length);

/* Instancia do PubSubClient
    Broker: Endereco do broker
    Porta: 1883
    callback: funcao de callback
    atrelado: conexao wifi ou ethernet, conforme o caso
*/
PubSubClient client(BROKER, 1883, callback, wificlient);

//Funcao para limpar o array de char da mensagem
void clear(char *target, int lenght) {
  for (int i = 0; i < lenght; i++) {
    target[i] = 0;
  }
}

//Pega informacao de um topico
void getTopic(char *mTopic) {
  strcpy(msg, "/mcu/");
  strcat(msg, mTopic);
  client.subscribe(msg);
  Serial.print("Topic: ");
  Serial.println(msg);
  clear(msg, 15);
}

//Funcao de callback do timer. esta tudo declarado mas nao esta em uso
void tCallback(void *tCall) {
  timeout = true;
}

//inicializador do timer
void usrInit(void) {
  os_timer_setfn(&mTimer, tCallback, NULL);
  os_timer_arm(&mTimer, 1000, true);
}

//funcao para iniciar a conexao wifi
void connectWiFi() {
  WiFi.begin(mSSID, mPASS);
  Serial.print("Trying to connect Wifi... ");

  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
  }
  Serial.println("");
  Serial.println("Done.");
  Serial.print("IP: ");
  Serial.println(WiFi.localIP());
}

//checa a conexao com o broker. se necessario, reconecta
void checkBrokerConnection() {
  while (!client.connected()) {
    if (client.connect(ID, USER, PASS)) {
      Serial.println("Connection to broker had successfuly");
      timestamp   = millis();
      String t = String(timestamp);
      clear(temp, 15);
      t.toCharArray(temp, sizeof(t));

      client.publish("/mcu/BrokerConn", temp, 1);
      getTopic("LED");
      getTopic("fw_update");
      return;
    }
    Serial.println("Trying connect to Broker. Please, wait...");
    delay(3000);
  }
}

void doLED(byte turnOnOff) {
  digitalWrite(LED, turnOnOff - 48);
  //byte eh unsigned char, portanto 0 eh 48
  led_is_on = turnOnOff > 48 ? true : false;
}

void doUpdate(char *fullString) {
  t_httpUpdate_return ret = ESPhttpUpdate.update(fullString);
  switch (ret) {
    case HTTP_UPDATE_FAILED:
      Serial.println("HTTP_UPDATE_FAILED");
      break;

    case HTTP_UPDATE_NO_UPDATES:
      Serial.println("HTTP_UPDATE_NO_UPDATES");
      break;

    case HTTP_UPDATE_OK:
      Serial.println("HTTP_UPDATE_OK");
      break;
  }
}

//executa acao de acender o led ou buscar pelo firmware
void analyser(byte *msg, int lenght) {
  //se for firmware...
  if (msg[0] != '1' && msg[0] != '0') {
    char fw_name[40];
    clear(fw_name, 40);
    strcpy(fw_name, (char*)msg);
    if (strcmp(FW_VERSION, fw_name) == 0) {
      return;
    }
    Serial.println(fw_name);
    clear(fw_name, 40);
    strcpy(fw_name, "http://ns1.dobitaobyte.lan/");
    strcat(fw_name, (char*)msg);
    Serial.print("New version available: ");
    Serial.println(fw_name);
    Serial.println("Starting update. Nice to meet you. Bye!");
    doUpdate(fw_name);
    return;
  }
  doLED(msg[0]);
}

//callback da classe PubSubClient, explicado la no comeco do codigo
void callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("callback called: ");
  byte* p = (byte*)malloc(length);
  memcpy(p, payload, length);
  Serial.println(p[1]);
  analyser(p, length);
  client.publish("/mcu/callback", p, length);
  free(p);
}

void setup() {
  delay(2000);
  pinMode(LED, OUTPUT);
  digitalWrite(LED, OFF);

  Serial.begin(115200);
  connectWiFi();
  //client.setServer(BROKER, 1883);
  //usrInit();
}

void loop() {
  client.loop();
  if (WiFi.status() != WL_CONNECTED) {
    connectWiFi();
  }
  checkBrokerConnection();

  tNow = system_get_time() / 1000;
  tSum = (tNow - tOld) / 1000;
  if (tSum > 10) {
    timeout = true;
    tOld = tNow;
  }

  if (timeout) {
    //getTopic("LED");
    //getTopic((char*)"LED");
    //getTopic((char*)"firmwareUpdate");
    clear(stat, 4);
    stat[0] = 'O';
    if (led_is_on) {
      stat[1] = 'n';
    }
    else {
      stat[1] = 'f';
      stat[2] = 'f';
    }

    client.publish("/mcu/LED_status", stat, 3);
    client.publish("/mcu/fw_version", FW_VERSION, 1);
    timeout = false;
  }
  yield();
}

No vídeo você vê o upload do sketch e a comunicação acontecendo com informativos na serial. Em dado momento eu mostro algumas das mensagens que são tratadas sendo exibidas no console e finalizo informando ao ESP8266 que tem um firmware novo pra ele.

Prometo que vou fazer o mesmo processo com o NodeMCU, percebi vários comportamentos estranhos com o Wemos, mesmo tendo compilado meus próprios firmwares. Não sei se por alguma razão a alimentação na USB não está sendo suficiente ou outro problema que seja, mas não importa, certeza que o video vai te deixar animado para reproduzir esse processo.

Inscreva-se no nosso canal Manual do Maker Brasil no YouTube.

Próximo post a caminho! 

Nome do Autor

Djames Suhanko

Autor do blog "Do bit Ao Byte / Manual do Maker".

Viciado em embarcados desde 2006.
LinuxUser 158.760, desde 1997.