-->
Page 1 of 5

Start up an AP if unable to reconnect to last network

PostPosted: Wed Feb 25, 2015 1:36 am
by draco
I wanted to be able to configure a device purely through wifi, in case I didn't have a serial port available. I couldn't find anything on here to do that, so I decided to write my own. Maybe it will be useful for someone.

On startup, it attempts to reconnect to its last SSID. If that's successful, it launches 'task.lua'. If it's not able to reconnect after 30 seconds, it starts up an Access Point named 'ConfigureMe-XX-YY' with no security. If you connect to it and go to http://192.168.4.1/ in your browser, you get a little configuration screen that will let you tell the ESP8266 what SSID and password you'd like it to connect to. Then you hit the reboot button, and (hopefully) your new settings take effect.

This is bigger than I'd like, but it seems to work for me. Maybe others have some ideas for optimizing it. It's split into three files to avoid heap problems.

init.lua:
Code: Select alltimeout = 30 -- seconds to wait for connect before going to AP mode
               
statuses = {[0]="Idle",
            [1]="Connecting",
            [2]="Wrong password",
            [3]="No AP found",
            [4]="Connect fail",
            [5]="Got IP",
            [255]="Not in STATION mode"}
           
checkCount = 0
function checkStatus()
  checkCount = checkCount + 1
  local s=wifi.sta.status()
  print("Status = " .. s .. " (" .. statuses[s] .. ")") 
  if(s==5) then -- successful connect
    launchApp()
    return
  elseif(s==2 or s==3 or s==4) then -- failed
    startServer()
    return
  end
  if(checkCount >= timeout) then
    startServer()
    return
  end
end

function launchApp()
  cleanup()
  print("I'm connected to my last network. Launching my real task.")
  local task = 'task.lua'
  local f=file.open(task, 'r')
  if(f == nil) then
    print('Error opening file ' .. task)
    return
  end
  f.close()
  dofile(task)
end

function startServer()
  lastStatus = statuses[wifi.sta.status()]
  cleanup()
  print("network not found, switching to AP mode")
  dofile('configServer.lua')
end

function cleanup()
  -- stop our alarm
  tmr.stop(0)
  -- nil out all global vars we used
  timeout = nil
  statuses = nil
  checkCount = nil
  -- nil out any functions we defined
  checkStatus = nil
  launchApp = nil
  startServer = nil
  cleanup = nil
  -- take out the trash
  collectgarbage()
  -- pause a few seconds to allow garbage to collect and free up heap
  tmr.delay(5000)
end

-- make sure we are trying to connect as clients
wifi.setmode(wifi.STATION)
wifi.sta.autoconnect(1)

-- every second, check our status
tmr.alarm(0, 1000, 1, checkStatus)



configServer.lua:
Code: Select alldofile("configServerInit.lua")

apRefresh = 15 -- how many seconds between when we scan for updated AP info for the user
currentAPs = {}

newssid = ""

function listAPs_callback(t)
  if(t==nil) then
    return
  end
  currentAPs = t
end

function listAPs()
  wifi.sta.getap(listAPs_callback)
end

function sendPage(conn)
  conn:send('HTTP/1.1 200 OK\n\n')
  conn:send('<!DOCTYPE HTML>\n<html>\n<head><meta content="text/html; charset=utf-8">\n<title>Device Configuration</title></head>\n<body>\n<form action="/" method="POST">\n')

  if(lastStatus ~= nil) then
    conn:send('<br/>Previous connection status: ' .. lastStatus ..'\n')
  end
 
  if(newssid ~= "") then
    conn:send('<br/>Upon reboot, unit will attempt to connect to SSID "' .. newssid ..'".\n')
  end
   
  conn:send('<br/><br/>\n\n<table>\n<tr><th>Choose SSID to connect to:</th></tr>\n')

  for ap,v in pairs(currentAPs) do
    conn:send('<tr><td><input type="button" onClick=\'document.getElementById("ssid").value = "' .. ap .. '"\' value="' .. ap .. '"/></td></tr>\n')
  end
 
  conn:send('</table>\n\nSSID: <input type="text" id="ssid" name="ssid" value=""><br/>\nPassword: <input type="text" name="passwd" value=""><br/>\n\n')
  conn:send('<input type="submit" value="Submit"/>\n<input type="button" onClick="window.location.reload()" value="Refresh"/>\n<br/>If you\'re happy with this...\n<input type="submit" name="reboot" value="Reboot!"/>\n')
  conn:send('</form>\n</body></html>')
 
end

function url_decode(str)
  local s = string.gsub (str, "+", " ")
  s = string.gsub (s, "%%(%x%x)",
      function(h) return string.char(tonumber(h,16)) end)
  s = string.gsub (s, "\r\n", "\n")
  return s
end

function incoming_connection(conn, payload)
  if (string.find(payload, "GET /favicon.ico HTTP/1.1") ~= nil) then
    print("GET favicon request")
  elseif (string.find(payload, "GET / HTTP/1.1") ~= nil) then
    print("GET received")
    sendPage(conn)
  else
    print("POST received")
    local blank, plStart = string.find(payload, "\r\n\r\n");
    if(plStart == nil) then
      return
    end
    payload = string.sub(payload, plStart+1)
    args={}
    args.passwd=""
    -- parse all POST args into the 'args' table
    for k,v in string.gmatch(payload, "([^=&]*)=([^&]*)") do
      args[k]=url_decode(v)
    end
    if(args.ssid ~= nil and args.ssid ~= "") then
      print("New SSID: " .. args.ssid)
      print("Password: " .. args.passwd)
      newssid = args.ssid
      wifi.sta.config(args.ssid, args.passwd)
    end
    if(args.reboot ~= nil) then
      print("Rebooting")
      conn:close()
      node.restart()
    end
    conn:send('HTTP/1.1 303 See Other\n')
    conn:send('Location: /\n')
  end
end

-- start a periodic scan for other nearby APs
tmr.alarm(0, apRefresh*1000, 1, listAPs)
listAPs() -- and do it once to start with
 
-- Now we set up the Web Server
srv=net.createServer(net.TCP)
srv:listen(80,function(sock)
  sock:on("receive", incoming_connection)
  sock:on("sent", function(sock)
    sock:close()
  end)
end)


configServerInit.lua:
Code: Select allapNamePrefix = "ConfigureMe" -- your AP will be named this plus "-XX-YY", where XX and YY are the last two bytes of this unit's MAC address

apNetConfig = {ip      = "192.168.4.1", -- NodeMCU seems to be hard-coded to hand out IPs in the 192.168.4.x range, so let's make sure we're there, too
               netmask = "255.255.255.0",
               gateway = "192.168.4.1"}

-- Set up our Access Point with the proper name and settings
local apName = apNamePrefix .. "-" .. string.sub(wifi.ap.getmac(),13)
print("Starting up AP with SSID: " .. apName);
wifi.setmode(wifi.STATIONAP)
local apSsidConfig = {}
apSsidConfig.ssid = apName
wifi.ap.config(apSsidConfig)
wifi.ap.setip(apNetConfig)

Re: Start up an AP if unable to reconnect to last network

PostPosted: Wed Feb 25, 2015 6:54 am
by andrew melvin
Good work, I've been trying to do exactly that, and made something that works quite well. Although there is one thing that I think might be useful...

regardless of what is set up... it might be worth having the esp made a hotspot for 60 seconds to give the IP address via a web page.

That way, even if it connects to a network, initially you can connect to it, get its IP, then it goes to station mode and continues as normal.. I wrote a file called sleep.txt when the esp was shutdown/rebooted/sent to sleep gracefully, so that it didn't make an AP when waking back up. But if it powered off, then the file is not there and it makes an AP for 60 seconds.

Your code also does not work for apple browsers... I figured out after a huge amount of head scratching why this it... it is true for safari, and browsers on iPad and phone. I kind of think having it work is fairly important...

what i noticed is that for POST requests... apple browsers send the first POST request, but it is empty... the second POST request contains nothing but the data, no headers... this is different from chrome and firefox where the payload is within the first POST request. I used a variable that was made true to scan the second POST request if the first one failed the string match!

Re: Start up an AP if unable to reconnect to last network

PostPosted: Thu Feb 26, 2015 11:48 am
by Macjbraun
Andrew, could you post your mods for us Safari fans?

Re: Start up an AP if unable to reconnect to last network

PostPosted: Thu Feb 26, 2015 12:42 pm
by draco
Thanjs for the feedback!
I'm not an Apple user, never occurred to me that they might handle such a simple use-case so differently. If you can share your fix, I'll see about borrowing an iDevice and integrating it into something that works for everyone, and posting an edit.