-->
Page 1 of 1

Voltage monitoring web server using NodeMCU and ZMPT101B

PostPosted: Wed May 04, 2016 6:38 am
by AlexC
Hi, here's my first project using LUA on the NodeMCU.

I needed to monitor A.C. voltage fluctuations, log the data on the ESP and make the current voltage available on the network.

At first I got the data acquisition working with the Arduino IDE, but it wasn't very practical having to recompile everything each time I made a small change to code which required a lot of trial and error, so I decided to start over using the NodeMCU firmware as it was a lot easier to just send commands to the LUA interpreter to see if my code would work.

After gathering a bit of code from the firmware examples on Git, I mostly figured out the LUA syntax and put together this code which provides the features I wanted most:

    - Connect to the local network
    - Set the clock via NTP
    - Read the min/max bounds of the A.C. voltage from the ZMPT module to derive the actual voltage
    - Turn on the LED while retrieving and calculating the current voltage
    - Use the Flash button to toggle displaying the current voltage on serial every 2 sec.
    - Log the current voltage with time stamp to flash every 5 minutes
    - Automatically write to a different log file each month
    - Listen for incoming HTTP GET requests and return the current voltage
    - Accept telnet connections to issue simple commands

It wasn't easy figuring out how to write code with timers behaving like separate threads, but since I read that we can't hog the CPU for more than 15ms or risk a crash, and needed at least 17ms to analyze a full 60Hz cycle, once I got the hang of it, the timers turned out to be very handy.

Here's the init.lua script:
Code: Select all-- init.lua for Volts Server

wifi.setmode(wifi.STATION)
-- CHANGE AS NEEDED --
wifi.sta.setmac("5C:CF:7F:00:82:66") -- Espressif is 5C:CF:7F, unit 00, 8266. Easier to regognize that way
wifi.sta.setip({ip="192.168.1.8", netmask="255.255.255.0", gateway="192.168.1.1"})
wifi.sta.config("SSID","PASSWORD",1)
net.dns.setdnsserver("192.168.1.1",0)
-- (only wifi.sta.config is required, other settings are optional)

tmr.alarm(0, 1000, 1, function() -- main loop for connecting to AP
 local wfs = wifi.sta.status() -- get wifi status
 if wfs < 5 then -- less than 5 means it's not connected yet
  if wfs == 1 then
   print("Connecting to AP...") -- 1 is okay, just takes a few seconds
  else
   print("Connection status:",wfs) -- 0, 2, 3, 4 = idle, wrong pwd, ap not found, failure
  end -- if wfs < 5
 else
  print("Connected")
  print("IP:",wifi.sta.getip())
  net.dns.resolve("pool.ntp.org", function(sk, ntpip) -- get IP of nearest NTP server
   if (ntpip == nil) then
    print("Failed to get ntp server IP!")
   else
    print("Sync ntp",tmr.now()) -- start time
    sntp.sync(ntpip)
    print("Ntp done",tmr.now()) -- end time, tells how long it took to sync (mainly for debugging)
    tmr.alarm(6, 200, 0, function() -- 200ms delay
     local ue,um = rtctime.get()
     print(string.format("Got time %s from ntp server %s",ue,ntpip))
    end) -- tmr.alarm 6
   end -- if ntpip == nil
  end) -- dns resolve
  if (wifi.sta.sethostname("ESP8266LUA")) then -- optional
   print("Hostname set to ESP8266LUA")
  else
   print("Hostname not set")
  end -- wifi.sta.sethostname
  tmr.alarm(6, 1000, 0, function()
   dofile("voltsserver.lua") -- launch main program
  end)
  tmr.stop(0) -- connected to AP, stop timer/loop
 end -- if/else (wfs==5)
end) -- tmr.alarm 0


voltsserver.lua script:
Code: Select all-- voltsserver.lua

print("Starting VoltsServer.lua...")

function startup() -- all the settings are here. Adjust the TCP port and voltage bias if needed
 port=888 bias=0.076 timerb=1 timerv=2 timerm=3 timerl=4 timers=5 sw=0 ledpin=0 on=0 off=1
 gpio.mode(ledpin, gpio.OUTPUT)
 tmr.register(timerb,   2000, tmr.ALARM_AUTO, getbutton)
 tmr.register(timerm,      1, tmr.ALARM_AUTO, getminmax)
 tmr.register(timerl, 300000, tmr.ALARM_AUTO, logvolts)
 tmr.start(timerl) print("Logger started")
 tmr.start(timerb) print("Switch started")
 led(off)
end

function led(state) gpio.write(ledpin,state) end -- turn led on or off

function getbutton() -- use flash button to toggle serial monitoring of voltage on or off
 if gpio.read(3) == 0 then  sw = (1 - sw) print("Switch "..sw) end
 if sw == 1 then getvolts("button") end
end

function getminmax() -- read min/max voltages from ZMPT101B
 v = adc.read(0) if v < min then min = v elseif v > max then max = v end
 i = i + 1 if i == 80 then tmr.stop(timerm) end -- stop after 80 samples
end

function getvolts(target) -- get data and format string for specified target
 i=0 min=2000 max=0 v=0 limit=0
 vstring=" 00.00 No data\n" lstring="-\t-\t-\t-\t-\n" mstring=vstring
 led(on) -- turn led on to show activity
 tmr.start(timerm) -- get min/max
 tmr.alarm(timerv, 100, 1, function() -- wait 100ms to let getminmax() finish first
  local trun,tmode = tmr.state(timerm) -- get timerm state to see if it finished running
  if (trun == false) then -- it's not running anymore so we can use the min/max values
   if (target == "logger") then -- log to flash ram
    lstring = string.format("%d\t%0.3f\t%0.3f\t%03d\t%0.3f\n",rtctime.get(),min/1000,max/1000,max-min,(max-min)*bias)
    -- generate monthly log filename by counting pseudo-months since 1-1-1970
    -- 365 days = 8760h * 60 * 60 / 12 = 2628000 sec -- close enough
    logfile = string.format("log_%d.txt",rtctime.get()/2628000) -- set monthly log filename
    if file.open(logfile,"a+") then file.write(lstring) file.close() end -- append to log
   elseif (target == "server") then -- format string for http client
    vstring = string.format(" %05.2f Volts A.C.\n",(max-min)*bias)
    -- I wish I could use c:send() c:close() here but LUA complains :-/
   else -- default target
    mstring = string.format("Min: %0.3f Max: %0.3f D: %03d = %0.3fV",min/1000,max/1000,max-min,(max-min)*bias)
    print(mstring) -- output on serial (or telnet console)
   end -- if target
   led(off)
   tmr.stop(timerv) -- done checking if timerm is running
  else -- if trun - timerm is still running, keep waiting/checking
   limit = limit + 1 -- just in case, keep track of how many times it loops
   if limit == 10 then limit = 0 -- it looped 10 times (1s) something went wrong, abort
    led(off)
    print("timeout") -- not very informative but most likely nobody is reading anyway
    tmr.stop(timerm) -- just in case, stop timerm as it shouldn't be running this long
    tmr.stop(timerv) -- done checking if timerm is running
   end -- if limit == 10
  end -- if trun
 end) -- timerv
end

function logvolts() getvolts("logger") end -- log to flash ram

-- This is where it really starts. LUA doesn't seem to scan the whole script before
-- running it, so it needs all the functions listed first before referencing them.
startup()

if not srv then -- prevents error in case the script is re-launched manually
 srv=net.createServer(net.TCP,120) -- start server
  srv:listen(port,function(c) -- listen for incoming connections (c)
  c:on("receive",function(c,d) -- function to execute when receiving first data
   if d:sub(1,6) == "telnet" or d:byte(1) == 10 or d:byte(1) == 13 then -- detect telnet input
    node.output(function(s) if c ~= nil then c:send(s) end end,0) -- send LUA output to telnet
    c:on("receive",function(c,d) -- function to execute when receiving subsequent data
     if d:byte(1) == 27 or d:sub(1,5) == "exit" then  -- user sent Esc (27) or "exit"
      c:close() -- close connection
     else
      node.input(d) -- send telnet input to LUA interpreter
     end -- if d:byte
    end) -- c:on receive (subsequent)
    c:on("disconnection",function(c) node.output(nil) end) -- stop routing output to telnet
    print("NodeMCU Volt Server. (Type exit or send [Esc] or [Ctrl]+[Esc] to exit)\r\n\r\n> ")
    node.input("\r\n") -- send CR/LF to LUA interpreter
    return -- let c:on callbacks handle it automatically from here
   end -- if telnet
   -- so the connection wasn't telnet, carry on.
   if d:sub(1,5) ~= "GET /" then c:close() return end -- ignore unexpected requests
   getvolts("server") -- get the voltage and vstring formatted
   tmr.alarm(timers, 200, 0, function() -- 200ms delay for timerm and timerv
    c:send(vstring) -- could check timerv but the default vstring will do
    c:close() -- close connection
   end) -- timers
  end) -- c:on receive (first)
 end) -- srv:listen
end -- if not srv

print("Server started")
local ip,bc,gw=wifi.sta.getip()
print("To start/stop printing voltage to serial: Press FLASH button")
print("To retrieve current voltage: Send HTTP GET request to http://"..ip..":"..port.."/ ")
print("To access the LUA prompt: Telnet to IP "..ip.." port "..port.." and press [Enter]")

zipped init.lua and voltsserver.lua
(3.4 KiB) Downloaded 704 times


and a picture of the hardware:
NodeMCU_Volts.jpg
Picture of NodeMCU with ZMPT101B voltage sensor

Although the voltage sensor module is supposed to be used to monitor the voltage between the main and neutral wire, I use it to measure transient voltages between the ground/earth and neutral wires of my wall outlet.
With a voltage bias value of 0.076, for measuring voltages between 1V and 20V I'm getting sufficiently accurate readings, but the bias may need adjusting for monitoring 120V or 240V.

Re: Voltage monitoring web server using NodeMCU and ZMPT101B

PostPosted: Thu May 05, 2016 7:56 am
by saugatchandra
Hi,
I have just found out about NodeMCU, I am very inspired by your work and want to monitor both AC voltage and current i.e. power. Any inputs from you would be very much appreciated.

Thanks,
Saugat.

Re: Voltage monitoring web server using NodeMCU and ZMPT101B

PostPosted: Fri May 06, 2016 6:40 am
by AlexC
Hi Saugat,
saugatchandra wrote:Hi,
I have just found out about NodeMCU, I am very inspired by your work and want to monitor both AC voltage and current i.e. power. Any inputs from you would be very much appreciated.

While I was still trying to use the Arduino IDE I found the Emon library which provides a voltage and current monitoring example, but it's for the Arduino and would probably require some effort to port to LUA. In any case, there is a lot of useful information on their openenergymonitor website.

For the NodeMCU with LUA your best bet may be to search on Google for NodeMCU AC current voltage as that returns many good examples and tutorials that may already do exactly what you need.

I didn't find many options for >50V AC sensors, I just wanted something with a rectified analog DC output, compatible with the 5V of the Arduino and the 3.3v of the ESP. The module I got was the only cheap one I could find. LC Technology seems to be the only company making one and it didn't have any datasheet nor documentation so I took a chance.
I thought it would provide a stable DC output from e.g. 0 to 5VDC representing 0 to 250VAC, but the output is a DC waveform oscillating in sync with the AC Hz, so it requires taking many samples to find the actual amplitude of the waveform. It took me a while to find a formula which gives values coming close to the actual AC voltage.

For measuring the current you'll find many options, both for AC and DC. I don't have any experience with these modules though.