Manual

do

Maker

.

com

IoT - ESP8266 NodeMCU com MQTT a seu próprio Broker

IoT - ESP8266 NodeMCU com MQTT a seu próprio Broker

Como citado nesse post sobre como configurar um MQTT Broker, vamos agora fazer uma comunicação simples e rápida mostrando controle remoto de uma board NodeMCU, programando emLua (link) através da IDE ESPlorer (link). Se precisar de conceitos sobre MQTT e Broker, leia esse post. Sobre o NodeMCU e o ESPlorer, sugiro que leia esse post.

Construa seu firmware online na hora

Invés de montar um SDK gigante pra construir seu firmware de poucos KB, construa seu firmware online da Frightanic através do site NodeMCU build (link). No site você poderá selecionar os recursos que deseja para seu firmware e ao final bastará mandar construir o firmware, com link que será enviado para o email especificado no site; é o único requisito, seu email! Em aguns minutos seu firmware estará disponível e ficará armazenado por 24hs, nas opções float e int.

Apesar de inicialmente a velocidade de conexão com o novo firmware ser a mesma (9600 kbauds), o tempo de upload ficou perceptivelmente mais rápido com o firmware customizado. Mais uma vantagem é que a verbosidade de erro agora existe! Fiquei muito satisfeito com a construção online e recomendo.

No shot do boot pelo ESPlorer você pode ver informações importantes sobre sua própria build, separada pelas categorias branch, commit, SSL (true/false), modules, além da data do build e versão do SDK utilizado para contruí-lo. Simplesmente fascinante!

LED para a prova de conceito

É o comum mesmo para prova de conceito. Meu propósito inicial era só monitoramento, mas monitoramento não me deu a mesma sensação de fazer o controle remoto através do Broker. Depois vou implementar monitoramento em Arduino, PIC, ARM, MIPS and so on, mas aqui vamos ver como acender um LED remotamente. Para não ficar igual todo mundo que vive usando LED verde de 5mm, vamos usar um LED verde de 3mm (sarcasmo).

Preparando o ambiente

Antes de começar a programar, foi necessário atualizar o firmware do NodeMCU porque não estava conseguindo subir nada a partir do ESPlorer no firmware original. Se você estiver na mesma condição e pretende utilizar o ESPlorer (recomendo muuuuuito), siga o procedimento de atualização de firmware baixando previamente seu firmware personalizado citado mais acima. Eu utilizei o dev build, sem medo. Baixe o ESPTool (link) (Linux), descomprima-o e execute o seguinte comando:

./esptool.py --port /dev/ttyUSB0 write_flash 0x000000 /home/djames/Downloads/nodemcu_float_0.9.6-dev_20150704.bin

Can't autodetect firmware, because proper answer not received.

Essa será a mensagem que você vai receber do ESPlorer na maioria dos casos (porque aconteceu de aparecer a versão pra mim umas duas vezes, não sei se está relacionado à velocidade da serial e não me importei porque meu propósito agora é outro). Não se esqueça de ajustar a velocidade da serial para 9600 na primeira conexão para não ter aborrecimentos, porque infelizmente a IDE não sugere que você faça a mudança nem tentar as velocidades automaticamente.

Primeiro boot

Agora conecte-se ao NodeMCU através da IDE ESPlorer a 9600kbauds e então clique no botão Reset.Ao primeiro boot você verá a mensagem "lua: cannot open init.lua" no firmware original ou no firmware baixado pelo github. No firmware personalizado você não vê essa mensagem.

O arquivo init.lua é como um arquivo rc no Linux, mais semelhante ao init. Se esse arquivo entrar em loop, dificilmente você conseguirá recuperar o boot, tornando-se necessário um novo flashing, mas sem maiores agravos, portanto, não entre em desespero.

Eu tive que fazer várias vezes o procedimento até entender a divisão e testes prévios de códigos em scripts separados, de forma a somente carregá-los posteriormente no init.lua e tê-los iniciando em tempo de boot. Por fim, pesquisando por outras características, achei alguns bons conselhos em relação à divisão dos códigos em scripts e adotarei sempre essa prática conforme descrevo a partir da próxima linha.

config.lua

Esse arquivo conterá apenas constantes que serão utilizadas nos demais scripts.

-- WiFi setup
MySSID    = "SuhankoFamily"
MyPASSWD  = "naoContoAsenha"
STATION   = wifi.STATION


-- MQTT
MQTT_CLIENTID = "ESP_001"
MQTT_HOST     = "192.168.1.230"
MQTT_PORT     = 8888

-- LED
LED_PIN = 2
ON      = 1
OFF     = 0

print("Global vars loaded")

init.lua

O arquivo init.lua apenas fará o start do programa. É o último a subir, porque se você cometer um erro agora, pode dar mais trabalho que eventualmente você teria fazendo na ordem descomplicada.

print("http://www.manualdomaker.com")
dofile("init_man.lua")

init_main.lua

O init.lua fará a chamada desse script, como você pode notar. Fazê-lo desse modo lhe permite testar fazendo dofile() na caixa de comando do ESPlorer ou Ctrl+S para subir-e-executar.

dofile("config.lua")

-- ### led ### --
function light(conditionIs)
    if conditionIs == "ON" then
        gpio.write(LED_PIN, gpio.HIGH)
    else
        gpio.write(LED_PIN,gpio.LOW)
    end
end

-- loop para o blink inicial
gpio.mode(LED_PIN,gpio.OUTPUT)
for i=0,3 do
    light(ON)
    tmr.delay(200000)
    light(OFF)
    tmr.delay(200000)
end
-- contador de tentativas de conexao
retriesCounter = 0

-- #### checker ### --
function checker() 
    if wifi.sta.getip() == nil then
        print("Conectando ao AP...")
        retriesCounter = retriesCounter + 1
    else
        retriesCounter = 0
        ip,mask,gw = wifi.sta.getip()
        print("-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-")
        print("Resumo da conexao:")
        print("IP     : ",ip)
        print("Mascara: ",mask)
        print("Gateway: ",gw)
        print("-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-")
        tmr.stop(0)
        light(ON)

        dofile("main.lua")
    end
end

-- ### configMyWifi ### --
function configMyWiFi()
    wifi.setmode(STATION)
    -- conecta aa sua rede
    wifi.sta.config(MySSID,MyPASSWD)

    -- verificador de conexao
    -- utilizando um LED RGB com o seguinte padrao:
    -- fade vermelho ateh que haja conexao
    -- 5 piscadas de 200ms em branco ao conectar, estabilizando apos isso
    -- parametros:
    -- ID, TEMPO, REPETIR, FUNCAO
    tmr.alarm(0,1000,1,checker)
end    

configMyWiFi()

main.lua

Esse arquivo contém todo o handshake com o protocolo MQTT. Está muito limpinho, uma parte eu fiz idêntica a um exemplo que encontrei, outra parte escrevi do jeito que achei mais interessante. O código só não deve estar muito bonito porque esse é meu segundo programa em Lua, mas pelo menos está funcionando sem bugs.

--como um dicionario em python, apenas criado. Sera alimentado mais adiante
m_dist = {}

--variavel para o loop da conexao
local count = 0 

-- funcao para acender ou apagar o LED.
function manipulateLED(STATUS)
    m:publish("/mcu/LED_status/", "ON/OFF",0,0, function(m) print("ON/OFF") end)

    light(STATUS)

    if STATUS == OFF then
    -- confirma a recepcao de mensagem no topico /mcu/comando
        m:publish("/mcu/LED_status/","LED OFF",0,0, function(m) print("LED OFF") end)
    else
    -- confirma a recepcao de mensagem no topico /mcu/comando
        m:publish("/mcu/LED_status/","LED ON",0,0, function(m) print("LED ON") end)
    end
end

    -- Como dicionario em Python, um array que no momento esta armazenando apenas a funcao de LED
    m_dist["/mcu/comandos/LED_status"] = manipulateLED
    
    m = mqtt.Client("ESP_001", 60, "dobitaobyte", "naoContoAsenha")

    m:lwt("/lwt","online",0,0)

    m:on("connect",function(m) print("nClient: ",
                                    MQTT_CLIENTID, 
                                     "nBroker: ",
                                        MQTT_HOST,
                                     "nPorta : ",
                                        MQTT_PORT,
                                        "nn")
    -- Topico para receber os comandos remotos
    m:subscribe("/mcu/comandos/#",0, function(m) print("Topico COMANDOS alinhado") end)

    end)

    -- na desconexao...
    m:on("offline", function(m) print("Desconectado do Broker")
                              print("Heap: ", node.heap()) 
    end)

    -- dispatcher e interpreter
    m:on("message", function(m,t,pl) print("Payload: ", pl)
                                     print("Topic  : ", t)
                                     if pl~=nil and m_dist[t] then
                                         m_dist[t](pl)
                                     end
                                     
    end)

    -- conexao ao Broker
    m:connect(MQTT_HOST, MQTT_PORT, 0, 1)

A nuance entre o refresh e o reflash

Pode ser tão comum quanto um F5. Se tiver um bug em seu script init.lua que o mantenha em um loop infinito, meu amigo, tenha certeza que a única solução é fazer um reflash. Eu fiz algumas tantas vezes até entender como as coisas funcionam (se é que eu entendi mesmo). Se for para testar, faça scripts à parte. Se der um problema federal, o sistema vai reiniciar sem executar o script, mas se esse script estiver sendo chamado dentro do init.lua, problemas na certa.

Código MQTT para NodeMCU

Inicialmente o propósito era apenas monitorar a luminosidade do ambiente com o sensor TSL235R, o qual escrevi um post para Arduino há alguns anos. Não vou utilizá-lo nesse post porque tem 1 bug e ainda não encontrei; ele está rodando por um determinado periodo e depois pára. Teve caso de reiniciar a MCU. Você consegue encontrar um bug? Veja o código em Lua.
Será necessário analisarmos a board em questão (NodeMCU) para escolher o pino a utilizar para a leitura do sensor. Encontrei um pinout bem bonito fornecido por eles mesmos. Em seguida vamos ao código.

Mais uma vez enfatizo que estou começando a aprender Lua e esse é meu segundo programa, então sinto-me à vontade para utilizar um código consolidado. De qualquer modo, alguma coisa do código a seguir consigo explicar. Além do mais, a leitura do sensor estou fazendo tal qual no meu post sobre TSL235R. Vou portar o código C++ para Lua, por isso coloco-o aqui para comparação, talvez você codifique melhor:

volatile unsigned long cnt = 0;
unsigned long oldcnt = 0;
unsigned long t = 0;
unsigned long last;

void irq1()
{
  cnt++;
}

void setup() 
{
  Serial.begin(9600);
  Serial.println("START");
  pinMode(2, INPUT);
  digitalWrite(2, HIGH);
  attachInterrupt(0, irq1, RISING);
}

void loop() 
{
  if (millis() - last > 1000)
  {
    last = millis();
    t = cnt;
    unsigned long hz = t - oldcnt;
    Serial.print("FREQ: "); 
    Serial.print(hz);
    Serial.print("t = "); 
    Serial.print((hz+50)/100);  // +50 == rounding last digit
    Serial.println(" mW/m2");
    oldcnt = t;
  }
}

Como você pode ver acima, a interrupção trata exclusivamente de incrementar a variável cnt. No loop, millis() é chamado para verificar se a diferença de last é igual ou maior que 1 segundo. O pino está em pull-up e é entrada (leitura). O pino utilizado na interrupção (0) é o pino 2 do Arduino UNO ou Leonardo, tanto faz.

Eliminei todo o código relacionado ao MQTT; esse script era todo o programa, antes de eu entender melhor como manipular scripts Lua para NodeMCU. Deixei essa parte para que você possa utilizar e debugar, ou pelo menos ajudar a debugar antes que eu faça um novo post com a solução permanente para o sensor. Por estar "meia-boca", preferi não adicioná-lo a este post, como citei anteriormente.

-- criar a interrupcao no GPIO12 (D6)               --
cnt        = 0 -- acumulador da interrupcao         --
oldcnt     = 0 -- diferenca do acumulador           -- 
t          = 0 -- migracao do valor de cnt          --
lst        = 0 -- manipulacao da diferenca de tempo --
strValue   = 0 -- valor para string                 --
SENSOR_PIN = 5 -- D5 na board                       --

oneSecond  = false

-- funcao da interrupcao
function adder(level)
    -- incremento pela interrupcao
    if cnt == nil then
        cnt = 0
    end
    
    cnt = cnt+1
    
    if oneSecond then
        pcall(sender)
        
        -- entrou na condicional, passar para false
        oneSecond = false
        
        -- se chegou no limite definido, volta tudo a 0
        if cnt > 32000 then
            cnt      = 0
            oldcnt   = 0
            t        = 0
            lst      = 0
            strValue = 0
        end
    end
end

-- tmr registra 1 segundo, entao muda a flag para o adder
function protector()
    oneSecond = true
end

-- funcao dos informes
function sender()
    t = cnt
    
    if t == nil or oldcnt == nil then
        t      = 0
        oldcnt = 0
        cnt    = 0
    end

    Hz = t - oldcnt
    val = (Hz+50)/100
    oldcnt = t
    
    if val == nil then
        val = 0
    end
    strValue = tostring(val)
    
    if strValue ~= nil then
        print("Luminancia: " .. strValue .. " mW/m2")
    end
end

-- interrupcao
-- gpio.mode(PINO,MODO, ESTADO)
-- PINO  : O pino a usar, conforme o pinout da da board
-- MODO  : INPUT,OUTPUT ou INT(funcao)
-- ESTADO: PULLUP ou FLOAT - float eh default
gpio.mode(SENSOR_PIN,gpio.INT)

-- gatilho - em que momento considerar interrupcao
-- PINO   : pino a monitorar
-- MODO   : gpio.INT para tratar como interrupcao
gpio.trig(SENSOR_PIN,"up",adder)

-- Timer com tmr
-- O NodeMCU eh fabuloso.
-- id: 0~6, alarmer id.
-- Interval: alarm time, unit: millisecond
-- repeat: 0 - one time alarm, 1 - repeat
-- function do(): callback function for alarm timed out
-- tmr.alarm(0,1000,1,sender)
tmr.alarm(0,1000,1,protector)

Nessa prévia você vê como fica a comunicação na serial e a recepção pelo Broker, exibido aí no terminal. No video estou apenas mostrando que de fato o LED apaga e acende através do aplicativo MyMQTT, fazendo o papel de client até que eu exiba novas implementações em outros posts (é, realmente estou empolgado com isso).

USE SSL NA COMUNICAÇÃO COM O BROKER

Se você estiver fazendo um laboratório ou se for em um ambiente que não haverá consulta na nuvem nem acesso público, não há problema em utilizar a conexão aberta como fiz. Mas se pretende implementar seriamente, não deixe de implementar SSL porque senão o usuário e senha pode ser sniffado. Por exemplo:

tcpdump -i wlan0 -vvv -XXX -s4192 port 8883

17:17:36.226677 IP (tos 0x0, ttl 255, id 19, offset 0, flags [none], proto TCP (6), length 44)
    192.168.1.234.43595 > 192.168.1.230.8883: Flags [S], cksum 0x9531 (correct), seq 39429, win 5840, options [mss 1460], length 0
        0x0000:  303a 643a 08cc 18fe 34f1 f94e 0800 4500  0:d:....4..N..E.
        0x0010:  002c 0013 0000 ff06 3698 c0a8 01ea c0a8  .,......6.......
        0x0020:  01e6 aa4b 22b3 0000 9a05 0000 0000 6002  ...K".........`.
        0x0030:  16d0 9531 0000 0204 05b4                 ...1......
17:17:36.226711 IP (tos 0x0, ttl 64, id 0, offset 0, flags [DF], proto TCP (6), length 44)
    192.168.1.230.8883 > 192.168.1.234.43595: Flags [S.], cksum 0x6957 (correct), seq 1350402059, ack 39430, win 29200, options [mss 1460], length 0
        0x0000:  18fe 34f1 f94e 303a 643a 08cc 0800 4500  ..4..N0:d:....E.
        0x0010:  002c 0000 4000 4006 b5ab c0a8 01e6 c0a8  .,..@.@.........
        0x0020:  01ea 22b3 aa4b 507d 800b 0000 9a06 6012  .."..KP}......`.
        0x0030:  7210 6957 0000 0204 05b4                 r.iW......
17:17:36.842410 IP (tos 0x0, ttl 255, id 21, offset 0, flags [none], proto TCP (6), length 99)
    192.168.1.234.43595 > 192.168.1.230.8883: Flags [P.], cksum 0x2b2b (correct), seq 1:60, ack 1, win 5840, length 59
        0x0000:  303a 643a 08cc 18fe 34f1 f94e 0800 4500  0:d:....4..N..E.
        0x0010:  0063 0015 0000 ff06 365f c0a8 01ea c0a8  .c......6_......
        0x0020:  01e6 aa4b 22b3 0000 9a06 507d 800c 5018  ...K".....P}..P.
        0x0030:  16d0 2b2b 0000 1039 0004 4d51 5454 04c6  ..++...9..MQTT..
        0x0040:  0078 0007 4553 505f 3030 3100 042f 6c77  .x..ESP_001../lw
        0x0050:  7400 076f 6666 6c69 6e65 000b 646f 6269  t..offline..

dobi

        0x0060:  7461 6f62 7974 6500 0866 736a 6d72 3131

taobyte..senha12

        0x0070:  32

3

17:17:36.842472 IP (tos 0x0, ttl 64, id 14662, offset 0, flags [DF], proto TCP (6), length 40)

Apesar de eu ter colocado suporte a SSL em ambos, tanto o broker como nesse novo firmware NodeMCU, não fiz nenhum teste ainda, vou fazer um post mais específico apenas sobre implementação de alguns recursos extras como base de dados diferentes para fazer persistência, etc.

Pra finalizar, um video simples de tudo, só como prova de conceito.

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.