Jumat, 25 November 2016

Tutorial NodeMCU: Mengendalikan ESP8266 behind firewall dengan Android (ESP8266, WeMos, NodeMCU, Lua)

Tulisan ini adalah kelanjutan dari tulisan sebelumnya tentang bagaimana mengakses board IoT yang berada dibelakang firewall yang tidak memperbolehkan port forwarding.

Agar board IoT dapat dikendalikan dari luar melalui Android, padahal IoT board berada dibelakang firewall, berarti bahwa diperlukan suatu perantara diantara kedua perangkat. Dalam hal ini saya akan menggunakan Firebase Realtime Database sebagai sarana perantara tersebut.

Gambar 1. Firebase Database sebagai perantara

Dalam hal ini IoT board akan mengambil nilai request dari database Firebase, kemudian menentukan status led builtin berdasarkan nilai request (on/off), dan selanjutnya akan mengupdate nilai response sebagai konfirmasi bahwa request telah berhasil dilakukan.

startup = true
request = false
position = "off"

database = "https://friendlychat-6ae71.firebaseio.com/"
nodeUpdate = "ledbuiltin.json"
dbSecret = "kia43oq8Nn76kTJQhLIshyRpSYDHoEwjGTI*****"
urlUpdate = database .. nodeUpdate .. "?auth=" .. dbSecret

-- initial payload
payload = "{\"request\":\"".. position .. "\",\"response\":\"".. position .. "\"}"

nodeRead = "ledbuiltin/request.json"
urlRead = database .. nodeRead .. "?auth=" .. dbSecret

function updateToFirebase()

   if not request then --sentinel
      request = true 

      http.request(urlUpdate,
         "PATCH",
         "Content-Type: application/json\r\n",
         payload,
         function(code, data)
            if (code ~= 200) then
               print("Failed : HTTP error code : " .. code)
               request = false
            else
               -- stop alarm, mission succeed
               tmr.stop(0)
               print("Succeed : " .. code, data)
               startup = false
               request = false
            end
         end)
    end
end

function led_builtin(position)
    gpio.mode(4, gpio.OUTPUT)
    if (position == "on") then
        gpio.write(4, gpio.LOW) -- LED BUILTIN ON
    else
        gpio.write(4, gpio.HIGH) -- LED BUILTIN ON
    end
end

function readfromFirebase()

   if not startup then -- sentinel
      if not request then -- sentinel
         request = true
         http.request(urlRead,
            "GET",
            "Content-Type: application/json\r\n",
            nil,
            function(code, data)
               if (code ~= 200) then
                  print("Failed : HTTP error code : " .. code)
                  request = false
               else
                  print("Succeed : " .. code, data)
                  slash = "\\"                   
                  newposition = string.gsub(string.gsub(data,slash,""),"\"","")
                  if position ~= newposition then
                     position = newposition
                     --update response payload
                     payload = "{\"response\":\"".. position .. "\"}"
                     led_builtin(position)
                     tmr.alarm(0,2000,1,updateToFirebase) -- dua detik berikutnya update status
                  end           
                  request = false   
               end
            end)
       end      
    end
end

tmr.alarm(0,2000,1,updateToFirebase)
tmr.alarm(1,4000,1,readfromFirebase) -- setiap 4 detik
Pengembangan aplikasi pada sisi Android akan menggunakan AppInventor yang berfungsi sebagai IDE yang bersifat Cloud yang memungkinkan anda melakukan pengembangan aplikasi Android tanpa menggunakan ADT.

 Gambar 2. Design Screen dan Component pada Aplikasi

Gambar 3. Block Code pada Aplikasi

Selasa, 22 November 2016

Tutorial NodeMCU: Mengendalikan ESP8266 dibelakang Firewall (ESP8266, WeMos, Lua)

Tulisan ini saya buat setelah menyadari bahwa provider saya tidak menyediakan ip public pada interface luar modem saya, sehingga saya tidak dapat mengendalikan iot board saya melalui internet (selama ini uji coba dilakukan secara intranet)

Jika anda memiliki ip public, maka koneksi ke iot board anda dapat dilakukan dengan mudah yaitu dengan mengaktifkan fitur ip forwarding pada modem anda, sehingga koneksi pada port tertentu pada interface external modem anda (melalui ip public) dapat diforward ke host intranet anda pada port tertentu (default gateway dari host intranet yang menjadi target diset ke ip interface dalam modem anda)

Tetapi kondisi yang kita hadapi saat ini adalah provider tidak menyediakan ip public, sehingga kita tidak dapat mencapai iot board dari internet. Setelah melakukan pemikiran akhirnya timbul ide untuk bagaimana kita dapat mengendalikan led-builtin pada board WeMos yang berada dibelakang firewall?

Pada tulisan ini saya mencoba menggunakan firebase realtime database untuk menyimpan request ON/OFF terhadap led-builtin, secara berkala board WeMos akan melakukan query ke Firebase untuk mengambil nilai dari tag tertentu.

Langkah-langkah:
1. Aktifkan ke firebird console anda, dan dapatkan token database Secret anda.
2. Lakukan koding berikut:

startup = true
request = false
position = "off"

-- ganti dengan url database anda
database = "https://friendlychat-6ae71.firebaseio.com/"
nodeUpdate = "ledbuiltin.json"
-- ganti dengan database secret anda
dbSecret = "kia43oq8Nn76kTJQhLIshyRpSYDHoEwjGTI*****"
urlUpdate = database .. nodeUpdate .. "?auth=" .. dbSecret

-- initial payload
payload = "{\"request\":\"".. position .. "\",\"response\":\"".. position .. "\"}"

nodeRead = "ledbuiltin/request.json"
urlRead = database .. nodeRead .. "?auth=" .. dbSecret

function updateToFirebase()

   if not request then --sentinel
      request = true

      http.request(urlUpdate,
         "PATCH",
         "Content-Type: application/json\r\n",
         payload,
         function(code, data)
            if (code ~= 200) then
               print("Failed : HTTP error code : " .. code)
               request = false
            else
               -- stop alarm, mission succeed
               tmr.stop(0)
               print("Succeed : " .. code, data)
               startup = false
               request = false
            end
         end)
    end
end

function led_builtin(position)
    gpio.mode(4, gpio.OUTPUT)
    if (position == "on") then
        gpio.write(4, gpio.LOW) -- LED BUILTIN ON
    else
        gpio.write(4, gpio.HIGH) -- LED BUILTIN ON
    end
end

function readfromFirebase()

   if not startup then -- sentinel
      if not request then -- sentinel
         request = true
         http.request(urlRead,
            "GET",
            "Content-Type: application/json\r\n",
            nil,
            function(code, data)
               if (code ~= 200) then
                  print("Failed : HTTP error code : " .. code)
                  request = false
               else
                  print("Succeed : " .. code, data)
                  newposition = string.gsub(data,"\"","")
                  if position ~= newposition then
                     position = newposition
                     --update response payload
                     payload = "{\"response\":\"".. position .. "\"}"
                     led_builtin(position)
                     tmr.alarm(0,2000,1,updateToFirebase) -- dua detik berikutnya update status
                  end          
                  request = false  
               end
            end)
       end     
    end
end

tmr.alarm(0,2000,1,updateToFirebase)
tmr.alarm(1,4000,1,readfromFirebase) -- setiap 4 detik
2.Setelah script diatas dijalankan, maka pada database Firebase anda  terbentuk node baru dengan nama ledbuiltin yang memiliki dua tag yaitu request dan response yang masing-masing bernilai awal "off"


Gambar 1. Hasil initialisasi nilai awal

3. Ubah nilai tag request dari "off" menjadi "on"

Gambar 2. Hasil perubahan nilai request menjadi "off"

4. Led builtin pada board akan hidup, dan tag response akan diset menjadi "on"

Gambar 3. Hasil respon dari board.

Adapun penjelasan pada koding diatas adalah pada awalnya akan diinitialisasi nilai awal untuk node ledbuiltin pada Firebase dengan tag request dan response ke "off", selanjutnya secara berkala (4 detik sekali) dan dilakukan pemantauan terhadap nilai request dengan menggunakan metode REST yang disediakan oleh firebase. Jika nilai request berbeda dengan nilai pembacaan sebelumnya, maka akan dilanjutkan dengan mengatur led-builtin pada board sesuai dengan nilai pembacaan terbaru. Setelah status led-builtin disesuaikan, maka selanjutnya dalah mengupdate status respon sesuai dengan status led-builtin.

Keyword:  behind the firewall, without using public ip address, and not using port forwarding

Senin, 21 November 2016

Tutorial NodeMCU + AppInventor: Memantau suhu dan kelembaban via Android (ESP8266, WeMos, Lua, DHT11)

Pada tulisan sebelumnya telah kita bahas bagaimana push data dari WeMos ke Firebase Realtime Database dengan menggunakan bahasa pemrograman Lua (anda membutuhkan firmware yang dikustomisasi dengan module dht, http, sntp, rtctimer dan SSL).
NodeMCU custom build by frightanic.com
    branch: master
    commit: 7b83bbb2ea134cd85ac9d63108603cc02c4e20f7
    SSL: true
    modules: dht,file,gpio,http,i2c,net,node,rtctime,sntp,tmr,uart,wifi
 build     built on: 2016-11-15 12:39
 powered by Lua 5.1.4 on SDK 1.5.4.1(39cb9a32)

Module sntp diperlukan untuk sinkronisasi waktu antara WeMos dengan time server melalui NTP dan diupdate ke software RTC yang terdapat pada NodeMCU, module http dan SSL diperlukan untuk melakukan push data ke Firebase yang membutuhkan koneksi https)


Gambar 1. Hasil Push ke Firebase Realtime Database

Untuk melakukan push data ke Firebase Realtime Database silakan mengacu pada tulisan saya sebelumnya.

Pada tulisan ini kita akan memantau data yang dipush ke Firebase dengan menggunakan Android. Untuk memudahkan proses developement, pada tulisan ini kita akan menggunakan App Inventory. Adapun keunggulan dari App Inventor dibandingkan dan Android Studio adalah anda tidak perlu menginstalasi software ADT apapun, cukup memiliki web browser, App Inventor adalah IDE yang ditempatkan di Cloud. Untuk mengaktifkan App Inventor, silakan mengacu pada alamat ini http://appinventor.mit.edu/explore/

Langkah-langkah:
1. Klik pada Create Apps
2. Login dengan Google Account anda
3. Klik pada Project, dan pilih New Project dan isikan FirebaseIOT sebagai nama project.
4. Tambahkan sebuah TextBox dan FirebaseDB komponen ke Screen1

Gambar 2. Komponen TextBox dan FireBaseDB pada Screen1

5. Set properti pada komponen FirebaseDB1 sebagai berikut:


Gambar 3. Setting pada Komponen FirebaseDB1

FirebaseToken merupakan Database Secret project Firebase Anda, yang dapat anda peroleh dari dari Console Firebase (Klik pada Project Setting, pilih tab Account Service, klik pada Database Secret, dan klik pada Show, copy dan paste ke FirebaseToken)

FirebaseURL merupakan URL dari database anda, yang dapat diperoleh dengan klik pada database.


 Gambar 4. FirebaseURL

ProjectBucket diisi node database anda yang dalam hal ini adalah dht11.

6. Buatlah koding untuk Screen1 pada jendela Blocks.

Gambar 5. Koding pada Aplikasi

Block koding diatas dapat dijelaskan bahwa event when FirebaseDB1.DataChanged akan dipicu setiap kali adanya perubahan data pada database anda pada node dht11 sebagai akibat push data dari NodeMCU silakan mengacu pada tulisan saya sebelumnya.

 7. Klik pada Build, dan pilih App (Provide QR code for .apk)

 Gambar 5. Build Aplikasi

8. Scan QRcode untuk download dan install aplikasi, anda perlu mengaktifkan keamanan untuk memperbolehkan instalasi dari sumber yang tidak diketahui pada menu setting perangkat anda.


 Gambar 6. QR Code Aplikasi

7. Berikut ini adalah hasil run aplikasi.

Gambar 7. Hasil Run Aplikasi.




Minggu, 20 November 2016

Tutorial NodeMCU + AppInventor: Mengendalikan LED-BUILTIN pada WeMos D1 mini dengan Android (ESP8266, WeMos, Lua)

Tulisan ini dibuat untuk bagaimana mengintegrasikan antara teknologi IOT dengan mobile sehingga dimungkinkan untuk mengendalikan board WeMos via smartphone Android. Agar pembahasan pada tulisan ini dapat lebih sederhana, kita akan menggunakan pemrograman bahasa Lua pada sisi board WeMos, dan App Inventor pada sisi Android.

Agar dapat melakukan pemrograman Lua, maka pada board WeMos anda perlu diinstalasi firmware NodeMCU. Sedangkan App Inventor dapat diakses pada alamat http://appinventor.mit.edu/explore/.

App Inventor merupakan suatu IDE berbasis cloud yang dapat diakses dengan menggunakan web browser yang mendukung HTML5, sehingga dalam hal ini anda tidak perlu menginstalasi software tambahan pada komputer anda, cukup web browser saja.

Pada tulisan ini saya asumsikan anda telah mengerti pembuatan aplikasi pada board WeMos dengan menggunakan software Esplore.

Koding pada sisi WeMos:


appconfig.lua
cfg={}
cfg.ssid="myssid"
cfg.pwd="mypassword"
cfg.auth=wifi.WPA_WPA2_PSK

dhcp_config ={}
dhcp_config.start = "192.168.4.100"
init.lua
-- aplikasi ini berfungsi menghindarkan module anda terjebak
-- pada kondisi restart tak terhingga jika terjadi bugs pada
-- init.lua.

-- aplikasi ini dapat mengaktifkan board anda sebagai access
-- point, jika anda menginginkan hal ini, maka anda perlu
-- menyediakan file apconfig.lua.

-- aplikasi dapat berjalan pada dua modus, yaitu standalone
-- atau terkoneksi ke wifi.

-- agar dapat terkoneksi ke wifi, maka username dan password
-- didefinisikan pada file credentials.lua

-- aplikasi akan menunggu selama 3 detik sebelum file app.lua
-- dieksekusi, jika terjadi bugs pada app.lua, maka anda
-- dapat menjalankan file.remove("app.lua") sehingga tidak
-- terjebak pada kondisi restart tak terhingga.

function startup()
    print("Running")
    -- aplikasi autoexec anda  sekarang adalah 'app.lua'
    if file.open("app.lua") == nil then
        print("app.lua tidak ditemukan, silakan buat app.lua sebagai auto executable.")
    else
        file.close("app.lua")
        dofile("app.lua")
    end
end

if file.open("apconfig.lua") ~= nil then
    print("activate as access point")
    dofile("apconfig.lua")
    -- set mode as stationAP 
    wifi.setmode(wifi.STATIONAP)
    wifi.ap.config(cfg)
    print("AP IP Address:" .. wifi.ap.getip())  
    pool_ipstart, pool_ipend = wifi.ap.dhcp.config(dhcp_config)
    if wifi.ap.dhcp.start() then
        print("DHCP server started, pool addr from " .. pool_ipstart .. " to " .. pool_ipend .. "!")
    else
        print("DHCP server failed!")
    end
else
    -- default mode as station
    wifi.setmode(wifi.STATION)  
end

-- periksa keberadaan file credentials.lua yang mendefinisikan
-- 'SSID' and 'PASSWORD' wifi
if file.open("credentials.lua") == nil then
    -- bagian module standalone
    print("Boot sebagai module standalone")
    print("You have 3 seconds to abort")
    print("Waiting...")
    -- jalankan fungsi startup setelah 3 detik     
    tmr.alarm(0, 3000, 0, startup)
else  
    -- bagian module koneksi wifi
    file.close("credentials.lua")
    -- muat variable SSID dan PASSWORD
    dofile("credentials.lua")
    print("Koneksi ke " .. SSID)
    wifi.sta.config(SSID, PASSWORD)
    -- wifi.sta.connect() not necessary because config() uses auto-connect=true by default

    -- mencoba koneksi setiap detik sampai tmr.stop(1)
    trycount = 1
    tmr.alarm(1, 1000, 1, function()
        if wifi.sta.getip() == nil then
            print("trying " .. trycount .. " second(s)")
            trycount = trycount + 1
        else
            -- berhasil koneksi dan mendapatkan IP Address
            tmr.stop(1)
            print("WiFi connected")
            print("Client IP Address:" .. wifi.sta.getip())
            print("You have 3 seconds to abort")
            print("Waiting...")
            -- jalankan fungsi startup setelah 3 detik     
            tmr.alarm(0, 3000, 0, startup)
        end      
    end)
end
app.lua
-- aplikasi ini membentuk web server pada board anda
-- dengan kemampuan mengaktifkan LED BUILTIN.
-- untuk mengaktifkan LED BUILTIN maka pin D4 diset ke LOW,
-- dan sebaliknya adalah diset ke ON.

-- web server akan memberikan respon status dari LED BUILTIN
-- untuk mengaktifkan LED BUILTIN maka dilakukan call
-- http://ipaddress?led=on
-- untuk mematikan LED BUILTIN maka dilakukan call
-- http://ipaddress?led=off

position = "off"

function led_builtin(position)
    gpio.mode(4, gpio.OUTPUT)
    if (position == "on") then
        gpio.write(4, gpio.LOW) -- LED BUILTIN ON
    else
        gpio.write(4, gpio.HIGH) -- LED BUILTIN ON
    end
end

function receive(conn, payload)
    --debug
    print(payload)

    -- extra path and variables
    local _,_,method,path,vars = string.find(payload,"([A-Z]+) (.+)?(.+) HTTP")
    if(method==nil) then
       _, _, method, path = string.find(payload,"([A-Z]+) (.+) HTTP")
    end

    -- extract the variables passed in the url
    local _GET = {}
    if (vars~=nil) then
       for k,v in string.gmatch(vars,"(%w+)=(%w+)&*") do
           _GET[k] = v
           --debug
           print(k .. ":" .. v)
       end

       if (_GET['led'] ~= nil) then
            position = _GET['led']      
       end    
    end

    -- turn led ON/OFF
    led_builtin(position)

    local content="LED BUILTIN is " .. position .. "!"
       
    local contentLength=string.len(content)

    conn:on("sent", function(sck) sck:close() collectgarbage() end)
    conn:send("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length:" .. contentLength .. "\r\n\r\n" .. content)
end

function connection(conn)
    conn:on("receive", receive)
end

-- check whether server is already started
if srv~=nil then
    print("stop existing server...")
    srv:close()
    print("OK")
end

-- start a new server
print("start a new server ...")
led_builtin(position)
srv=net.createServer(net.TCP, 1)
srv:listen(80, connection)
print("OK")

 Koding pada sisi App Inventor

1. Aktifkan ke http://appinventor.mit.edu/explore/
2. Klik pada Create Apps!\
3. Login dengan Google Account Anda
4. Klik pada Project, dan pilih New Project
5. Pada Project Name isikan IOT
6. Pada layar Screen1 tambahkan komponen berikut ini

 Gambar 1. Screen1 pada modus Designer

7. Ubah nama dan text masing-masing komponen sesuai dengan tampilan pada layar Designer dan Component.

Gambar 2. Setting pada Component

8. Buatlah koding berikut ini pada layar Block (Bahasa pemrograman pada App Inventor adalah berupa blok LEGO, dimana anda dapat memasang masing-masing blok yang nantinya menjadi satu aplikasi yang terintegrasi)


Gambar 3. Koding pada Layar Block

9. Klik pada Built dan pilih App (provide QR code for .apk)

Gambar 4. Built Aplikasi

10. Scan QR code, jika pada smartphone anda belum terinstalasi QR code scanner, maka anda perlu menginstalasinya terlebih dahulu.

Gambar 5. QR Code untuk download APK ke Smartphone

11. Smartphone akan memblokir instalasi APK dari sumber yang tidak diketahui, sehingga anda perlu mengubah Setting, Security, Unknown Sources (ON)

12. Setelah instalasi dan Run, dan silakukan coba klik pada On dan Off dan perhatikan LED BUILTIN pada board WeMos.
 Gambar 6. Icon aplikasi IOT pada Launcher

Gambar 7. Hasil Run Aplikasi




Selasa, 15 November 2016

Tutorial NodeMCU: Mencatat suhu dan temperatur ke Firebase Realtime Database (ESP8266, WeMos, Lua, DHT11)

Tulisan ini bertujuan menghasilkan aplikasi iot dengan memanfaatkan board WeMos D1 mini yang diinstalasi dengan firmware NodeMCU yang dikustomisasi dengan module dht,  http, rtctimer, sntp yang mendukung float dan tambahan module SSL.

NodeMCU custom build by frightanic.com
    branch: master
    commit: 7b83bbb2ea134cd85ac9d63108603cc02c4e20f7
    SSL: true
    modules: dht,file,gpio,http,i2c,net,node,rtctime,sntp,tmr,uart,wifi
 build     built on: 2016-11-15 12:39
 powered by Lua 5.1.4 on SDK 1.5.4.1(39cb9a32)

 Gambar 1. Pin Sensor DHT11

Untuk pengukuran suhu dan kelembaban, adalah menggunakan sensor DHT11, dimana pin4 dihubungan ke GND pada board WeMos, pin 1 ke VCC 5V, dan pin2 ke D1.

Setiap seperlima menit (12 detik), akan dilakukan pengukuran suhu dan temperatur dan akan dikalkulasi ulang dengan menggunakan exponensial smooting alpha = 20%, dan hasil perhitungan akan di Push ke firebase realtime database yang telah dipersiapkan sebelumnya. Awalnya sistem akan melakukan sinkronisasi waktu software RTC dengan time server melalui protocol NTP.

credentials.lua
Dapat diambil dari tutorial sebelumnya
init.lua
 Dapat diambil dari tutorial sebelumnya
app.lua
pinDHT11 = 1 -- pin D1 pada board WeMos D1 mini
lastTemp = nil --exponential smoothing Temperature
lastHumi = nil --exponential smoothing Humidity

database = "https://friendlychat-6ae71.firebaseio.com/"
node = "dht11.json"
dbSecret = "kia43oq8Nn76kTJQhLIshyRpSYDHoEwjGTI*****"
url = database .. node .. "?auth=" .. dbSecret
print(url)
payload = ""

function readFromDHT11()
    status, temp, humi, temp_dec, humi_dec = dht.read11(pinDHT11)

    if status == dht.OK then
        if lastTemp == nil then
            lastTemp = temp
        else
            lastTemp = math.floor((0.2*temp + 0.8*lastTemp)*100)/100 -- exp snoothing
        end
   
        if lastHumi == nil then
            lastHumi = humi
        else
            lastHumi = math.floor((0.2*humi + 0.8*lastHumi)*100)/100 -- exp snoothing
        end

        wib = rtctime.epoch2cal(rtctime.get()+7*60*60) -- tambah 7 WEST INDONESIA TIME
        timeStr = string.format("%04d/%02d/%02d %02d:%02d:%02d", wib["year"], wib["mon"], wib["day"], wib["hour"], wib["min"], wib["sec"])
        payload = "{\"time\":\"" .. timeStr .."\",\"temperature\":" .. lastTemp .. ",\"humidity\":" .. lastHumi .. "}"
        print(payload)
       
    elseif status == dht.ERROR_CHECKSUM then
        print( "DHT Checksum error." )
    elseif status == dht.ERROR_TIMEOUT then
        print( "DHT timed out." )
    end
end

function postToFirebase()

   http.request(url,
      "POST",
      "Content-Type: application/json\r\n",
      payload,
      function(code, data)
         if (code ~= 200) then
            print("Failed : HTTP error code : " .. code)
         else
            print("Succeed : " .. code, data)
         end
      end)
     
end

sntp.sync("1.id.pool.ntp.org",
 function(sec,usec,server)
   print('setting time to:', sec, usec, "from: " .. server)
   rtctime.set(sec, usec) 
 end,

 function()
   print('failed!')
 end
)

tmr.alarm(0,12000,1,readFromDHT11) -- setiap seperlima menit baca dari sensir DHT11
tmr.alarm(1,60000,1,postToFirebase) -- setiap menit catat ke firebase realtime database

Minggu, 13 November 2016

Tutorial NodeMCU: Web Server dengan Basic Authentication (ESP8266, WeMos, Lua)

Tulisan ini dibuat untuk memaksa user melakukan authentication sebelum mengakses aplikasi yang dapat menghidupkan ataupun mematikan LED BUILTIN melalui web, untuk menjalankan koding berikut ini, anda membutuhkan firmware NodeMCU yang dikustomisasi dengan module crypto karena menggunakan perintah crypto.toBase64()

Gambar 1. Permintaan Authentication

Walaupun Basic authentication memiliki tingkat keamanan yang rendah, dan berpotensi terhadap penyadapan, tetapi adalah memadai untuk pengendalian untuk kepentingan internal.

app.lua
position = "off"

-- Basic Authorization username and password
username = "user"
password = "pass"


function led_builtin(position)
    gpio.mode(4, gpio.OUTPUT)
    if (position == "on") then
        gpio.write(4, gpio.LOW) -- LED BUILTIN ON
    else
        gpio.write(4, gpio.HIGH) -- LED BUILTIN ON
    end
end

function receive(conn, payload)
    --debug
    print(payload)

    -- extra path and variables
    local _,_,method,path,vars = string.find(payload,"([A-Z]+) (.+)?(.+) HTTP")
    if(method==nil) then
      _, _, method, path = string.find(payload,"([A-Z]+) (.+) HTTP")
    end

    local _, _, auth = string.find(payload, "%cAuthorization: Basic ([%w=\+\/]+)");--Authorization:
      if (auth == nil or auth ~= crypto.toBase64(username .. ":" .. password)) then --user:pass
           conn:send("HTTP/1.0 401 Authorization Required\r\nWWW-Authenticate: Basic realm=\"ESP8266 Web Server\"\r\n\r\n<h1>Unauthorized Access</h1>");
           conn:close();
           return;
    end   


    -- extract the variables passed in the url
    local _GET = {}
    if (vars~=nil) then
      for k,v in string.gmatch(vars,"(%w+)=(%w+)&*") do
        _GET[k] = v
        --debug
        print(k .. ":" .. v)
      end

      if (_GET['led'] ~= nil) then
        position = _GET['led']      
      end    
    end

    -- turn led ON/OFF
    led_builtin(position)

    local content="<!DOCTYPE html>"
     .. "<html>"
     .. "<head>"
     .. "<link rel='icon' type='image/png' href='http://nodemcu.com/favicon.png' />"
     .. "</head>"
     .. "<body>"
     .. "<h1>LED BUILTIN is " .. position .. "</h1>"
     .. "<p>"
     .. "<a href='?led=on'><button>on</button></a>"
     .. "<a href='?led=off'><button>off</button></a>"
     .. "</p>"
     .. "</body>"
     .. "</html>"
   
    local contentLength=string.len(content)

    conn:on("sent", function(sck) sck:close() collectgarbage() end)
    conn:send("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length:" .. contentLength .. "\r\n\r\n" .. content)
end

function connection(conn)
    conn:on("receive", receive)
end

-- check whether server is already started
if srv~=nil then
    print("stop existing server...")
    srv:close()
    print("OK")
end

-- start a new server
print("start a new server ...")
srv=net.createServer(net.TCP, 1)
srv:listen(80, connection)
print("OK")

Sabtu, 12 November 2016

Tutorial NodeMCU: Push data dari WeMos D1 mini ke Firebase Realtime Database via REST (ESP8266, WeMos, NodeMCU, Lua)

Pada tulisan-tulisan sebelumnya, kita telah memanfaatkan board WeMos D1 mini untuk mengendalikan Led BUILTIN melalui interface Telnet maupun Web yang berfungsi di intranet. Pada tulisan akan dicoba untuk bagaimana WeMos difungsikan untuk melakukan push data ke Firebase Realtime Database yang merupakan salah satu layanan Google yang memungkinkan kita melakukan push data ke database realtime melalui berbagai interface seperti web, Android, IOS dan dalam hal ini kita akan menggunakan metode REST yang sesuai dengan tujuan pemanfaatan WeMos D1 mini sebagai perangkat IoT.

Untuk melakukan push data ke firebase realtime database melalui REST, maka firmware NodeMCU yang akan digunakan perlu di kustomisasi dengan menambahkan module HTTP dan SSL support, TLS 1.1 with 4 cipher suites.


 Gambar 1. Kustomisasi Module fireware NodeMCU

Langkah-langkah:
1. Aktifkan ke console Firebase
2. Buatlah project anda

 Gambar 2. Membuat Project Firebase

3. Klik pada Project Setting

Gambar 3. Mengatur Project setting

4. Kemudian pada klik pada Service Account

Gambar 4. Tab Service Accounts

5. Duplikasi Database Secrets anda yang akan digunakan untuk authentication ketika proses POST, PUT maupun GET data melalui REST.

Gambar 5. Duplikasi Database Secrets

6. Aktifkan ke Database dan duplikasi url akses database yang nantinya akan dilewatkan ke server untuk mengakses sumber daya database.

Gambar 6. Alamat akses Database

7. Lakukan koding berikut ini:

database = "https://nodemculua-677a8.firebaseio.com/"
node = "sensor1.json"
dbSecret = "xjTm2qhygBgVNqQauJC2n1JyQB4NCXbc********"
url = database .. node .. "?auth=" .. dbSecret
print(url)
payload = "{\"time\":\"2016-11-13\",\"temperature\":25,\"humidity\":45}"
print(payload)

http.request(url,
  "POST",
  "Content-Type: application/json\r\n", 
  payload,
  function(code, data)
    if (code ~= 200) then
      print("Failed : HTTP error code : " .. code)
    else
      print("Succeed : " .. code, data)
    end
  end)

Pada koding diatas, kita akan melakukan push data yang terdiri dari tiga atribut, yaitu time, temperatur, dan humidity ke node sensor1 yang terdapat pada database https://nodemculua-677a8.firebaseio.com/.
 8. Jika berhasil di Run akan muncuk response sebagai berikut:

dofile('app.lua')
https://nodemculua-677a8.firebaseio.com/sensor1.json?auth=xjTm2qhygBgVNqQauJC2n1JyQB4NCXbcC******
> Succeed : 200    {"name":"-KWQDQVFJHSIdmrVyyQ1"}
9. Jika mendapatkan pesan seperti ini:
https://nodemculua-677a8.firebaseio.com/sensor1.json?auth=xjTm2qhygBgVNqQauJC2n1JyQB4NCXbcC******
Failed : HTTP error code : 400{
  "error" : "Could not parse auth token."
}
Maka pastikan database secret adalah telah benar, untuk memastikan, coba duplikasi url diatas ke web browser, dan jika berhasil harusnya mendapat respons:

Gambar 7. URL benar dan Authentication sukses

Jika terdapat kegagalan pada token, maka akan muncul pesan

Gambar 3. Firebase server gagal melakukan authentication

dan jika hasil run dengan pesan Failed : HTTP error code : -1, walaupun sudah dipastikan koding, url datatabase maupun database secret adalah telah benar, maka perlu dipastikan bahwa firmware anda mendukung module tambahan SSL support, TLS 1.1 with 4 cipher suites.

Jika koding diatas adalah menggunakan perintah http.request, maka dapat juga menggunakan perintah http.post.

database = "https://nodemculua-677a8.firebaseio.com/"
node = "sensor1.json"
secret = "xjTm2qhygBgVNqQauJC2n1JyQB4NCXbcCQ******"
url = database .. node .. "?auth=" .. secret
payload = "{\"time\":\"2016-11-13\",\"temperature\":25,\"humidity\":45}"

print(url)

http.post(url,
  "Content-Type: application/json\r\n", 
  payload,
  function(code, data)
    if (code ~= 200) then
      print("Failed : HTTP error code : " .. code)
    else
      print("Succeed : " .. code, data)
    end
  end)

Petunjuk tambahan terkait dengan pemanfaatan REST pada firebird dapat dibaca di https://firebase.google.com/docs/reference/rest/database/

Tutorial NodeMCU: ESP8266 sebagai Access Point (WeMos, Lua)

Tulisan ini saya buat setelah membutuhkan fungsi koneksi langsung dari WIFI smartphone ke board WeMos, setelah melakukan surfing, ternyata wifi pada board dapat diset sebagai  wifi.STATION, wifi.SOFTAP, wifi.STATIONAP atau wifi.NULLMODE.

Koding berikut akan mengaktifkan wifi sebagai STATIONAP, disertai dengan setting SSID dan PASSWORD, dan mencetak IP Address.

cfg={}
cfg.ssid="myssid"
cfg.pwd="mypassword" -- minimal 8 character, jika tidak maka AP tidak berpassword
cfg.auth=wifi.WPA_WPA2_PSK
-- set mode as stationAP
wifi.setmode(wifi.STATIONAP)
wifi.ap.config(cfg)
print("AP IP Address:" .. wifi.ap.getip()) 
 Koding berikut ini akan mengaktifkan fungsi DHCP server pada AP.

dhcp_config ={}
dhcp_config.start = "192.168.4.100"
pool_ipstart, pool_ipend = wifi.ap.dhcp.config(dhcp_config)
if wifi.ap.dhcp.start() then
    print("DHCP server started, pool addr from " .. pool_ipstart .. " to " .. pool_ipend .. "!")
else
    print("DHCP server failed!")
end
Karena diaktifkan dengan modus STATIONAP, berarti wifi pada board berfungsi sebagai AP sekaligus sebagai STATION (client), sehingga dimungkinkan untuk koneksi ke AP lain sebagai client.

-- Credentials
SSID = "BOLT!Super4G-899A"
PASSWORD = "********"

print("Koneksi ke " .. SSID)
wifi.sta.config(SSID, PASSWORD
Berikut ini adalah koding lengkap untuk implementasi sebagai init.lua

apconfig.lua
cfg={}
cfg.ssid="myssid"
cfg.pwd="mypassword"
cfg.auth=wifi.WPA_WPA2_PSK

dhcp_config ={}
dhcp_config.start = "192.168.4.100"
 credentials.lua
-- Credentials
SSID = "BOLT!Super4G-899A"
PASSWORD = "*******"
init.lua

-- aplikasi ini berfungsi menghindarkan module anda terjebak
-- pada kondisi restart tak terhingga jika terjadi bugs pada
-- init.lua.

-- aplikasi dapat berjalan pada dua modus, yaitu standalone
-- atau terkoneksi ke wifi.

-- agar dapat terkoneksi ke wifi, maka username dan password
-- didefinisikan pada file credentials.lua

-- aplikasi akan menunggu selama 3 detik sebelum file app.lua
-- dieksekusi, jika terjadi bugs pada app.lua, maka anda
-- dapat menjalankan file.remove("app.lua") sehingga tidak
-- terjebak pada kondisi restart tak terhingga.

function startup()
    print("Running")
    -- aplikasi autoexec anda  sekarang adalah 'app.lua'
    if file.open("app.lua") == nil then
        print("app.lua tidak ditemukan, silakan buat app.lua sebagai auto executable.")
    else 
        file.close("app.lua")
        dofile("app.lua")
    end
end

if file.open("apconfig.lua") ~= nil then
    print("activate as access point")
    dofile("apconfig.lua")
    -- set mode as stationAP  
    wifi.setmode(wifi.STATIONAP)
    wifi.ap.config(cfg)
    print("AP IP Address:" .. wifi.ap.getip())   
    pool_ipstart, pool_ipend = wifi.ap.dhcp.config(dhcp_config)
    if wifi.ap.dhcp.start() then
        print("DHCP server started, pool addr from " .. pool_ipstart .. " to " .. pool_ipend .. "!")
    else
        print("DHCP server failed!")
    end
else
    -- default mode as station
    wifi.setmode(wifi.STATION)   
end

-- periksa keberadaan file credentials.lua yang mendefinisikan
-- 'SSID' and 'PASSWORD' wifi
if file.open("credentials.lua") == nil then
    -- bagian module standalone
    print("Boot sebagai module standalone")
    print("You have 3 seconds to abort")
    print("Waiting...")
    -- jalankan fungsi startup setelah 3 detik      
    tmr.alarm(0, 3000, 0, startup)
else   
    -- bagian module koneksi wifi
    file.close("credentials.lua")
    -- muat variable SSID dan PASSWORD
    dofile("credentials.lua")
    print("Koneksi ke " .. SSID)
    wifi.sta.config(SSID, PASSWORD)
    -- wifi.sta.connect() not necessary because config() uses auto-connect=true by default

    -- mencoba koneksi setiap detik sampai tmr.stop(1)
    trycount = 1
    tmr.alarm(1, 1000, 1, function()
        if wifi.sta.getip() == nil then
            print("trying " .. trycount .. " second(s)")
            trycount = trycount + 1
        else
            -- berhasil koneksi dan mendapatkan IP Address
            tmr.stop(1)
            print("WiFi connected")
            print("Client IP Address:" .. wifi.sta.getip())
            print("You have 3 seconds to abort")
            print("Waiting...")
            -- jalankan fungsi startup setelah 3 detik      
            tmr.alarm(0, 3000, 0, startup)
        end       
    end)
end

Jumat, 11 November 2016

Tutorial NodeMCU: Membuat Web Server (ESP8266, WeMos, Lua)

Pada tulisan ini saya mencoba melakukan eksplorasi lanjutan setelah sebelumnya membuat Telnet Server yang digunakan untuk mengemulasikan console NodeMCU Lua yang dilengkapi dengan perintah ls, sntp dan time. Pada bagian ini kita mencoba membuat Web Server mini yang nantinya dapat mengendalikan LED BUILTIN dengan menggunakan klik pada button ON atau OFF.

Secara umum pembuatan server web adalah tidak berbeda dengan telnet server, yang membedakan keduanya adalah nomor port yang digunakan, jika telnet server melakukan listen pada port 23, maka http server adalah melakukan listen pada port 80. Berikut ini adalah source code yang saya adaptasi dari berbagai sumber dari internet.

app.lua
position = "off"

function led_builtin(position)
    gpio.mode(4, gpio.OUTPUT)
    if (position == "on") then
        gpio.write(4, gpio.LOW) -- LED BUILTIN ON
    else
        gpio.write(4, gpio.HIGH) -- LED BUILTIN ON
    end
end

function receive(conn, payload)
    --debug
    print(payload)

    -- extra path and variables
    local _,_,method,path,vars = string.find(payload,"([A-Z]+) (.+)?(.+) HTTP")
    if(method==nil) then
      _, _, method, path = string.find(payload,"([A-Z]+) (.+) HTTP")
    end

    -- extract the variables passed in the url
    local _GET = {}
    if (vars~=nil) then
      for k,v in string.gmatch(vars,"(%w+)=(%w+)&*") do
        _GET[k] = v
        --debug
        print(k .. ":" .. v)
      end

      if (_GET['led'] ~= nil) then
        position = _GET['led']       
      end     
    end

    -- turn led ON/OFF
    led_builtin(position)

    local content="<!DOCTYPE html>"
     .. "<html>"
     .. "<head>"
     .. "<link rel='icon' type='image/png' href='http://nodemcu.com/favicon.png' />"
     .. "</head>"
     .. "<body>"
     .. "<h1>LED BUILTIN is " .. position .. "</h1>"
     .. "<p>"
     .. "<a href='?led=on'><button>on</button></a>"
     .. "<a href='?led=off'><button>off</button></a>"
     .. "</p>"
     .. "</body>"
     .. "</html>"
    
    local contentLength=string.len(content)

    conn:on("sent", function(sck) sck:close() collectgarbage() end)
    conn:send("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length:" .. contentLength .. "\r\n\r\n" .. content)
end

function connection(conn)
    conn:on("receive", receive)
end

-- check whether server is already started
if srv~=nil then
    print("stop existing server...")
    srv:close()
    print("OK")
end

-- start a new server
print("start a new server ...")
srv=net.createServer(net.TCP, 1)
srv:listen(80, connection)
print("OK")

 Gambar 1. Verbose Web Server pada Esplore

Gambar 2. Turn LED BUILTIN ON/OFF via Web

Kamis, 10 November 2016

Tutorial NodeMCU: Membuat telnet server (ESP8266, WeMos, Lua)

Tulisan ini saya buat setelah berpikir bagaimana dapat mengendalikan board WeMos melalui telnet via jaringan, berdasarkan hasil browsing kita dapat melakukannya melalui interface web dengan membuat HTTP Server, pendekatan lainnya adalah membuat telnet server.

Suatu telnet server adalah menerima koneksi melalui port 23. Software client yang dapat digunakan adalah Putty. Interfacing antara server dengan client adalah menggunakan event on receive yang akan menerima payload dari client. payload akan dikirim ke interpreter Lua untuk diproses dengan menggunakan perintah node.input(payload), hasil dari interpreter akan dikirim kembali ke klien dengan melakukan redirect output ke suatu fungsi yang sudah disiapkan. Untuk jelasnya dapat dilihat pada source code berikut ini:

credentials.lua
Dapat dilihat pada tulisan sebelumnya
init.lua
Dapat dilihat pada tulisan sebelumnya
app.lua
-- a simple telnet server
-- diadaptasi dari internet oleh Hendra Soewarno (19067305)


telnet_srv = net.createServer(net.TCP, 180)
telnet_srv:listen(23, function(socket)
    local fifo = {}
    local fifo_drained = true

    local function sender(c)
        if #fifo > 0 then
            c:send("\r" .. table.remove(fifo, 1))
        else
            fifo_drained = true
        end
    end

    local function s_output(str)
        table.insert(fifo, str)
        if socket ~= nil and fifo_drained then
            fifo_drained = false
            sender(socket)
        end
    end

    node.output(s_output, 0)   -- mengarahkan output ke function s_ouput.

    socket:on("receive", function(c, l)
        if string.byte(l) == 255 then
        elseif string.byte(l) == 26 then
            c:close()         -- ctrl-z to exit
        else
            node.input(l)  -- works like pcall(loadstring(l)) but support multiple separate line
        end    
    end)
    socket:on("disconnection", function(c)
        node.output(nil)  -- un-regist the redirect output function, output goes to serial
    end)
    socket:on("sent", sender)

    print("Welcome to NodeMCU world.\r\nPress Enter to continue or Ctrl-Z to close.")
end)

 Gambar 1. Memulai untuk telnet ke server

Gambar 2. Sesi telnet untuk menghidupkan Led Builtin pada board

Tekan Ctrl-Z untuk mengakhiri sesi telnet

Contoh telnet server yang dilengkapi dengan perintah ls, sntp, dan time.

-- a simple telnet server
-- diadaptasi dari internet oleh Hendra Soewarno (19067305)

telnet_srv = net.createServer(net.TCP, 180)
telnet_srv:listen(23, function(socket)
    local fifo = {}
    local fifo_drained = true

    local function sender(c)
        if #fifo > 0 then
            c:send("\r" .. table.remove(fifo, 1))
        else
            fifo_drained = true
        end
    end

    local function s_output(str)
        table.insert(fifo, str)
        if socket ~= nil and fifo_drained then
            fifo_drained = false
            sender(socket)
        end
    end

    node.output(s_output, 0)   -- mengarahkan output ke function s_ouput.

    local function ls()
        l = file.list();
        for k,v in pairs(l) do
            print(k.."("..v .. ")")
        end
    end

    local function time()
        utc = rtctime.epoch2cal(rtctime.get())
        print("UTC:" .. string.format("%04d/%02d/%02d %02d:%02d:%02d", utc["year"], utc["mon"], utc["day"], utc["hour"], utc["min"], utc["sec"]))
    end   

    local function _sntp()

        sntp.sync("1.id.pool.ntp.org",

        function(sec,usec,server)
            print("setting time from: " .. server)
            rtctime.set(sec, usec)
            time()
        end,

        function()
            print('failed!')
        end)
    end
   
    socket:on("receive", function(c, l)
        if string.byte(l) == 255 then
        elseif string.byte(l) == 26 then
            c:close()         -- ctrl-z to exit
        elseif string.sub(l,1,2) == "ls" then
            ls()
        elseif string.sub(l,1,4) == "time" then
            time()
        elseif string.sub(l,1,4) == "sntp" then
            _sntp()                       
        else
            node.input(l)  -- works like pcall(loadstring(l)) but support multiple separate line
        end   
    end)
    socket:on("disconnection", function(c)
        node.output(nil)  -- un-regist the redirect output function, output goes to serial
    end)
    socket:on("sent", sender)

    print("Welcome to NodeMCU world.\r\nPress Enter to continue or Ctrl-Z to close.")
end)

Rabu, 09 November 2016

Tutorial NodeMCU: Sync software RTC to google.com via Web (ESP8266, WeMos, Lua)


Tulisan ini saya buat karena menyadari bahwa lingkungan LAN kami tidak memperolehkan koneksi ke internet selain port 80 untuk keperluan browsing internet. Aplikasi akan melakukan sinkronisasi waktu ke situs web www.google.com dengan melakukan HTTP HEAD request yang biasanya digunakan untuk memeriksa apakah layanan url tersedia, atau file yang diinginkan tersedia (HTTP HEAD request adalah berbeda dengan HTTP GET request).Berikut ini adalah contoh HTTP HEAD request yang dapat dikirim sesaat setelah koneksi berhasil dilakukan.

HEAD / HTTP/1.1\r\n
Accept: */*\r\n
User-Agent: Mozilla/4.0 (compatible; ESP8266 NodeMcu Lua;)\r\n\r\n
Dimana \r\n adalah merepresentasikan CRLF, dan berikut ini adalah hasil response dari server.
Location: http://www.google.co.id/?gws_rd=cr&ei=6kYjWNOYFovvvgSzw424Ag
Cache-Control: private
Content-Type: text/html; charset=UTF-8
P3P: CP="This is not a P3P policy! See https://www.google.com/support/accounts/answer/151657?hl=en for more info."
Date: Wed, 09 Nov 2016 15:55:22 GMT
Server: gws
Content-Length: 261
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
Set-Cookie: NID=90=tP2nCmiU8y7o7oZt24v2mLxAE7WSyvUcu0hYL6kreKdKrlV5C98ut_KYTK7SljVgk7B0XdzRrwNVEXUIE4JHYI2WUh8ssyDJpebpLtNltY6F9OaF-ez5l-43PoZK_HY5; expires=Thu, 11-May-2017 15:55:22 GMT; path=/; domain=.google.com; HttpOnly
Diantara request tersebut terdapat baris Date yang nantinya akan kita parse untuk menggambil komponen tahun, bulan, tanggal, jam, menit dan detik.

Karena belum familiar dengan HTTP request dengan menggunakan Lua, maka saya memulai dengan mencoba source-code yang disediakan pada situs www.nodemcu.com dengan rangkaian koding sebagai berikut:
-- A simple http client
conn=net.createConnection(net.TCP, false) 
conn:on("receive", function(conn, pl) print(pl) end)
conn:connect(80,"www.google.com")
-- tidak ada jaminan koneksi telah berhasil
conn:send("GET / HTTP/1.1\r\nHost: www.nodemcu.com\r\n"
    .."Connection: keep-alive\r\nAccept: */*\r\n\r\n")
Yaitu conn:send langsung dibawah baris conn:connect, sehingga tidak ada suatu jaminan bahwa ketika HTTP GET request dilakukan, conn:connect telah berhasil membuka koneksi dengan server, sehingga response yang diharapkan tidak terjadi. Berdasarkan hasil surfing akhirnya saya melakukan perbaikan dengan koding sebagai berikut:
-- A simple http client
conn=net.createConnection(net.TCP, false)
conn:on("connection", function(conn, pl)
conn:send("GET / HTTP/1.1\r\nHost: www.nodemcu.com\r\n" .."Connection: keep-alive\r\nAccept: */*\r\n\r\n")end)
conn:on("receive", function(conn, pl) print(pl) end)
conn:connect(80,"www.google.com")
Dimana HTTP GET request dilakukan setelah terjadi event on connection, yang berarti bahwa koneksi ke server telah berhasil dilakukan, dan aplikasi client berhasil mendapatkan response dari server.

Berikut ini adalah koding lengkap untuk melakukan HTTP HEAD request, yang diserta dengan setting waktu rtctime.

credentials.lua
Dapat diperoleh pada tulisan sebelumnya
mylib.lua
Dapat diperoleh pada tulisan sebelumnya
init.lua
Dapat diperoleh pada tulisan sebelumnya
app.lua
--Date: Wed, 09 Nov 2016 15:57:58 GMT
--         1         2         3    
--12345678901234567890123456789012345
-- diadaptasikan kembali oleh Hendra Soewarno (0119067305)
require("mylib")

retried = 1
conn=net.createConnection(net.TCP, false)

function sendHeader(conn, pl)
    conn:send("HEAD / HTTP/1.1\r\n"
    .. "Accept: */*\r\n"
    .. "User-Agent: Mozilla/4.0 (compatible; ESP8266 NodeMcu Lua;)\r\n"
    .. "\r\n")
end

function setTime(conn, pl)
    --contain Date signature
    posisi = string.find(pl, "Date")
    if posisi > 0 then
        --cancel next alarm
        tmr.stop(0)
        year = 2000+tonumber(string.sub(pl,posisi+19,posisi+22))
        mon = string.sub(pl,posisi+14,posisi+16)
        month = math.floor(string.find("JanFebMarAprMayJunJulAugSepOctNovDec",mon)/3)+1
        day = tonumber(string.sub(pl,posisi+12,posisi+13))       
        hour = tonumber(string.sub(pl,posisi+23,posisi+24))
        minute = tonumber(string.sub(pl,posisi+26,posisi+27))
        second = tonumber(string.sub(pl,posisi+29,posisi+30))
        mikrosecond = 0
        --datetime to unix epoch time
        unixtime = mylib.cal2epoch(year, month, day, hour, minute, second)
        -- update software RTC
        rtctime.set(unixtime, mikrosecond)
        utc = rtctime.epoch2cal(rtctime.get())
        print("Waktu adalah UTC:" .. string.format("%04d/%02d/%02d %02d:%02d:%02d", utc["year"], utc["mon"], utc["day"], utc["hour"], utc["min"], utc["sec"]))
        -- WIB UTC + 7
        wib = rtctime.epoch2cal(rtctime.get()+7*60*60) -- tambah 7
        print("Waktu adalah WIB:" .. string.format("%04d/%02d/%02d %02d:%02d:%02d", wib["year"], wib["mon"], wib["day"], wib["hour"], wib["min"], wib["sec"]))          
    end
end

function syncTime()
    print("trying www.google.com " .. retried)
    conn:on("connection", sendHeader)
    conn:connect(80,"www.google.com")
    retried = retried + 1   
    -- close connection if not got response in minute
    tmr.alarm(1,1000,0, function() conn:close() end)   
end  

syncTime()
--no more frequently than once every 4 seconds
tmr.alarm(0,5000,1, syncTime)

Gambar 1. Hasil Run Aplikasi

Selasa, 08 November 2016

Tutorial NodeMCU: Sync software RTC to NIST time server using Day Time Protocol (ESP8266, WeMos, Lua)

Untuk menjalankan contoh pada tulisan ini, maka firmware NodeMCU harus tersedia module RTC time, jika tidak maka berpotensi memicu PANIC pada saat runtime.

Tulisan ini saya buat setelah membaca bahwa NIST menyediakan jam atom yang paling akurat didunia,  pemakaian time server NIST yang dapat diakses melalui alamat time.nist.gov dengan menggunakan Day Time Protocol (RFC-867) pada port 13, maupun melalui Network Time Protocol (RFC-1305) pada port 113.

Pada tulisan ini saya mencoba men-sinkron software RTC pada WeMos D1 mini dengan time.nist.gov melalui Day Time Protocol.

Response dari NIST time server adalah berupa suatu string sepanjang 49 karakter yang diawali dengan suatu karakter line feed (LF) dengan gambaran format sebagai berikut

--x57700 16-11-08 12:33:48 00 0 0 454.5 UTC(NIST) *
--xJJJJJ YR-MO-DA HH:MM:SS TT L H msADV UTC(NIST) *
--         1         2         3         4        
--1234567890123456789012345678901234567890123456789
--first characater x is LF

Karena response adalah berupa string sehingga kita harus memisahkan masing-masing bagian menjadi tahun (YR), bulan (MO), hari (DA), jam(HH), menit(MM), detik(SS), dan milidetik (msADV) dengan menggunakan fungsi bantu string.sub(). Sebelum melakukan pemisahan, maka adalah penting untuk memastikan respon memiliki signature NIST.

Sesuatu hal yang menjadi masalah untuk melakukan setting waktu pada software RTC pada NodeMCU LUA adalah perintah rtctimer.set(unix epoch time, mikrodetik) menggunakan unix epoch time yang merupakan jumlah detik sejak 1 Januari 1970.

Setelah melakukan pencarian diinternet, ternyata NodeMCU Lua tidak menyediakan fungsi untuk konversi datetime menjadi unix epoch time, sehingga dalam hal ini kita perlu membuat library sendiri. Hal pertama yang perlu menjadi perhatian adalah adanya perbedaan jumlah hari pada tahun kabisat dengan tahun biasa khususnya pada bulan February.

Berikut ini adalah library yang saya siapkan untuk fungsi konversi cal2epoch yang beberapa bagian saya adaptasi dari rtctime.c yang ada di source code NodeMCU dengan rincian koding berikut ini.

mylib.lua

-- berberapa bagian saya adaptasi dari rtctime.c
-- dikoding oleh: hendra soewarno (0119067305)

local moduleName = ...
local M = {}
_G[moduleName] = M

--day per month/jumlah hari perbulan?
local dpm = {
    0,
    (31),
    (31+28),
    (31+28+31),
    (31+28+31+30),
    (31+28+31+30+31),
    (31+28+31+30+31+30),
    (31+28+31+30+31+30+31),
    (31+28+31+30+31+30+31+31),
    (31+28+31+30+31+30+31+31+30),
    (31+28+31+30+31+30+31+31+30+31),
    (31+28+31+30+31+30+31+31+30+31+30),
    (31+28+31+30+31+30+31+31+30+31+30+31)
}

function mod(a, b)
    return a - (math.floor(a/b))*b
end

function isleap (year)
    -- every fourth year is a leap year except for century years that are
    -- not divisible by 400.
  return (mod(year,4)==0 and (mod(year,100)~=0 or mod(year,400)==0))
end

function M.cal2epoch(yyyy, mo, dd, hh, mm, ss)
    days = 0
    for year = 1970, yyyy-1 do
        if isleap(year) then
            days = days + 366
        else
            days = days + 365
        end
    end
    days = days + dpm[mo]
    if isleap(yyyy) and mo > 2 then days = days + 1 end
    days = days + dd - 1

    seconds = days*24*60*60 + hh*60*60+  mm*60 + ss

    return seconds
end

return M
Adapun contoh unit test untuk module mylib.lua:

require("mylib")
--unit test
print(mylib.cal2epoch(2016,11,8,21,15,25)==1478639725)
Yang harus menghasil nilai true.

Selanjutnya adalah menyiapkan koding untuk koneksi wifi, dan sikronisasi waktu.

mylib.lua
Silakan mengacu pada koding diatas
credentials.lua
 Silakan mengacu pada koding tulisan saya sebelumnya
init.lua
Silakan mengacu pada koding tulisan saya sebelumnya

app.lua

--x57700 16-11-08 12:33:48 00 0 0 454.5 UTC(NIST) *
--xJJJJJ YR-MO-DA HH:MM:SS TT L H msADV UTC(NIST) *
--         1         2         3         4        
--1234567890123456789012345678901234567890123456789
--first characater x is LF
require("mylib")

retried = 1
conn=net.createConnection(net.TCP, false)

function setTime(conn, pl)
    -- contain NIS signature
    if string.find(pl, "NIST") > 0 then
        tmr.stop(0)
        print(pl)
        year = 2000+tonumber(string.sub(pl,8,9))
        month = tonumber(string.sub(pl,11,12))
        day = tonumber(string.sub(pl,14,15))
        hour = tonumber(string.sub(pl,17,18))
        minute = tonumber(string.sub(pl,20,21))
        second = tonumber(string.sub(pl,23,24))
        mikrosecond = tonumber(string.sub(pl,33,37))*1000
        -- datetime to unix epoch time
        unixtime = mylib.cal2epoch(year, month, day, hour, minute, second)
        -- update software RTC
        rtctime.set(unixtime, mikrosecond)
        utc = rtctime.epoch2cal(rtctime.get())
        print("Waktu adalah UTC:" .. string.format("%04d/%02d/%02d %02d:%02d:%02d", utc["year"], utc["mon"], utc["day"], utc["hour"], utc["min"], utc["sec"]))
        -- WIB UTC + 7
        wib = rtctime.epoch2cal(rtctime.get()+7*60*60) -- tambah 7
        print("Waktu adalah WIB:" .. string.format("%04d/%02d/%02d %02d:%02d:%02d", wib["year"], wib["mon"], wib["day"], wib["hour"], wib["min"], wib["sec"]))          
    end
end

function syncTime()
    print("trying time.nist.gov " .. retried)
    conn:on("receive", setTime)
    conn:connect(13,"time.nist.gov")
    retried = retried + 1
    -- close connection if not got response in minute
    tmr.alarm(1,1000,0, function() conn:close() end)
end  

syncTime()
--no more frequently than once every 4 seconds
tmr.alarm(0,5000,1, syncTime)


Catatan:
  • NIST time server menyarankan agar tidak melakukan lebih dari satu koneksi dalam jangka waktu 4 detik, jika terdeteksi maka koneksi anda berpotensi untuk diblokir.
  • Pada algoritma diatas saya menggunakan strategi jika tidak ada respon dalam 1 detik, maka koneksi otomatis ditutup.
  • Algoritma diatas akan terus mencoba sinkronisasi waktu per 4 detik sampai berhasil dengan menggunakan fungsi tmr.alarm.
 Gambar 1. Hasil sikronisasi RTC software ke NIST time server