HUZZAH ESP8266 PIR Webserver |
Objective
To build the following using the Adafruit HUZZAH ESP8266 (using NodeMCU):
- WiFi PIR Motion Sensor with a configurable website interface (e.g. for setting up a php socket or webpage to record sensor data).
- Webpage of server that constantly polls the status of the PIR Motion Sensor and update the website in (roughly) real time.
- Have the motion sensor ESP8266 webserver contact a php server which then records the data to a file when motion occurs/ends, configurable via a webpage on the HUZZAH ESP8266 server.
- Use an OLED with the HUZZAH ESP8266 (for debugging purposes).
Overview of the Huzzah ESP8266 PIR Webserver Design
The Adafruit HUZZAH ESP8266 can run from batteries if they provide sufficient current. Note: The device itself is fairly power hungry consuming up to half an amp so beware, the batteries I use below aren't up to scratch really, more power is necessary here ideally (e.g. cell phone USB power supply works well as it can deliver more current). Nevertheless the battery setup does work (albeit it slightly flakey at times).
Battery powered HUZZAH ESP8266 Circuit -courtesy of Fritzing and Adafruit's Fritzing component library. |
I found that a decoupling capacitor (10 uF) was necessary as my circuit was fairly noisy. The capacitor is connected close pin #4 on the HUZZAH (= gpio 2) and ground (short distances are important here in order to avoid inductive components/noise creeping in!).
NOTE: Everything here is running at 3.3 volts (i.e. not 5volts). The input voltage is 6 volts (the HUZZAH's regulator takes this down to 3.3 volts).
Software Operation
If motion is detected by the PIR, the OLED displays the following message. Pin #4 (gpio 2) is setup as an interrupt - see below code.
HUZZAH ESP8266 OLED - Motion Sensor Output |
And the corresponding webpage (at 192.168.1.124 in my case) says:
If no motion is detected the HUZZAH OLED displays this message:
HUZZAH ESP8266 OLED - Motion Sensor Output |
And the webpage says:
If
you go to the server's home webpage (at 192.168.1.124 in my case - run the Lua command print(wifi.sta.getip()) to find out the IP Address or look at your router's website or run say Nmap in Linux or Advanced IP Scanner in windows).
Clicking the “on” radio button results in the Huzzah's on-board
red LED lighting up and also a message on the OLED:
Clicking the “off” radio button turns of the Huzzah
on-board red LED and the OLED displays this message:
The Settings webpage is used to specify the webpage on my Linux server (a Virtual Box) which runs php. This then logs the data (for test purposes). Click the below "Settings" hyperlink to see the Settings page...
The above only works on my LAN network, however if you want to make this work via the internet you would merely have to configure your router to forward the LAN address 192.168.1.124 (in my case) to a port (e.g. say 8080) and then you would be able to access the ESP8266 server from anywhere in the world (assuming you know the public IP Address of your router).
I use the excellent Russian software ESPlorer (http://esp8266.ru/esplorer/) for everything here including uploading the above files by the way.
/ESP8266_Test/php_Server.php?PIR_Value=
On your linux machine (in my case I’m using a Virtual Box) you can use the Linux "watch" command to see things every say 2 seconds. The php code time stamps the motion sensor (PIR) events:
Now when you activate the PIR motion sensor, the log file on the Linux machine will update as the HUZZAH hits the php webpage (and you will see the update every 2 seconds via "watch"). Note that the right most "1" digit below mean logic 1 (i.e. PIR activated), 0 is logic "0" i.e. not activated. The time difference is specified on the actual PIR device via the potentiometers, in the case below the time difference is 7 seconds. That is to the say, the PIR will go high (logic 1) for about 7 seconds and then go low logic 0 if no longer being activated (no motion occurring).
=>
PIR Running from 3.3 volts (not 5 volts)
You can run a 5 volt PIR sensor from 3.3 volts by doing the following (in my case):
5volt PIR Motion Sensor using 3.3volt power source |
i.e. don’t
connect anything to the +5volt (vcc) pin, and just use the other 3 pins
connecting 3.3 volts as above.
Connecting the PIR up the conventional way:
More Stability & A Less Erratic PIR
A more stable solution to using the above circuit would be to use a 5 volt power supply (e.g. a breadboard 5 volt power supply easily found on ebay) and then connect the PIR directly to the output of this as the below illustrates. This would mean that the PIR is much more stable and less erratic than the above circuit. Note that the HUZZAH is powered via the VBat pin (which supplies the HUZZAH ESP8266 circuitry with the required 3.3 volts. Also the OLED runs from 3.3 volts).Much more stable PIR behavior |
Connecting the PIR up the conventional way:
Files/Code
There are 5 separate files here to upload to the HUZZAH ESP8266:- LED_Server_OLED_Ajax.lua
- init.lua
- LED_HTML_Ajax.html
- Settings.html
- ConfigureWebpage.txt
I use the excellent Russian software ESPlorer (http://esp8266.ru/esplorer/) for everything here including uploading the above files by the way.
Lua code and compiling it
I compile the lua code via ESPlorer (http://esp8266.ru/esplorer/).
In order to compile the lua code, you have to upload the normal “.lua” file and then clicking the
Save & Compile button which consequently produces a “.lc” file. Note that the below init.lua code runs the more efficient "L ED_Server_OLED_Ajax.lc" file and NOT the more memory hungry "LED_Server_OLED_Ajax.lua" file.
init.lua
function startup()
print('Huzzah ESP8266 Started') --dofile('LED_Server_OLED_Ajax.lua') dofile('LED_Server_OLED_Ajax.lc') end print("\nInitially delaying for 7 seconds in case there is a bug somewhere in the main code ") print("thus giving me a 7 sec window to fix things etc") tmr.alarm(0,7000,0,startup)
ESP8266 PIR Server Code
--(LED_Server_OLED_Ajax.lua -> compiles to LED_Server_OLED_Ajax.lc)local pin = 3 --> pin 3 on my board (which is GPIO0) i.r. the onboard Red LED. local value = gpio.LOW function init_OLED(sda,scl) --Set up the u8glib lib sla = 0x3c i2c.setup(0, sda, scl, i2c.SLOW) disp = u8g.ssd1306_128x64_i2c(sla) disp:setFont(u8g.font_6x10) disp:setFontRefHeightExtendedText() disp:setDefaultForegroundColor() disp:setFontPosTop() end init_OLED(6,7) --Run setting up function Output_To_OLED(ledstatus) disp:firstPage() repeat disp:drawStr(0,0,"LED WEBSERVER") -- Starting on line 0 --disp:drawStr(0,11*2,"STATUS: "..ledstatus) disp:drawStr(0,11*2,ledstatus) until disp:nextPage() == false end --The following code allows me to load/send bigger files (html files in my case) --https://github.com/nodemcu/nodemcu-firmware/issues/211 function Sendfile(conn, filename) if file.open(filename, "r") then repeat local line=file.read(128) if line then conn:send(line)end until not line file.close() end end function serve_html(conn) -- file.open("LED_HTML_Ajax.html") -- conn:send(file.read()) -- send data back to client -- file.close() Sendfile(conn,"LED_HTML_Ajax.html") -- send data back to client. end function serve_html_Settings(conn) -- file.open("LED_HTML_Ajax.html") -- conn:send(file.read()) -- send data back to client -- file.close() --Sendfile(conn,"LED_HTML_Ajax.html") -- send data back to client. Sendfile(conn,"Settings.html") -- send data back to client. end function set_led(value) gpio.write(pin, value) end function HitWebpage(URL,PathOfWebpage) sk=net.createConnection(net.TCP, 0) sk:on("receive", function(sck, c) end ) -- Don't print the response from the webpage --URL="192.168.1.100" --PathOfWebpage="/ESP8266_Test/php_Server.php?PIR_Value=1" sk:connect(80,URL) sk:send("GET "..PathOfWebpage.." HTTP/1.1\r\nHost: "..URL.."\r\nConnection: keep-alive\r\nAccept: */*\r\n\r\n") print("Hitting my php VirtualBox Server") end ------------------------------FILE I/O FUNCS------------------------ function SplitStringBetweenTwoChars(TheString,StartChar,EndChar) local TheResult="" string.gsub(TheString..".",StartChar.."(.-)"..EndChar, function(a) TheResult=a end) --return string between "|" and "|" return TheResult end function CommaSeparatedValuesToArray(StringOfCSV) --CSV to array local TempArray = {} --array of values from file for word in string.gmatch(StringOfCSV, '([^,]+)') do --print(word) table.insert (TempArray, word); end return TempArray end -- end of func function PrintArrayValues(NameOfArray) for arrayCount = 1, #NameOfArray do print (NameOfArray[arrayCount]) end end --end of func function ConvertFileToArray(NameOfTextFile) local TempArrayOfValuesFromFile = {} --array of values from file file.open(NameOfTextFile, 'r+') while true do local line = file.readline() if (line == nil) then break end --HitCountFromFile=line -- print(line) table.insert (TempArrayOfValuesFromFile, line); end file.close() return TempArrayOfValuesFromFile end function SaveArrayToFile(NameOfArray,NameOfTextFile) file.open(NameOfTextFile, 'w') for arrayCount = 1, #NameOfArray do print (NameOfArray[arrayCount]) file.write(NameOfArray[arrayCount].."\n") end file.close() end function PrintArray(NameOfArray) for arrayCount = 1, #NameOfArray do print (NameOfArray[arrayCount]) end end function PrintOutFileContents(NameOfTextFile) file.open(NameOfTextFile, "r") while true do local line = file.readline() if (line == nil) then break end print(line) end file.close() end ------------------------------------------------------------------- PIR_Status=gpio.read(4) --Get value of PIR at initial boot up of the chip function MotionDetected() PIR_Status=gpio.read(4) print("Motion Detected!! => PIR_Status:"..PIR_Status) PIR_string="No Data" if PIR_Status==0 then PIR_string="Not Activated" else PIR_string="Activated" end Output_To_OLED("PIR : "..PIR_string) function trim1(s) return (s:gsub("^%s*(.-)%s*$", "%1")) end print("** Read the settings file: **") ArrayOfValuesFromFile=ConvertFileToArray("ConfigureWebpage.txt") --put textfile lines into Global array called ArrayOfValuesFromFile print("URL = "..ArrayOfValuesFromFile[1].. "Path To Webpage = "..ArrayOfValuesFromFile[2]) print("here:"..trim1(ArrayOfValuesFromFile[2])..PIR_Status) HitWebpage(trim1(ArrayOfValuesFromFile[1]),trim1(ArrayOfValuesFromFile[2])..PIR_Status) return PIR_Status end --Check for an ip every 1 second. --if no router found then just keep looping printing out a message DotsCounter=0; tmr.alarm(3,1000, 1, function() if wifi.sta.getip()==nil then print("Trying to connect to Access Point (i.e. my router)...") DotsCounter=DotsCounter+1 if DotsCounter==0 then Output_To_OLED("Can't find router") end if DotsCounter==1 then Output_To_OLED("Can't find router.") end if DotsCounter==2 then Output_To_OLED("Can't find router..") end if DotsCounter==3 then Output_To_OLED("Can't find router...") end if DotsCounter==4 then DotsCounter=-1 end else ip, netmask, gateway = wifi.sta.getip() --netmask=null --gateway=null Output_To_OLED("Server: "..ip) tmr.stop(3) -- stop timer 3 and break out of here end end) --loop for ever unless you connect to the router AP gpio.mode(4,gpio.INT) -- attach interrupt to PIR trigger pin 2 (= gpio 4) on my board -- Attach interrupt to PIR trigger pin. -- both means this triggers when the PIR is activated (Logic 1) -- and then deavtivated (logic 0) gpio.trig(4,"both",MotionDetected) gpio.mode(pin, gpio.OUTPUT) srv=net.createServer(net.TCP) srv:listen(80,function(conn) conn:on("receive",function(conn,DataReceived) print("Data received from client:\n") print(DataReceived) if string.find(DataReceived, "/on") then set_led(gpio.LOW) Output_To_OLED("Onboard LED: On") -- if you look at FireBug, you can see the response (i.e. this) from the server -- however the javascript code in html files doesn't use this info but it does need a text response -- of some kind or it just waits and waits forever conn:send("The Server Says: You have to send some response for On here or the javascript waits for ever and ever for it") elseif string.find(DataReceived, "/off") then set_led(gpio.HIGH) Output_To_OLED("Onboard LED: Off") conn:send("The Server Says: You have to send some response for Off here or the javascript waits for ever and ever for it") elseif string.find(DataReceived, "/PIR_Status") then print("in the PIR code ( v19). PIR_Status="..PIR_Status) print('Heap Size: ',node.heap(),'\n') conn:send("PIR Value = "..PIR_Status) elseif string.find(DataReceived, "GET /Settings") then Output_To_OLED("Setting Selected") serve_html_Settings(conn) elseif string.find(DataReceived, "/NewSettingsMadeByClient") then --GET /NewSettingsMadeByClient|a,b,c| HTTP/1.1 SettingsBackFromClientAsCSV=SplitStringBetweenTwoChars(DataReceived,"|","|") print("SettingsBackFromClientAsCSV="..SettingsBackFromClientAsCSV) UserSettingsAsArray=CommaSeparatedValuesToArray(SettingsBackFromClientAsCSV) print("** Write over the original file with the new array values: **") SaveArrayToFile(UserSettingsAsArray,"ConfigureWebpage.txt") print("** Read back the file: **") PrintOutFileContents("ConfigureWebpage.txt") PrintArrayValues(UserSettingsAsArray) UserSettingsAsArray=nil --delete the value SettingsBackFromClientAsCSV=nil --delete the value arrayCount=nil --delete the value conn:send("Server wrote to settings file") --you have to send something back to the client or the javascript hangs elseif string.find(DataReceived, "/RequestSettingValues") then ArrayOfValuesFromFile=ConvertFileToArray("ConfigureWebpage.txt") --put textfile lines into Global array called ArrayOfValuesFromFile conn:send(ArrayOfValuesFromFile[1]..","..ArrayOfValuesFromFile[2]) elseif string.find(DataReceived, "GET / HTTP") then --if the standard 192.168.1.124 page is loaded then do this serve_html(conn) else --if we don't recognise the data e.g. say 192.168.1.124/something_unexpected then conn:send("<h1> Hello, NodeMcu.</h1>") end end) conn:on("sent",function(conn) conn:close() collectgarbage() end) end)
LED_HTML_Ajax.html
<!DOCTYPE html> <html> <head> <style> body {background-color:lightgrey} .PIR_ACTIVATED_CLASS {color:black; background-color:red; font-size:22px;} .PIR_NOT_ACTIVATED_CLASS {color:blue; font-size:22px;} </style> <script src="https://code.jquery.com/jquery-1.11.3.js"></script> <!-- <script src="http://192.168.1.100/ESP8266_Test/LED_HTML_Ajax.js"></script> --> <script> function power(state) { $.get("/" + state); } $(function(){ $("#power_on").click(function(){ power('on');}); $("#power_off").click(function(){ power('off');}); }); $(document).ready(function(){ window.setInterval(AskServerForPIRStatus, 2*1000);//2 secs. $("div.PIR_Status").click(function(){ power('PIR_Status'); }); function AskServerForPIRStatus() { $.get("/PIR_Status", function(data){ if(data.indexOf("PIR Value = 0")!=-1) { $("div.PIR_Status").html("<span class='PIR_NOT_ACTIVATED_CLASS'>"+"PIR: Not Activted"+"</span>"); } else { $("div.PIR_Status").html("<span class='PIR_ACTIVATED_CLASS'>"+"PIR: Activated!"+"</span>"); } }); } }); </script> </head> <body> <h1>LED Power ESP8266 Webserver + PIR</h1><span>Onboard Red LED: </span><input type="radio" name="power" id="power_on" value="on">on</input> <input type="radio" name="power" value="off" id="power_off" value="off" checked>off</input> <br><br><div class="PIR_Status">Loading...</div><br><a href="/Settings.html">Settings</a> </body></html>
ConfigureWebpage.txt
192.168.1.100/ESP8266_Test/php_Server.php?PIR_Value=
php Code (/var/www/ESP8266_Test/php_Server.php)
<?php
//Access via 192.168.1.100/ESP8266_Test/php_Server.php for test purposes date_default_timezone_set("America/Panama"); //Set time to Panama $TimeStamp=date('Y-m-d H:i:s',time()); //I have the following here for test purposes: echo "<table border='1' style='width:50%'>"; echo "<tr><td>IP Address of device contacting this php server</td><td>".$_SERVER['REMOTE_ADDR']."</td></tr>"; echo "<tr><td>PIR Value from ESP8266</td><td>".$_GET['PIR_Value']."</td></tr>"; echo "</table>"; $file = 'ESP_Log.txt'; // Open the file to get existing content $current = file_get_contents($file); // Append a new person to the file $current .= "\n".$TimeStamp.", "; $current .= "IP Address of device contacting this php server = ".$_SERVER['REMOTE_ADDR'].", "; $current .= "PIR Value from ESP8266 = ".$_GET['PIR_Value']; // Write the contents back to the file file_put_contents($file, $current);?> ?>
php Socket
NOTE: If you wish to go down the php socket route then the following code may be useful although I don't use this here for this example.<?php date_default_timezone_set("America/Panama"); //Set time to Panama $TimeStamp=date('Y-m-d H:i:s',time()); $reply='10000000'; //10s $reply='php socket server from Virtual Box says hi'; $socket = socket_create(AF_INET, SOCK_STREAM, 0); if ( socket_bind($socket, "0.0.0.0" , 2500) === FALSE ) { exit(1); } socket_listen( $socket, 0 ); while(1) { $connection = socket_accept($socket); socket_send($connection, $reply, strlen($reply), 0); socket_recv($connection, $pkt, 1024, 0); if (strlen($pkt)>0){ date("H:i:s").",".$IP_Address_Of_Client." Data: ".trim($pkt)."\n"; echo date("H:i:s")." Data: ".trim($pkt)."\n"; } socket_close($connection); } ?>
Settings.html
<!DOCTYPE html> <html> <head> <style> body {background-color:lightgrey} .PIR_ACTIVATED_CLASS {color:black; background-color:red; font-size:22px;} .PIR_NOT_ACTIVATED_CLASS {color:blue; font-size:22px;} #UpdateSettings{color: blue; text-decoration: underline;} #FullURLPath {color:grey;} </style> <script src="https://code.jquery.com/jquery-1.11.3.js"></script> <script> function SendToServer(state) { $.get("/" + state); } $(document).ready(function(){ AskServerForUserSettings() $("span#UpdateSettings").click(function(){ var Username=$("html body input#Username").val(); var Password=$("html body input#Password").val(); var Email=$("html body input#Email").val(); SendToServer("NewSettingsMadeByClient"+"|"+Username+","+Password+"|"); }); $("input").keyup(function(){ $("span#FullURLPath").text("=> "+$("html body input#Username").val()+$("html body input#Password").val()); }); function AskServerForUserSettings() { $.get("/RequestSettingValues", function(data){ var array = data.split(','); //console.log(array[0]); //console.log(array[1]); $("html body input#Username").val(array[0]); $("html body input#Password").val(array[1]); $("span#FullURLPath").text("=> "+$("html body input#Username").val()+$("html body input#Password").val()); }); } }); //end of document ready </script> </head> <body> <h1>Settings</h1><h3>Location of webpage to hit when the PIR detects motion</h3> URL:<br> <input type="text" id="Username" name="Username" maxlength="100" size="100"><br> Path to Webpage:<br> <input type="text" id="Password" name="Password" maxlength="100" size="100"><br> <br><span id="FullURLPath""></span> <br><br> <span id="UpdateSettings">Save Settings</span><br><a href="/">Home</a> </body> </html>
Testing - php Log file
Via the HUZZAH ESP8266 server, I set the URL location of my php webpage (that will record things as they occur). Be careful setting the correct URL details because there is no error checking code on the ESP8266 server so the lua code will start to crash and you will have to re upload the "ConfigureWebpage.txt" file to fix things!HUZZAH ESP8266 Webserver - Settings html webpage |
On your linux machine (in my case I’m using a Virtual Box) you can use the Linux "watch" command to see things every say 2 seconds. The php code time stamps the motion sensor (PIR) events:
watch -n 2 cat
/var/www/ESP8266_Test/ESP_Log.txt
Now when you activate the PIR motion sensor, the log file on the Linux machine will update as the HUZZAH hits the php webpage (and you will see the update every 2 seconds via "watch"). Note that the right most "1" digit below mean logic 1 (i.e. PIR activated), 0 is logic "0" i.e. not activated. The time difference is specified on the actual PIR device via the potentiometers, in the case below the time difference is 7 seconds. That is to the say, the PIR will go high (logic 1) for about 7 seconds and then go low logic 0 if no longer being activated (no motion occurring).
=>
Supperputty screenshot of logfile |
Useful References:
Installing NodeMCU firmware:
Flasher Download:
=>Execute ESP8266_flasher.exe and burn the bin inside LUA Firmware
ESPlorer
NodeMCU Tutorial Page:
Adafruit Tutorial
Good tutorial (update firmware etc):
No comments:
Post a Comment