Adafruit HUZZAH ESP8266 PIR Webserver

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:

Note that the circuits above aren't using the decoupling capacitor as I was running the circuit from a "USB to TTL Serial Console Cable" at the time and didn't experience problems.

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).

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.


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:

  1. LED_Server_OLED_Ajax.lua
  2. init.lua
  3. LED_HTML_Ajax.html
  4. Settings.html
  5. 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 "LED_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

















No comments:

Post a Comment