Voltage monitoring web server using NodeMCU and ZMPT101B
Posted: Wed May 04, 2016 6:38 am
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:
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:
voltsserver.lua script:
and a picture of the hardware:
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.
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]")
and a picture of the hardware:
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.