Commit 86839d6e authored by Johny Mattsson's avatar Johny Mattsson
Browse files

Fixes and enchancements for enduser_setup module.

Fixes:

* Removed spurious \0 bytes being sent to the client in many cases.

* Properly terminated 404 header.

* Now including Content-length: header for index.html (including built-in).

  Clients otherwise had to wait for their own timeout before considering
  the page load complete, which resulting in an unpleasant sluggish
  experience.

* Switched form submission to GET, as iOS sometimes does not include the
  body in the same packet as the POST header, which led to the module
  not finding the form values.

* Added redirect after form submission to avoid blank-page-in-browser.

* Moved common "enduser_setup" prefix to a lua_pushfstring() to reduce
  string literal overhead.

* Replaced cached lua_State* with calls to lua_getstate(), as in other
  modules.

* Fixed broken appending of MAC to the SSID.

* Fixed up macro definitions to not break `if` clauses unintentionally.

Enhancements:

* Support for scanning for available WiFi networks, and picking from the list.

* Support for retrieving the current WiFi station status.

* Added "manual" mode, where the module does not (re)configure the AP setup,
  nor shut down automatically. Default is still automatic, as before.

* Updated docs for new `enduser_setup.manual()` function.
parent d5f71eb0
......@@ -29,12 +29,15 @@
* OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @author Robert Foss <dev@robertfoss.se>
*
* Additions & fixes: Johny Mattsson <jmattsson@dius.com.au>
*/
#include "module.h"
#include "lauxlib.h"
#include "platform.h"
#include "c_stdlib.h"
#include "c_stdio.h"
#include "c_string.h"
#include "ctype.h"
#include "user_interface.h"
......@@ -42,6 +45,7 @@
#include "flash_fs.h"
#define MIN(x, y) (((x) < (y)) ? (x) : (y))
#define LITLEN(strliteral) (sizeof (strliteral) -1)
#define ENDUSER_SETUP_ERR_FATAL (1 << 0)
#define ENDUSER_SETUP_ERR_NONFATAL (1 << 1)
......@@ -70,14 +74,163 @@ static const char dns_body[] = { 0x00, 0x01, 0x00, 0x01,
0xC0, 0x0C, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x78, 0x00, 0x04 };
static const char http_html_filename[] = "index.html";
static const char http_header_200[] = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n";
static const char http_header_404[] = "HTTP/1.1 404 Not Found\r\n";
static const char http_html_backup[] = "<!DOCTYPE html><html><head><meta charset=utf-8><meta name=viewport content='width=380'><title>Connect gadget to you WiFi</title><style media=screen type=text/css>*{margin:0;padding:0}html{height:100%;background:linear-gradient(rgba(196,102,0,.2),rgba(155,89,182,.2)),url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEYAAAA8AgMAAACm+SSwAAAADFBMVEVBR1FFS1VHTlg8Q0zU/YXIAAADVElEQVQ4yy1TTYvTUBQ9GTKiYNoodsCF4MK6U4TZChOhiguFWHyBFzqlLl4hoeNvEBeCrlrhBVKq1EUKLTP+hvi1GyguXqBdiZCBzGqg20K8L3hDQnK55+OeJNguHx6UujYl3dL5ALn4JOIUluAqeAWciyGaSdvngOWzNT+G0UyGUOxVOAdqkjXDCbBiUyjZ5QzYEbGadYAi6kHxth+kthXNVNCDofwhGv1D4QGGiM9iAjbCHgr2iUUpDJbs+VPQ4xAr2fX7KXbkOJMdok965Ksb+6lrjdkem8AshIuHm9Nyu19uTunYlOXDTQqi8VgeH0kBXH2xq/ouiMZPzuMukymutrBmulUTovC6HqNFW2ZOiqlpSXZOTvSUeUPxChjxol8BLbRy4gJuhV7OR4LRVBs3WQ9VVAU7SXgK2HeUrOj7bC8YsUgr3lEV/TXB7hK90EBnxaeg1Ov15bY80M736ekCGesGAaGvG0Ct4WRkVQVHIgIM9xJgvSFfPay8Q6GNv7VpR7xUnkvhnMQCJDYkYOtNLihV70tCU1Sk+BQrpoP+HLHUrJkuta40C6LP5GvBv+Hqo10ATxxFrTPvNdPr7XwgQud6RvQN/sXjBGzqbU27wcj9cgsyvSTrpyXV8gKpXeNJU3aFl7MOdldzV4+HfO19jBa5f2IjWwx1OLHIvFHkqbBj20ro1g7nDfY1DpScvDRUNARgjMMVO0zoMjKxJ6uWCPP+YRAWbGoaN8kXYHmLjB9FXLGOazfFVCvOgqzfnicNPrHtPKlex2ye824gMza0cTZ2sS2Xm7Qst/UfFw8O6vVtmUKxZy9xFgzMys5cJ5fxZw4y37Ufk1Dsfb8MqOjYxE3ZMWxiDcO0PYUaD2ys+8OW1pbB7/e3sfZeGVCL0Q2aMjjPdm2sxADuejZxHJAd8dO9DSUdA0V8/NggRRanDkBrANn8yHlEQOn/MmwoQfQF7xgmKDnv520bS/pgylP67vf3y2V5sCwfoCEMkZClgOfJAFX9eXefR2RpnmRs4CDVPceaRfoFzCkJVJX27vWZnoqyvmtXU3+dW1EIXIu8Qg5Qta4Zlv7drUCoWe8/8MXzaEwux7ESE9h6qnHj3mIO0/D9RvzfxPmjWiQ1vbeSk4rrHwhAre35EEVaAAAAAElFTkSuQmCC)}body{font-family:arial,verdana}div{position:absolute;margin:auto;top:0;right:0;bottom:0;left:0;width:320px;height:274px}form{width:320px;text-align:center;position:relative}form fieldset{background:#fff;border:0 none;border-radius:5px;box-shadow:0 0 15px 1px rgba(0,0,0,.4);padding:20px 30px;box-sizing:border-box}form input{padding:15px;border:1px solid #ccc;border-radius:3px;margin-bottom:10px;width:100%;box-sizing:border-box;font-family:montserrat;color:#2C3E50;font-size:13px}form .action-button{width:100px;background:#27AE60;font-weight:700;color:#fff;border:0 none;border-radius:3px;cursor:pointer;padding:10px 5px;margin:10px 5px}#msform .action-button:focus,form .action-button:hover{box-shadow:0 0 0 2px #fff,0 0 0 3px #27AE60}.fs-title{font-size:15px;text-transform:uppercase;color:#2C3E50;margin-bottom:10px}.fs-subtitle{font-weight:400;font-size:13px;color:#666;margin-bottom:20px}</style><body><div><form method='post'><fieldset><h2 class=fs-title>WiFi Login</h2><h3 class=fs-subtitle>Connect gadget to your WiFi</h3><input autocorrect=off autocapitalize=none name=wifi_ssid placeholder='WiFi Name'> <input type=password name=wifi_password placeholder='Password'1> <input type=submit name=save class='submit action-button' value='Save'></fieldset></form></div>";
static const char http_header_200[] = "HTTP/1.1 200 OK\r\nCache-control:no-cache\r\nContent-Type: text/html\r\n"; /* Note single \r\n here! */
static const char http_header_302[] = "HTTP/1.1 302 Moved\r\nLocation: /\r\n\r\n";
static const char http_header_401[] = "HTTP/1.1 401 Bad request\r\n\r\n";
static const char http_header_404[] = "HTTP/1.1 404 Not found\r\n\r\n";
static const char http_header_500[] = "HTTP/1.1 500 Internal Error\r\n\r\n";
/* The below is the un-minified version of the http_html_backup[] string.
* Minified using https://kangax.github.io/html-minifier/
* Note: using method="get" due to iOS not always sending body in same
* packet as the HTTP header, and us thus missing it in that case
*/
#if 0
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta charset="utf-8">
<meta name="viewport" content="width=380">
<title>WiFi Login</title>
<style media="screen" type="text/css">
*{margin:0;padding:0}
html{height:100%;background:linear-gradient(rgba(196,102,0,.2),rgba(155,89,182,.2)),url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEYAAAA8AgMAAACm+SSwAAAADFBMVEVBR1FFS1VHTlg8Q0zU/YXIAAADVElEQVQ4yy1TTYvTUBQ9GTKiYNoodsCF4MK6U4TZChOhiguFWHyBFzqlLl4hoeNvEBeCrlrhBVKq1EUKLTP+hvi1GyguXqBdiZCBzGqg20K8L3hDQnK55+OeJNguHx6UujYl3dL5ALn4JOIUluAqeAWciyGaSdvngOWzNT+G0UyGUOxVOAdqkjXDCbBiUyjZ5QzYEbGadYAi6kHxth+kthXNVNCDofwhGv1D4QGGiM9iAjbCHgr2iUUpDJbs+VPQ4xAr2fX7KXbkOJMdok965Ksb+6lrjdkem8AshIuHm9Nyu19uTunYlOXDTQqi8VgeH0kBXH2xq/ouiMZPzuMukymutrBmulUTovC6HqNFW2ZOiqlpSXZOTvSUeUPxChjxol8BLbRy4gJuhV7OR4LRVBs3WQ9VVAU7SXgK2HeUrOj7bC8YsUgr3lEV/TXB7hK90EBnxaeg1Ov15bY80M736ekCGesGAaGvG0Ct4WRkVQVHIgIM9xJgvSFfPay8Q6GNv7VpR7xUnkvhnMQCJDYkYOtNLihV70tCU1Sk+BQrpoP+HLHUrJkuta40C6LP5GvBv+Hqo10ATxxFrTPvNdPr7XwgQud6RvQN/sXjBGzqbU27wcj9cgsyvSTrpyXV8gKpXeNJU3aFl7MOdldzV4+HfO19jBa5f2IjWwx1OLHIvFHkqbBj20ro1g7nDfY1DpScvDRUNARgjMMVO0zoMjKxJ6uWCPP+YRAWbGoaN8kXYHmLjB9FXLGOazfFVCvOgqzfnicNPrHtPKlex2ye824gMza0cTZ2sS2Xm7Qst/UfFw8O6vVtmUKxZy9xFgzMys5cJ5fxZw4y37Ufk1Dsfb8MqOjYxE3ZMWxiDcO0PYUaD2ys+8OW1pbB7/e3sfZeGVCL0Q2aMjjPdm2sxADuejZxHJAd8dO9DSUdA0V8/NggRRanDkBrANn8yHlEQOn/MmwoQfQF7xgmKDnv520bS/pgylP67vf3y2V5sCwfoCEMkZClgOfJAFX9eXefR2RpnmRs4CDVPceaRfoFzCkJVJX27vWZnoqyvmtXU3+dW1EIXIu8Qg5Qta4Zlv7drUCoWe8/8MXzaEwux7ESE9h6qnHj3mIO0/D9RvzfxPmjWiQ1vbeSk4rrHwhAre35EEVaAAAAAElFTkSuQmCC)}
body{font-family:arial,verdana}
div{position:absolute;margin:auto;top:0;right:0;bottom:0;left:0;width:320px;height:304px}
form{width:320px;text-align:center;position:relative}
form fieldset{background:#fff;border:0 none;border-radius:5px;box-shadow:0 0 15px 1px rgba(0,0,0,.4);padding:20px 30px;box-sizing:border-box}
form input{padding:15px;border:1px solid #ccc;border-radius:3px;margin-bottom:10px;width:100%;box-sizing:border-box;font-family:montserrat;color:#2C3E50;font-size:13px}
form .action-button{border:0 none;border-radius:3px;cursor:pointer;}
#msform .submit:focus,form .action-button:hover{box-shadow:0 0 0 2px #fff,0 0 0 3px #27AE60;}
#msform .wifitoggle:focus,form .wifitoggle:hover{box-shadow:0 0 0 2px #fff,0 0 0 3px #ccc;}
select{width:100%;margin-bottom: 20px;padding: 10px 5px; border:1px solid #ccc;display:none;}
.fs-title{font-size:15px;text-transform:uppercase;color:#2C3E50;margin-bottom:10px}
.fs-subtitle{font-weight:400;font-size:13px;color:#666;margin-bottom:20px}
.fs-status{font-weight:400;font-size:13px;color:#666;margin-bottom:10px;padding-top:20px; border-top:1px solid #ccc}
.submit{width:100px;background: #27AE60; color: #fff;font-weight:700;margin:10px 5px; padding: 10px 5px; }
.wifitoggle{float:right;clear:both;max-width:75%; font-size:11px;background:#ccc; color: #000; margin: 0 0 10px; 0; padding: 5px 10px;}
</style>
</head>
<body>
<div>
<form method="get" action="/update">
<fieldset>
<h2 class="fs-title">WiFi Login</h2>
<h3 class="fs-subtitle">Connect gadget to your WiFi network</h3>
<input autocorrect="off" autocapitalize="none" name="wifi_ssid" id="ssid" placeholder="WiFi Name">
<button type='button' class="action-button wifitoggle" id="apbutton" disabled>Searching for networks...</button>
<select name="aplist" id="aplist" size="2">
</select>
<input name="wifi_password" placeholder="Password" type="password">
<input type=submit name=save class='action-button submit' value='Save'>
<h3 class="fs-status">Status: <span id="status">Updating...</span></h3>
</fieldset>
</form>
</div>
<script>
function aplist() { return document.getElementById("aplist"); }
function fetch(url, method, callback)
{
var xhr = new XMLHttpRequest();
xhr.onreadystatechange=check_ready;
function check_ready()
{
if (xhr.readyState === 4)
callback(xhr.status === 200 ? xhr.responseText : null);
}
xhr.open(method, url, true);
xhr.send();
}
function new_status(stat)
{
if (stat)
{
var e = document.getElementById("status");
e.innerHTML = stat;
}
setTimeout(refresh_status, 3000);
}
function refresh_status()
{
fetch('/status','GET',new_status);
}
function refresh_ap_list()
{
var sel = aplist ();
sel.innerHTML = '<option value="" disabled>Scanning...</option>';
fetch('/aplist','GET', got_ap_list);
}
function toggle_aplist(ev, force)
{
var sel = aplist();
var btn = document.getElementById("apbutton");
if (force || sel.style.display == 'block')
{
sel.style.display = 'none';
btn.innerHTML = 'Show networks';
}
else
{
sel.style.display = 'block';
btn.innerHTML = 'Hide networks';
}
}
function got_ap_list(json)
{
var btn = document.getElementById("apbutton");
if (json)
{
var list = JSON.parse(json);
list.sort(function(a,b){ return b.rssi - a.rssi; });
var ssids=list.map(function(a) { return a.ssid; }).filter(function(item, pos, self) { return self.indexOf(item)==pos; });
var sel = aplist();
sel.innerHTML = "";
sel.setAttribute("size", Math.max(Math.min(5, list.length), 2));
for (var i = 0; i < ssids.length; ++i)
{
var o = document.createElement("option");
sel.options.add(o);
o.innerHTML = ssids[i];
}
btn.disabled = false;
toggle_aplist(null, true);
btn.onclick = toggle_aplist;
}
else
{
btn.innerHTML = "No networks found";
}
}
window.onload=function()
{
refresh_status();
refresh_ap_list();
aplist().onchange = function() { document.getElementById("ssid").value = aplist().value; }
}
</script>
</body>
</html>
#endif
static const char http_html_backup[] =
"<!DOCTYPE html><html><head><meta http-equiv=content-type content=\"text/html; charset=UTF-8\"><meta charset=utf-8><meta name=viewport content=\"width=380\"><title>WiFi Login</title><style media=screen type=text/css>*{margin:0;padding:0}html{height:100%;background:linear-gradient(rgba(196,102,0,.2),rgba(155,89,182,.2)),url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEYAAAA8AgMAAACm+SSwAAAADFBMVEVBR1FFS1VHTlg8Q0zU/YXIAAADVElEQVQ4yy1TTYvTUBQ9GTKiYNoodsCF4MK6U4TZChOhiguFWHyBFzqlLl4hoeNvEBeCrlrhBVKq1EUKLTP+hvi1GyguXqBdiZCBzGqg20K8L3hDQnK55+OeJNguHx6UujYl3dL5ALn4JOIUluAqeAWciyGaSdvngOWzNT+G0UyGUOxVOAdqkjXDCbBiUyjZ5QzYEbGadYAi6kHxth+kthXNVNCDofwhGv1D4QGGiM9iAjbCHgr2iUUpDJbs+VPQ4xAr2fX7KXbkOJMdok965Ksb+6lrjdkem8AshIuHm9Nyu19uTunYlOXDTQqi8VgeH0kBXH2xq/ouiMZPzuMukymutrBmulUTovC6HqNFW2ZOiqlpSXZOTvSUeUPxChjxol8BLbRy4gJuhV7OR4LRVBs3WQ9VVAU7SXgK2HeUrOj7bC8YsUgr3lEV/TXB7hK90EBnxaeg1Ov15bY80M736ekCGesGAaGvG0Ct4WRkVQVHIgIM9xJgvSFfPay8Q6GNv7VpR7xUnkvhnMQCJDYkYOtNLihV70tCU1Sk+BQrpoP+HLHUrJkuta40C6LP5GvBv+Hqo10ATxxFrTPvNdPr7XwgQud6RvQN/sXjBGzqbU27wcj9cgsyvSTrpyXV8gKpXeNJU3aFl7MOdldzV4+HfO19jBa5f2IjWwx1OLHIvFHkqbBj20ro1g7nDfY1DpScvDRUNARgjMMVO0zoMjKxJ6uWCPP+YRAWbGoaN8kXYHmLjB9FXLGOazfFVCvOgqzfnicNPrHtPKlex2ye824gMza0cTZ2sS2Xm7Qst/UfFw8O6vVtmUKxZy9xFgzMys5cJ5fxZw4y37Ufk1Dsfb8MqOjYxE3ZMWxiDcO0PYUaD2ys+8OW1pbB7/e3sfZeGVCL0Q2aMjjPdm2sxADuejZxHJAd8dO9DSUdA0V8/NggRRanDkBrANn8yHlEQOn/MmwoQfQF7xgmKDnv520bS/pgylP67vf3y2V5sCwfoCEMkZClgOfJAFX9eXefR2RpnmRs4CDVPceaRfoFzCkJVJX27vWZnoqyvmtXU3+dW1EIXIu8Qg5Qta4Zlv7drUCoWe8/8MXzaEwux7ESE9h6qnHj3mIO0/D9RvzfxPmjWiQ1vbeSk4rrHwhAre35EEVaAAAAAElFTkSuQmCC)}body{font-family:arial,verdana}div{position:absolute;margin:auto;top:0;right:0;bottom:0;left:0;width:320px;height:304px}form{width:320px;text-align:center;position:relative}form fieldset{background:#fff;border:0 none;border-radius:5px;box-shadow:0 0 15px 1px rgba(0,0,0,.4);padding:20px 30px;box-sizing:border-box}form input{padding:15px;border:1px solid #ccc;border-radius:3px;margin-bottom:10px;width:100%;box-sizing:border-box;font-family:montserrat;color:#2C3E50;font-size:13px}form .action-button{border:0 none;border-radius:3px;cursor:pointer}#msform .submit:focus,form .action-button:hover{box-shadow:0 0 0 2px #fff,0 0 0 3px #27AE60}#msform .wifitoggle:focus,form .wifitoggle:hover{box-shadow:0 0 0 2px #fff,0 0 0 3px #ccc}select{width:100%;margin-bottom:20px;padding:10px 5px;border:1px solid #ccc;display:none}.fs-title{font-size:15px;text-transform:uppercase;color:#2C3E50;margin-bottom:10px}.fs-subtitle{font-weight:400;font-size:13px;color:#666;margin-bottom:20px}.fs-status{font-weight:400;font-size:13px;color:#666;margin-bottom:10px;padding-top:20px;border-top:1px solid #ccc}.submit{width:100px;background:#27AE60;color:#fff;font-weight:700;margin:10px 5px;padding:10px 5px}.wifitoggle{float:right;clear:both;max-width:75%;font-size:11px;background:#ccc;color:#000;margin:0 0 10px;padding:5px 10px}</style><body><div><form action=/update><fieldset><h2 class=fs-title>WiFi Login</h2><h3 class=fs-subtitle>Connect gadget to your WiFi network</h3><input autocorrect=off autocapitalize=none name=wifi_ssid id=ssid placeholder=\"WiFi Name\"> <button type=button class=\"action-button wifitoggle\" id=apbutton disabled>Searching for networks...</button><select name=aplist id=aplist size=2></select><input name=wifi_password placeholder=Password type=password> <input type=submit name=save class=\"action-button submit\" value=Save><h3 class=fs-status>Status: <span id=status>Updating...</span></h3></fieldset></form></div><script>function aplist(){return document.getElementById(\"aplist\")}function fetch(t,e,n){function s(){4===i.readyState&&n(200===i.status?i.responseText:null)}var i=new XMLHttpRequest;i.onreadystatechange=s,i.open(e,t,!0),i.send()}function new_status(t){if(t){var e=document.getElementById(\"status\");e.innerHTML=t}setTimeout(refresh_status,3e3)}function refresh_status(){fetch(\"/status\",\"GET\",new_status)}function refresh_ap_list(){var t=aplist();t.innerHTML='<option value=\"\" disabled>Scanning...</option>',fetch(\"/aplist\",\"GET\",got_ap_list)}function toggle_aplist(t,e){var n=aplist(),s=document.getElementById(\"apbutton\");e||\"block\"==n.style.display?(n.style.display=\"none\",s.innerHTML=\"Show networks\"):(n.style.display=\"block\",s.innerHTML=\"Hide networks\")}function got_ap_list(t){var e=document.getElementById(\"apbutton\");if(t){var n=JSON.parse(t);n.sort(function(t,e){return e.rssi-t.rssi});var s=n.map(function(t){return t.ssid}).filter(function(t,e,n){return n.indexOf(t)==e}),i=aplist();i.innerHTML=\"\",i.setAttribute(\"size\",Math.max(Math.min(5,n.length),2));for(var a=0;a<s.length;++a){var o=document.createElement(\"option\");i.options.add(o),o.innerHTML=s[a]}e.disabled=!1,toggle_aplist(null,!0),e.onclick=toggle_aplist}else e.innerHTML=\"No networks found\"}window.onload=function(){refresh_status(),refresh_ap_list(),aplist().onchange=function(){document.getElementById(\"ssid\").value=aplist().value}};</script>";
typedef struct scan_listener
{
struct scan_listener *next;
int remote_port;
uint8 remote_ip[4];
} scan_listener_t;
typedef struct
{
lua_State *lua_L;
struct espconn *espconn_dns_udp;
struct espconn *espconn_http_tcp;
char *http_payload_data;
......@@ -86,9 +239,11 @@ typedef struct
int lua_connected_cb_ref;
int lua_err_cb_ref;
int lua_dbg_cb_ref;
scan_listener_t *scan_listeners;
} enduser_setup_state_t;
static enduser_setup_state_t *state;
static bool manual = false;
static int enduser_setup_start(lua_State* L);
static int enduser_setup_stop(lua_State* L);
......@@ -104,7 +259,7 @@ static void enduser_setup_debug(lua_State *L, const char *str);
#if ENDUSER_SETUP_DEBUG_ENABLE
#define ENDUSER_SETUP_DEBUG(l, str) enduser_setup_debug(l, str)
#else
#define ENDUSER_SETUP_DEBUG(l, str)
#define ENDUSER_SETUP_DEBUG(l, str) do {} while(0)
#endif
......@@ -119,13 +274,21 @@ static void enduser_setup_debug(lua_State *L, const char *str)
}
#define ENDUSER_SETUP_ERROR(str, err, err_severity) if (err_severity & ENDUSER_SETUP_ERR_FATAL) enduser_setup_stop(state->lua_L);\
enduser_setup_error(state->lua_L, str, err);\
if (!(err_severity & ENDUSER_SETUP_ERR_NO_RETURN)) return err
#define ENDUSER_SETUP_ERROR(str, err, err_severity) \
do { \
if (err_severity & ENDUSER_SETUP_ERR_FATAL) enduser_setup_stop(lua_getstate());\
enduser_setup_error(lua_getstate(), str, err);\
if (!(err_severity & ENDUSER_SETUP_ERR_NO_RETURN)) \
return err; \
} while (0)
#define ENDUSER_SETUP_ERROR_VOID(str, err, err_severity) if (err_severity & ENDUSER_SETUP_ERR_FATAL) enduser_setup_stop(state->lua_L);\
enduser_setup_error(state->lua_L, str, err);\
if (!(err_severity & ENDUSER_SETUP_ERR_NO_RETURN)) return
#define ENDUSER_SETUP_ERROR_VOID(str, err, err_severity) \
do { \
if (err_severity & ENDUSER_SETUP_ERR_FATAL) enduser_setup_stop(lua_getstate());\
enduser_setup_error(lua_getstate(), str, err);\
if (!(err_severity & ENDUSER_SETUP_ERR_NO_RETURN)) \
return; \
} while (0)
static void enduser_setup_error(lua_State *L, const char *str, int err)
......@@ -136,7 +299,7 @@ static void enduser_setup_error(lua_State *L, const char *str, int err)
{
lua_rawgeti (L, LUA_REGISTRYINDEX, state->lua_err_cb_ref);
lua_pushnumber(L, err);
lua_pushstring(L, str);
lua_pushfstring(L, "enduser_setup: %s", str);
lua_call (L, 2, 0);
}
}
......@@ -156,8 +319,7 @@ static void enduser_setup_connected_callback(lua_State *L)
static void enduser_setup_check_station_start(void)
{
ENDUSER_SETUP_DEBUG(state->lua_L, "enduser_setup_check_station_start");
ENDUSER_SETUP_DEBUG(lua_getstate(), "enduser_setup_check_station_start");
os_timer_setfn(&(state->check_station_timer), enduser_setup_check_station, NULL);
os_timer_arm(&(state->check_station_timer), 1*1000, TRUE);
......@@ -166,7 +328,7 @@ static void enduser_setup_check_station_start(void)
static void enduser_setup_check_station_stop(void)
{
ENDUSER_SETUP_DEBUG(state->lua_L, "enduser_setup_check_station_stop");
ENDUSER_SETUP_DEBUG(lua_getstate(), "enduser_setup_check_station_stop");
os_timer_disarm(&(state->check_station_timer));
}
......@@ -197,10 +359,7 @@ static void enduser_setup_check_station(void *p)
return;
}
struct station_config cnf;
wifi_station_get_config(&cnf);
enduser_setup_connected_callback(state->lua_L);
enduser_setup_connected_callback(lua_getstate());
enduser_setup_stop(NULL);
}
......@@ -214,25 +373,11 @@ static void enduser_setup_check_station(void *p)
*/
static int enduser_setup_srch_str(const char *str, const char *srch_str)
{
int srch_str_len = c_strlen(srch_str);
int first_hit = INT_MAX;
int i;
for (i = 0; i < srch_str_len; ++i)
{
char *char_ptr = strchr(str, srch_str[i]);
if (char_ptr == NULL)
{
continue;
}
int char_idx = char_ptr - str;
first_hit = MIN(first_hit, char_idx);
}
if (first_hit == INT_MAX)
{
char *found = strpbrk (str, srch_str);
if (!found)
return -1;
}
return first_hit;
else
return found - str;
}
......@@ -245,18 +390,21 @@ static int enduser_setup_srch_str(const char *str, const char *srch_str)
*/
static int enduser_setup_http_load_payload(void)
{
ENDUSER_SETUP_DEBUG(state->lua_L, "enduser_setup_http_load_payload");
ENDUSER_SETUP_DEBUG(lua_getstate(), "enduser_setup_http_load_payload");
int f = fs_open(http_html_filename, fs_mode2flag("r"));
int err = fs_seek(f, 0, FS_SEEK_END);
int file_len = (int) fs_tell(f);
int err2 = fs_seek(f, 0, FS_SEEK_SET);
const char cl_hdr[] = "Content-length:%5d\r\n\r\n";
const size_t cl_len = LITLEN(cl_hdr) + 3; /* room to expand %4d */
if (f == 0 || err == -1 || err2 == -1)
{
ENDUSER_SETUP_DEBUG(state->lua_L, "enduser_setup_http_load_payload unable to load file index.html, loading backup HTML.");
ENDUSER_SETUP_DEBUG(lua_getstate(), "enduser_setup_http_load_payload unable to load file index.html, loading backup HTML.");
int payload_len = sizeof(http_header_200) + sizeof(http_html_backup);
int payload_len = LITLEN(http_header_200) + cl_len + LITLEN(http_html_backup);
state->http_payload_len = payload_len;
state->http_payload_data = (char *) c_malloc(payload_len);
if (state->http_payload_data == NULL)
......@@ -265,14 +413,15 @@ static int enduser_setup_http_load_payload(void)
}
int offset = 0;
c_memcpy(&(state->http_payload_data[offset]), &(http_header_200), sizeof(http_header_200));
offset += sizeof(http_header_200);
c_memcpy(&(state->http_payload_data[offset]), &(http_html_backup), sizeof(http_html_backup));
c_memcpy(&(state->http_payload_data[offset]), &(http_header_200), LITLEN(http_header_200));
offset += LITLEN(http_header_200);
offset += c_sprintf(state->http_payload_data + offset, cl_hdr, LITLEN(http_html_backup));
c_memcpy(&(state->http_payload_data[offset]), &(http_html_backup), LITLEN(http_html_backup));
return 1;
}
int payload_len = sizeof(http_header_200) + file_len;
int payload_len = LITLEN(http_header_200) + cl_len + file_len;
state->http_payload_len = payload_len;
state->http_payload_data = (char *) c_malloc(payload_len);
if (state->http_payload_data == NULL)
......@@ -281,8 +430,9 @@ static int enduser_setup_http_load_payload(void)
}
int offset = 0;
c_memcpy(&(state->http_payload_data[offset]), &(http_header_200), sizeof(http_header_200));
offset += sizeof(http_header_200);
c_memcpy(&(state->http_payload_data[offset]), &(http_header_200), LITLEN(http_header_200));
offset += LITLEN(http_header_200);
offset += c_sprintf(state->http_payload_data + offset, cl_hdr, file_len);
fs_read(f, &(state->http_payload_data[offset]), file_len);
return 0;
......@@ -351,7 +501,7 @@ static void enduser_setup_http_urldecode(char *dst, const char *src, int src_len
*/
static int enduser_setup_http_handle_credentials(char *data, unsigned short data_len)
{
ENDUSER_SETUP_DEBUG(state->lua_L, "enduser_setup_http_handle_credentials");
ENDUSER_SETUP_DEBUG(lua_getstate(), "enduser_setup_http_handle_credentials");
char *name_str = (char *) ((uint32_t)strstr(&(data[6]), "wifi_ssid="));
char *pwd_str = (char *) ((uint32_t)strstr(&(data[6]), "wifi_password="));
......@@ -360,8 +510,8 @@ static int enduser_setup_http_handle_credentials(char *data, unsigned short data
return 1;
}
int name_field_len = sizeof("wifi_ssid=") - 1;
int pwd_field_len = sizeof("wifi_password=") - 1;
int name_field_len = LITLEN("wifi_ssid=");
int pwd_field_len = LITLEN("wifi_password=");
char *name_str_start = name_str + name_field_len;
char *pwd_str_start = pwd_str + pwd_field_len;
......@@ -377,36 +527,26 @@ static int enduser_setup_http_handle_credentials(char *data, unsigned short data
enduser_setup_http_urldecode(cnf.ssid, name_str_start, name_str_len);
enduser_setup_http_urldecode(cnf.password, pwd_str_start, pwd_str_len);
int err = wifi_set_opmode(STATION_MODE | wifi_get_opmode());
if (err == FALSE)
{
wifi_set_opmode(~STATION_MODE & wifi_get_opmode());
ENDUSER_SETUP_ERROR("enduser_setup_station_start failed. wifi_set_opmode failed.", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_FATAL);
}
err = wifi_station_set_config(&cnf);
if (err == FALSE)
if (!wifi_station_disconnect())
{
wifi_set_opmode(~STATION_MODE & wifi_get_opmode());
ENDUSER_SETUP_ERROR("enduser_setup_station_start failed. wifi_station_set_config failed.", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_FATAL);
ENDUSER_SETUP_ERROR("station_start failed. wifi_station_disconnect failed.", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_NONFATAL);
}
err = wifi_station_disconnect();
if (err == FALSE)
if (!wifi_station_set_config(&cnf))
{
ENDUSER_SETUP_ERROR("enduser_setup_station_start failed. wifi_station_disconnect failed.", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_NONFATAL);
ENDUSER_SETUP_ERROR("station_start failed. wifi_station_set_config failed.", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_FATAL);
}
err = wifi_station_connect();
if (err == FALSE)
if (wifi_station_connect())
{
ENDUSER_SETUP_ERROR("enduser_setup_station_start failed. wifi_station_connect failed.\n", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_FATAL);
ENDUSER_SETUP_ERROR("station_start failed. wifi_station_connect failed.\n", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_FATAL);
}
ENDUSER_SETUP_DEBUG(state->lua_L, "WiFi Credentials Stored");
ENDUSER_SETUP_DEBUG(state->lua_L, "-----------------------");
ENDUSER_SETUP_DEBUG(state->lua_L, "name: ");
ENDUSER_SETUP_DEBUG(state->lua_L, cnf.ssid);
ENDUSER_SETUP_DEBUG(state->lua_L, "pass: ");
ENDUSER_SETUP_DEBUG(state->lua_L, cnf.password);
ENDUSER_SETUP_DEBUG(state->lua_L, "-----------------------");
ENDUSER_SETUP_DEBUG(lua_getstate(), "WiFi Credentials Stored");
ENDUSER_SETUP_DEBUG(lua_getstate(), "-----------------------");
ENDUSER_SETUP_DEBUG(lua_getstate(), "name: ");
ENDUSER_SETUP_DEBUG(lua_getstate(), cnf.ssid);
ENDUSER_SETUP_DEBUG(lua_getstate(), "pass: ");
ENDUSER_SETUP_DEBUG(lua_getstate(), cnf.password);
ENDUSER_SETUP_DEBUG(lua_getstate(), "-----------------------");
return 0;
}
......@@ -417,22 +557,22 @@ static int enduser_setup_http_handle_credentials(char *data, unsigned short data
*
* @return - return 0 iff html was served successfully
*/
static int enduser_setup_http_serve_header(struct espconn *http_client, char *header, uint32_t header_len)
static int enduser_setup_http_serve_header(struct espconn *http_client, const char *header, uint32_t header_len)
{
ENDUSER_SETUP_DEBUG(state->lua_L, "enduser_setup_http_serve_404");
ENDUSER_SETUP_DEBUG(lua_getstate(), "enduser_setup_http_serve_header");
int8_t err = espconn_sent(http_client, header, header_len);
int8_t err = espconn_send(http_client, (char *)header, header_len);
if (err == ESPCONN_MEM)
{
ENDUSER_SETUP_ERROR("enduser_setup_http_serve_header failed. espconn_send out of memory", ENDUSER_SETUP_ERR_OUT_OF_MEMORY, ENDUSER_SETUP_ERR_NONFATAL);
ENDUSER_SETUP_ERROR("http_serve_header failed. espconn_send out of memory", ENDUSER_SETUP_ERR_OUT_OF_MEMORY, ENDUSER_SETUP_ERR_NONFATAL);
}
else if (err == ESPCONN_ARG)
{
ENDUSER_SETUP_ERROR("enduser_setup_http_serve_header failed. espconn_send can't find network transmission", ENDUSER_SETUP_ERR_CONNECTION_NOT_FOUND, ENDUSER_SETUP_ERR_NONFATAL);
ENDUSER_SETUP_ERROR("http_serve_header failed. espconn_send can't find network transmission", ENDUSER_SETUP_ERR_CONNECTION_NOT_FOUND, ENDUSER_SETUP_ERR_NONFATAL);
}
else if (err != 0)
{
ENDUSER_SETUP_ERROR("enduser_setup_http_serve_header failed. espconn_send failed", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_NONFATAL);
ENDUSER_SETUP_ERROR("http_serve_header failed. espconn_send failed", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_NONFATAL);
}
return 0;
......@@ -446,31 +586,67 @@ static int enduser_setup_http_serve_header(struct espconn *http_client, char *he
*/
static int enduser_setup_http_serve_html(struct espconn *http_client)
{
ENDUSER_SETUP_DEBUG(state->lua_L, "enduser_setup_http_serve_html");
ENDUSER_SETUP_DEBUG(lua_getstate(), "enduser_setup_http_serve_html");
if (state->http_payload_data == NULL)
{
enduser_setup_http_load_payload();
}
int8_t err = espconn_sent(http_client, state->http_payload_data, state->http_payload_len);
int8_t err = espconn_send(http_client, state->http_payload_data, state->http_payload_len);
if (err == ESPCONN_MEM)
{
ENDUSER_SETUP_ERROR("enduser_setup_http_serve_html failed. espconn_send out of memory", ENDUSER_SETUP_ERR_OUT_OF_MEMORY, ENDUSER_SETUP_ERR_NONFATAL);
ENDUSER_SETUP_ERROR("http_serve_html failed. espconn_send out of memory", ENDUSER_SETUP_ERR_OUT_OF_MEMORY, ENDUSER_SETUP_ERR_NONFATAL);
}
else if (err == ESPCONN_ARG)
{
ENDUSER_SETUP_ERROR("enduser_setup_http_serve_html failed. espconn_send can't find network transmission", ENDUSER_SETUP_ERR_CONNECTION_NOT_FOUND, ENDUSER_SETUP_ERR_NONFATAL);
ENDUSER_SETUP_ERROR("http_serve_html failed. espconn_send can't find network transmission", ENDUSER_SETUP_ERR_CONNECTION_NOT_FOUND, ENDUSER_SETUP_ERR_NONFATAL);
}
else if (err != 0)
{
ENDUSER_SETUP_ERROR("enduser_setup_http_serve_html failed. espconn_send failed", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_NONFATAL);
ENDUSER_SETUP_ERROR("http_serve_html failed. espconn_send failed", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_NONFATAL);
}
return 0;
}
static void serve_status (struct espconn *conn)
{
ENDUSER_SETUP_DEBUG(lua_getstate(), "enduser_setup_serve_status");
const char fmt[] =
"HTTP/1.1 200 OK\r\n"
"Cache-control: no-cache\r\n"
"Content-type: text/plain\r\n"
"Content-length: %d\r\n"
"\r\n"
"%s";
const char *state[] =
{
"Idle",
"Connecting...",
"Failed to connect - wrong password",
"Failed to connect - network not found",
"Failed to connect",
"WiFi successfully connected!" /* TODO: include SSID */
};
const size_t num_states = sizeof(state)/sizeof(state[0]);
uint8_t which = wifi_station_get_connect_status ();
if (which < num_states)
{
const char *s = state[which];
int len = c_strlen (s);
char buf[sizeof (fmt) + 10 + len]; /* more than enough for the formatted */
len = c_sprintf (buf, fmt, len, s);
enduser_setup_http_serve_header (conn, buf, len);
}
else
enduser_setup_http_serve_header (conn, http_header_500, LITLEN(http_header_500));
}
/**
* Disconnect HTTP client
*
......@@ -478,90 +654,263 @@ static int enduser_setup_http_serve_html(struct espconn *http_client)
*/
static void enduser_setup_http_disconnect(struct espconn *espconn)
{
ENDUSER_SETUP_DEBUG(state->lua_L, "enduser_setup_http_disconnect");
ENDUSER_SETUP_DEBUG(lua_getstate(), "enduser_setup_http_disconnect");
//TODO: Construct and maintain os task queue(?) to be able to issue system_os_task with espconn_disconnect.
}
static void enduser_setup_http_recvcb(void *arg, char *data, unsigned short data_len)
/* --- WiFi AP scanning support -------------------------------------------- */
static void free_scan_listeners (void)
{
ENDUSER_SETUP_DEBUG(state->lua_L, "enduser_setup_http_recvcb");
struct espconn *http_client = (struct espconn *) arg;
int retval;
if (!state || !state->scan_listeners)
return;
if (c_strncmp(data, "POST ", 5) == 0)
scan_listener_t *l = state->scan_listeners , *next = 0;
while (l)
{
retval = enduser_setup_http_handle_credentials(data, data_len);
if (retval == 0)
{
enduser_setup_http_serve_header(http_client, (char *) http_header_200, sizeof(http_header_200));
enduser_setup_http_disconnect(http_client);
return;
}
else if (retval == 2)
next = l->next;
c_free (l);
l = next;
}
state->scan_listeners = 0;
}
static inline bool same_remote (uint8 addr1[4], int port1, uint8 addr2[4], int port2)
{
return (port1 == port2) && (c_memcmp (addr1, addr2, 4) == 0);
}
static void on_disconnect (void *arg)
{
struct espconn *conn = arg;
if (conn->type == ESPCONN_TCP && state && state->scan_listeners)
{
scan_listener_t **sl = &state->scan_listeners;
while (*sl)
{
ENDUSER_SETUP_ERROR_VOID("enduser_setup_http_recvcb failed. Failed to handle wifi credentials.", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_NONFATAL);
/* Remove any and all references to the closed conn from the scan list */
scan_listener_t *l = *sl;
if (same_remote (
l->remote_ip, l->remote_port,
conn->proto.tcp->remote_ip, conn->proto.tcp->remote_port))
{
*sl = l->next;
c_free (l);
/* No early exit to guard against multi-entry on list */
}
else
sl = &(*sl)->next;
}
}
}
static char *escape_ssid (char *dst, const char *src)
{
for (int i = 0; i < 32 && src[i]; ++i)
{
if (src[i] == '\\' || src[i] == '"')
*dst++ = '\\';
*dst++ = src[i];
}
return dst;
}
static void notify_scan_listeners (const char *payload, size_t sz)
{
if (!state)
return;
if (!state->espconn_http_tcp)
goto cleanup;
struct espconn *conn = state->espconn_http_tcp;
for (scan_listener_t *l = state->scan_listeners; l; l = l->next)
{
c_memcpy (conn->proto.tcp->remote_ip, l->remote_ip, 4);
conn->proto.tcp->remote_port = l->remote_port;
if (espconn_send(conn, (char *)payload, sz) != ESPCONN_OK)
ENDUSER_SETUP_DEBUG(lua_getstate(), "failed to send wifi list");
enduser_setup_http_disconnect(conn);
}
cleanup:
free_scan_listeners ();
}
if (retval != 1)
static void on_scan_done (void *arg, STATUS status)
{
if (!state || !state->scan_listeners)
return;
if (status == OK)
{
unsigned num_nets = 0;
for (struct bss_info *wn = arg; wn; wn = wn->next.stqe_next)
++num_nets;
const char header_fmt[] =
"HTTP/1.1 200 OK\r\n"
"Connection:close\r\n"
"Cache-control: no-cache\r\n"
"Content-type:application/json\r\n"
"Content-length:%4d\r\n"
"\r\n";
const size_t hdr_sz = sizeof (header_fmt) +1 -1; /* +expand %4d, -\0 */
/* To be able to safely escape a pathological SSID, we need 2*32 bytes */
const size_t max_entry_sz = sizeof("{\"ssid\":\"\",\"rssi\":},") + 2*32 + 6;
const size_t alloc_sz = hdr_sz + num_nets * max_entry_sz + 3;
char *http = os_zalloc (alloc_sz);
if (!http)
goto serve_500;
char *p = http + hdr_sz; /* start body where we know it will be */
/* p[0] will be clobbered when we print the header, so fill it in last */
++p;
for (struct bss_info *wn = arg; wn; wn = wn->next.stqe_next)
{
ENDUSER_SETUP_ERROR_VOID("enduser_setup_http_recvcb failed. Unknown error code.", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_NONFATAL);
if (wn != arg)
*p++ = ',';
const char entry_start[] = "{\"ssid\":\"";
strcpy (p, entry_start);
p += sizeof (entry_start) -1;
p = escape_ssid (p, wn->ssid);
const char entry_mid[] = "\",\"rssi\":";
strcpy (p, entry_mid);
p += sizeof (entry_mid) -1;
p += c_sprintf (p, "%d", wn->rssi);
*p++ = '}';
}
*p++ = ']';
size_t body_sz = (p - http) - hdr_sz;
c_sprintf (http, header_fmt, body_sz);
http[hdr_sz] = '['; /* Rewrite the \0 with the correct start of body */
notify_scan_listeners (http, hdr_sz + body_sz);
c_free (http);
return;
}
serve_500:
notify_scan_listeners (http_header_500, LITLEN(http_header_500));
}
/* ---- end WiFi AP scan support ------------------------------------------- */
static void enduser_setup_http_recvcb(void *arg, char *data, unsigned short data_len)
{
ENDUSER_SETUP_DEBUG(lua_getstate(), "enduser_setup_http_recvcb");
if (!state)
{
ENDUSER_SETUP_DEBUG(lua_getstate(), "ignoring received data while stopped");
return;
}
else if (c_strncmp(data, "GET ", 4) == 0)
struct espconn *http_client = (struct espconn *) arg;
if (c_strncmp(data, "GET ", 4) == 0)
{
/* Reject requests that probably aren't relevant to free up resources. */
if (c_strncmp(data, "GET / ", 6) != 0)
if (c_strncmp(data + 4, "/ ", 2) == 0)
{
ENDUSER_SETUP_DEBUG(state->lua_L, "enduser_setup_http_recvcb received too specific request.");
enduser_setup_http_serve_header(http_client, (char *) http_header_404, sizeof(http_header_404));
enduser_setup_http_disconnect(http_client);
return;
if (enduser_setup_http_serve_html(http_client) != 0)
ENDUSER_SETUP_ERROR_VOID("http_recvcb failed. Unable to send HTML.", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_NONFATAL);
}
else if (c_strncmp(data + 4, "/aplist ", 8) == 0)
{
scan_listener_t *l = os_malloc (sizeof (scan_listener_t));
if (!l)
ENDUSER_SETUP_ERROR_VOID("out of memory", ENDUSER_SETUP_ERR_OUT_OF_MEMORY, ENDUSER_SETUP_ERR_NONFATAL);
c_memcpy (l->remote_ip, http_client->proto.tcp->remote_ip, 4);
l->remote_port = http_client->proto.tcp->remote_port;
bool already = (state->scan_listeners != NULL);
l->next = state->scan_listeners;
state->scan_listeners = l;
retval = enduser_setup_http_serve_html(http_client);
if (retval != 0)
if (!already)
{
if (!wifi_station_scan (NULL, on_scan_done))
{
enduser_setup_http_serve_header (http_client, http_header_500, LITLEN(http_header_500));
free_scan_listeners ();
}
}
return;
}
else if (c_strncmp(data + 4, "/status ", 8) == 0)
{
serve_status (http_client);
}
else if (c_strncmp(data + 4, "/update?", 8) == 0)
{
switch (enduser_setup_http_handle_credentials(data, data_len))
{
case 0:
enduser_setup_http_serve_header(http_client, http_header_302, LITLEN(http_header_302));
break;
case 1:
enduser_setup_http_serve_header(http_client, http_header_401, LITLEN(http_header_401));
break;
default:
ENDUSER_SETUP_ERROR_VOID("http_recvcb failed. Failed to handle wifi credentials.", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_NONFATAL);
break;
}
}
else
{
enduser_setup_http_disconnect(http_client);
ENDUSER_SETUP_ERROR_VOID("enduser_setup_http_recvcb failed. Unable to send HTML.", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_NONFATAL);
ENDUSER_SETUP_DEBUG(lua_getstate(), "serving 404");
enduser_setup_http_serve_header(http_client, http_header_404, LITLEN(http_header_404));
}
}
else
else /* not GET */
{
enduser_setup_http_serve_header(http_client, (char *) http_header_404, sizeof(http_header_404));
enduser_setup_http_disconnect(http_client);
return;
enduser_setup_http_serve_header(http_client, http_header_401, LITLEN(http_header_401));
}
enduser_setup_http_disconnect (http_client);
}
static void enduser_setup_http_connectcb(void *arg)
{
ENDUSER_SETUP_DEBUG(state->lua_L, "enduser_setup_http_connectcb");
ENDUSER_SETUP_DEBUG(lua_getstate(), "enduser_setup_http_connectcb");
struct espconn *callback_espconn = (struct espconn *) arg;
int8_t err = 0;
err |= espconn_regist_recvcb(callback_espconn, enduser_setup_http_recvcb);
err |= espconn_regist_disconcb(callback_espconn, on_disconnect);
if (err != 0)
{
ENDUSER_SETUP_ERROR_VOID("enduser_setup_http_connectcb failed. Callback registration failed.", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_NONFATAL);
ENDUSER_SETUP_ERROR_VOID("http_connectcb failed. Callback registration failed.", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_NONFATAL);
}
}
static int enduser_setup_http_start(void)
{
ENDUSER_SETUP_DEBUG(state->lua_L, "enduser_setup_http_start");
ENDUSER_SETUP_DEBUG(lua_getstate(), "enduser_setup_http_start");
state->espconn_http_tcp = (struct espconn *) c_malloc(sizeof(struct espconn));
if (state->espconn_http_tcp == NULL)
{
ENDUSER_SETUP_ERROR("enduser_setup_http_start failed. Memory allocation failed (espconn_http_tcp == NULL).", ENDUSER_SETUP_ERR_OUT_OF_MEMORY, ENDUSER_SETUP_ERR_FATAL);
ENDUSER_SETUP_ERROR("http_start failed. Memory allocation failed (espconn_http_tcp == NULL).", ENDUSER_SETUP_ERR_OUT_OF_MEMORY, ENDUSER_SETUP_ERR_FATAL);
}
esp_tcp *esp_tcp_data = (esp_tcp *) c_malloc(sizeof(esp_tcp));
if (esp_tcp_data == NULL)
{
ENDUSER_SETUP_ERROR("enduser_setup_http_start failed. Memory allocation failed (esp_udp == NULL).", ENDUSER_SETUP_ERR_OUT_OF_MEMORY, ENDUSER_SETUP_ERR_FATAL);
ENDUSER_SETUP_ERROR("http_start failed. Memory allocation failed (esp_udp == NULL).", ENDUSER_SETUP_ERR_OUT_OF_MEMORY, ENDUSER_SETUP_ERR_FATAL);
}
c_memset(state->espconn_http_tcp, 0, sizeof(struct espconn));
......@@ -575,41 +924,41 @@ static int enduser_setup_http_start(void)
err = espconn_regist_connectcb(state->espconn_http_tcp, enduser_setup_http_connectcb);
if (err != 0)
{
ENDUSER_SETUP_ERROR("enduser_setup_http_start failed. Couldn't add receive callback.", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_FATAL);
ENDUSER_SETUP_ERROR("http_start failed. Couldn't add receive callback.", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_FATAL);
}
err = espconn_accept(state->espconn_http_tcp);
if (err == ESPCONN_ISCONN)
{
ENDUSER_SETUP_ERROR("enduser_setup_http_start failed. Couldn't create connection, already listening for that connection.", ENDUSER_SETUP_ERR_SOCKET_ALREADY_OPEN, ENDUSER_SETUP_ERR_FATAL);
ENDUSER_SETUP_ERROR("http_start failed. Couldn't create connection, already listening for that connection.", ENDUSER_SETUP_ERR_SOCKET_ALREADY_OPEN, ENDUSER_SETUP_ERR_FATAL);
}
else if (err == ESPCONN_MEM)
{
ENDUSER_SETUP_ERROR("enduser_setup_http_start failed. Couldn't create connection, out of memory.", ENDUSER_SETUP_ERR_OUT_OF_MEMORY, ENDUSER_SETUP_ERR_FATAL);
ENDUSER_SETUP_ERROR("http_start failed. Couldn't create connection, out of memory.", ENDUSER_SETUP_ERR_OUT_OF_MEMORY, ENDUSER_SETUP_ERR_FATAL);
}
else if (err == ESPCONN_ARG)
{
ENDUSER_SETUP_ERROR("enduser_setup_http_start failed. Can't find connection from espconn argument", ENDUSER_SETUP_ERR_CONNECTION_NOT_FOUND, ENDUSER_SETUP_ERR_FATAL);
ENDUSER_SETUP_ERROR("http_start failed. Can't find connection from espconn argument", ENDUSER_SETUP_ERR_CONNECTION_NOT_FOUND, ENDUSER_SETUP_ERR_FATAL);
}
else if (err != 0)
{
ENDUSER_SETUP_ERROR("enduser_setup_http_start failed. Unknown error", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_FATAL);
ENDUSER_SETUP_ERROR("http_start failed. Unknown error", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_FATAL);
}
err = espconn_regist_time(state->espconn_http_tcp, 2, 0);
if (err == ESPCONN_ARG)
{
ENDUSER_SETUP_ERROR("enduser_setup_http_start failed. Unable to set TCP timeout.", ENDUSER_SETUP_ERR_CONNECTION_NOT_FOUND, ENDUSER_SETUP_ERR_NONFATAL | ENDUSER_SETUP_ERR_NO_RETURN);
ENDUSER_SETUP_ERROR("http_start failed. Unable to set TCP timeout.", ENDUSER_SETUP_ERR_CONNECTION_NOT_FOUND, ENDUSER_SETUP_ERR_NONFATAL | ENDUSER_SETUP_ERR_NO_RETURN);
}
err = enduser_setup_http_load_payload();
if (err == 1)
{
ENDUSER_SETUP_DEBUG(state->lua_L, "enduser_setup_http_start info. Loaded backup HTML.");
ENDUSER_SETUP_DEBUG(lua_getstate(), "enduser_setup_http_start info. Loaded backup HTML.");
}
else if (err == 2)
{
ENDUSER_SETUP_ERROR("enduser_setup_http_start failed. Unable to allocate memory for HTTP payload.", ENDUSER_SETUP_ERR_OUT_OF_MEMORY, ENDUSER_SETUP_ERR_FATAL);
ENDUSER_SETUP_ERROR("http_start failed. Unable to allocate memory for HTTP payload.", ENDUSER_SETUP_ERR_OUT_OF_MEMORY, ENDUSER_SETUP_ERR_FATAL);
}
return 0;
......@@ -618,7 +967,7 @@ static int enduser_setup_http_start(void)
static void enduser_setup_http_stop(void)
{
ENDUSER_SETUP_DEBUG(state->lua_L, "enduser_setup_http_stop");
ENDUSER_SETUP_DEBUG(lua_getstate(), "enduser_setup_http_stop");
if (state != NULL && state->espconn_http_tcp != NULL)
{
......@@ -628,7 +977,7 @@ static void enduser_setup_http_stop(void)
static void enduser_setup_ap_stop(void)
{
ENDUSER_SETUP_DEBUG(state->lua_L, "enduser_setup_station_stop");
ENDUSER_SETUP_DEBUG(lua_getstate(), "enduser_setup_station_stop");
wifi_set_opmode(~SOFTAP_MODE & wifi_get_opmode());
}
......@@ -636,7 +985,7 @@ static void enduser_setup_ap_stop(void)
static void enduser_setup_ap_start(void)
{
ENDUSER_SETUP_DEBUG(state->lua_L, "enduser_setup_ap_start");
ENDUSER_SETUP_DEBUG(lua_getstate(), "enduser_setup_ap_start");
struct softap_config cnf;
c_memset(&(cnf), 0, sizeof(struct softap_config));
......@@ -646,33 +995,27 @@ static void enduser_setup_ap_start(void)
#endif
char ssid[] = ENDUSER_SETUP_AP_SSID;
int ssid_name_len = c_strlen(ENDUSER_SETUP_AP_SSID);
int ssid_name_len = c_strlen(ssid);
c_memcpy(&(cnf.ssid), ssid, ssid_name_len);
uint8_t mac[6];
wifi_get_macaddr(SOFTAP_IF, mac);
cnf.ssid[ssid_name_len] = '_';
cnf.ssid[ssid_name_len + 1] = (char) (65 + (0x0F & mac[3]));
cnf.ssid[ssid_name_len + 2] = (char) (65 + ((0xF0 & mac[3]) >> 4));
cnf.ssid[ssid_name_len + 3] = (char) (65 + (0x0F & mac[4]));
cnf.ssid[ssid_name_len + 4] = (char) (65 + ((0xF0 & mac[4]) >> 4));
cnf.ssid[ssid_name_len + 5] = (char) (65 + (0x0F & mac[5]));
cnf.ssid[ssid_name_len + 6] = (char) (65 + ((0xF0 & mac[5]) >> 4));
cnf.ssid_len = c_strlen(ssid) + 7;
c_sprintf(cnf.ssid + ssid_name_len + 1, "%02X%02X%02X", mac[3], mac[4], mac[5]);
cnf.ssid_len = ssid_name_len + 7;
cnf.channel = 1;
cnf.authmode = AUTH_OPEN;
cnf.ssid_hidden = 0;
cnf.max_connection = 5;
cnf.beacon_interval = 100;
wifi_set_opmode(SOFTAP_MODE | wifi_get_opmode()); // Changed behavior: Need to be in AP mode before AP config will save
wifi_set_opmode(STATIONAP_MODE);
wifi_softap_set_config(&cnf);
}
static void enduser_setup_dns_recv_callback(void *arg, char *recv_data, unsigned short recv_len)
{
ENDUSER_SETUP_DEBUG(state->lua_L, "enduser_setup_dns_recv_callback.");
ENDUSER_SETUP_DEBUG(lua_getstate(), "enduser_setup_dns_recv_callback.");
struct espconn *callback_espconn = arg;
struct ip_info ip_info;
......@@ -684,20 +1027,20 @@ static void enduser_setup_dns_recv_callback(void *arg, char *recv_data, unsigned
uint8_t if_mode = wifi_get_opmode();
if ((if_mode & SOFTAP_MODE) == 0)
{
ENDUSER_SETUP_ERROR_VOID("enduser_setup_dns_recv_callback failed. Interface mode not supported.", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_FATAL);
ENDUSER_SETUP_ERROR_VOID("dns_recv_callback failed. Interface mode not supported.", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_FATAL);
}
uint8_t if_index = (if_mode == STATION_MODE? STATION_IF : SOFTAP_IF);
if (wifi_get_ip_info(if_index , &ip_info) == false)
{
ENDUSER_SETUP_ERROR_VOID("enduser_setup_dns_recv_callback failed. Unable to get interface IP.", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_FATAL);
ENDUSER_SETUP_ERROR_VOID("dns_recv_callback failed. Unable to get interface IP.", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_FATAL);
}
char *dns_reply = (char *) c_malloc(dns_reply_len);
if (dns_reply == NULL)
{
ENDUSER_SETUP_ERROR_VOID("enduser_setup_dns_recv_callback failed. Failed to allocate memory.", ENDUSER_SETUP_ERR_OUT_OF_MEMORY, ENDUSER_SETUP_ERR_NONFATAL);
ENDUSER_SETUP_ERROR_VOID("dns_recv_callback failed. Failed to allocate memory.", ENDUSER_SETUP_ERR_OUT_OF_MEMORY, ENDUSER_SETUP_ERR_NONFATAL);
}
uint32_t insert_byte = 0;
......@@ -715,32 +1058,32 @@ static void enduser_setup_dns_recv_callback(void *arg, char *recv_data, unsigned
remot_info *pr = 0;
if (espconn_get_connection_info(callback_espconn, &pr, 0) != ESPCONN_OK)
{
ENDUSER_SETUP_ERROR_VOID("enduser_setup_dns_recv_callback failed. Unable to get IP of UDP sender.", ENDUSER_SETUP_ERR_CONNECTION_NOT_FOUND, ENDUSER_SETUP_ERR_FATAL);
ENDUSER_SETUP_ERROR_VOID("dns_recv_callback failed. Unable to get IP of UDP sender.", ENDUSER_SETUP_ERR_CONNECTION_NOT_FOUND, ENDUSER_SETUP_ERR_FATAL);
}
callback_espconn->proto.udp->remote_port = pr->remote_port;
os_memmove(callback_espconn->proto.udp->remote_ip, pr->remote_ip, 4);
int8_t err;
err = espconn_sent(callback_espconn, dns_reply, dns_reply_len);
err = espconn_send(callback_espconn, dns_reply, dns_reply_len);
c_free(dns_reply);
if (err == ESPCONN_MEM)
{
ENDUSER_SETUP_ERROR_VOID("enduser_setup_dns_recv_callback failed. Failed to allocate memory for send.", ENDUSER_SETUP_ERR_OUT_OF_MEMORY, ENDUSER_SETUP_ERR_FATAL);
ENDUSER_SETUP_ERROR_VOID("dns_recv_callback failed. Failed to allocate memory for send.", ENDUSER_SETUP_ERR_OUT_OF_MEMORY, ENDUSER_SETUP_ERR_FATAL);
}
else if (err == ESPCONN_ARG)
{
ENDUSER_SETUP_ERROR_VOID("enduser_setup_dns_recv_callback failed. Can't execute transmission.", ENDUSER_SETUP_ERR_CONNECTION_NOT_FOUND, ENDUSER_SETUP_ERR_FATAL);
ENDUSER_SETUP_ERROR_VOID("dns_recv_callback failed. Can't execute transmission.", ENDUSER_SETUP_ERR_CONNECTION_NOT_FOUND, ENDUSER_SETUP_ERR_FATAL);
}
else if (err != 0)
{
ENDUSER_SETUP_ERROR_VOID("enduser_setup_dns_recv_callback failed. espconn_send failed", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_FATAL);
ENDUSER_SETUP_ERROR_VOID("dns_recv_callback failed. espconn_send failed", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_FATAL);
}
}
static void enduser_setup_free(void)
{
ENDUSER_SETUP_DEBUG(state->lua_L, "enduser_setup_free");
ENDUSER_SETUP_DEBUG(lua_getstate(), "enduser_setup_free");
if (state == NULL)
{
......@@ -765,6 +1108,9 @@ static void enduser_setup_free(void)
c_free(state->espconn_http_tcp);
}
c_free(state->http_payload_data);
free_scan_listeners ();
c_free(state);
state = NULL;
}
......@@ -772,22 +1118,22 @@ static void enduser_setup_free(void)
static int enduser_setup_dns_start(void)
{
ENDUSER_SETUP_DEBUG(state->lua_L, "enduser_setup_dns_start");
ENDUSER_SETUP_DEBUG(lua_getstate(), "enduser_setup_dns_start");
if (state->espconn_dns_udp != NULL)
{
ENDUSER_SETUP_ERROR("enduser_setup_dns_start failed. Appears to already be started (espconn_dns_udp != NULL).", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_FATAL);
ENDUSER_SETUP_ERROR("dns_start failed. Appears to already be started (espconn_dns_udp != NULL).", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_FATAL);
}
state->espconn_dns_udp = (struct espconn *) c_malloc(sizeof(struct espconn));
if (state->espconn_dns_udp == NULL)
{
ENDUSER_SETUP_ERROR("enduser_setup_dns_start failed. Memory allocation failed (espconn_dns_udp == NULL).", ENDUSER_SETUP_ERR_OUT_OF_MEMORY, ENDUSER_SETUP_ERR_FATAL);
ENDUSER_SETUP_ERROR("dns_start failed. Memory allocation failed (espconn_dns_udp == NULL).", ENDUSER_SETUP_ERR_OUT_OF_MEMORY, ENDUSER_SETUP_ERR_FATAL);
}
esp_udp *esp_udp_data = (esp_udp *) c_malloc(sizeof(esp_udp));
if (esp_udp_data == NULL)
{
ENDUSER_SETUP_ERROR("enduser_setup_dns_start failed. Memory allocation failed (esp_udp == NULL).", ENDUSER_SETUP_ERR_OUT_OF_MEMORY, ENDUSER_SETUP_ERR_FATAL);
ENDUSER_SETUP_ERROR("dns_start failed. Memory allocation failed (esp_udp == NULL).", ENDUSER_SETUP_ERR_OUT_OF_MEMORY, ENDUSER_SETUP_ERR_FATAL);
}
c_memset(state->espconn_dns_udp, 0, sizeof(struct espconn));
......@@ -801,21 +1147,21 @@ static int enduser_setup_dns_start(void)
err = espconn_regist_recvcb(state->espconn_dns_udp, enduser_setup_dns_recv_callback);
if (err != 0)
{
ENDUSER_SETUP_ERROR("enduser_setup_dns_start failed. Couldn't add receive callback, unknown error.", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_FATAL);
ENDUSER_SETUP_ERROR("dns_start failed. Couldn't add receive callback, unknown error.", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_FATAL);
}
err = espconn_create(state->espconn_dns_udp);
if (err == ESPCONN_ISCONN)
{
ENDUSER_SETUP_ERROR("enduser_setup_dns_start failed. Couldn't create connection, already listening for that connection.", ENDUSER_SETUP_ERR_SOCKET_ALREADY_OPEN, ENDUSER_SETUP_ERR_FATAL);
ENDUSER_SETUP_ERROR("dns_start failed. Couldn't create connection, already listening for that connection.", ENDUSER_SETUP_ERR_SOCKET_ALREADY_OPEN, ENDUSER_SETUP_ERR_FATAL);
}
else if (err == ESPCONN_MEM)
{
ENDUSER_SETUP_ERROR("enduser_setup_dns_start failed. Couldn't create connection, out of memory.", ENDUSER_SETUP_ERR_OUT_OF_MEMORY, ENDUSER_SETUP_ERR_FATAL);
ENDUSER_SETUP_ERROR("dns_start failed. Couldn't create connection, out of memory.", ENDUSER_SETUP_ERR_OUT_OF_MEMORY, ENDUSER_SETUP_ERR_FATAL);
}
else if (err != 0)
{
ENDUSER_SETUP_ERROR("enduser_setup_dns_start failed. Couldn't create connection, unknown error.", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_FATAL);
ENDUSER_SETUP_ERROR("dns_start failed. Couldn't create connection, unknown error.", ENDUSER_SETUP_ERR_UNKOWN_ERROR, ENDUSER_SETUP_ERR_FATAL);
}
return 0;
......@@ -824,7 +1170,7 @@ static int enduser_setup_dns_start(void)
static void enduser_setup_dns_stop(void)
{
ENDUSER_SETUP_DEBUG(state->lua_L, "enduser_setup_dns_stop");
ENDUSER_SETUP_DEBUG(lua_getstate(), "enduser_setup_dns_stop");
if (state->espconn_dns_udp != NULL)
{
......@@ -839,19 +1185,18 @@ static int enduser_setup_init(lua_State *L)
if (state != NULL)
{
enduser_setup_error(L, "enduser_setup_init failed. Appears to already be started.", ENDUSER_SETUP_ERR_UNKOWN_ERROR);
enduser_setup_error(L, "init failed. Appears to already be started.", ENDUSER_SETUP_ERR_UNKOWN_ERROR);
return ENDUSER_SETUP_ERR_UNKOWN_ERROR;
}
state = (enduser_setup_state_t *) os_malloc(sizeof(enduser_setup_state_t));
state = (enduser_setup_state_t *) os_zalloc(sizeof(enduser_setup_state_t));
if (state == NULL)
{
enduser_setup_error(L, "enduser_setup_init failed. Unable to allocate memory.", ENDUSER_SETUP_ERR_OUT_OF_MEMORY);
enduser_setup_error(L, "init failed. Unable to allocate memory.", ENDUSER_SETUP_ERR_OUT_OF_MEMORY);
return ENDUSER_SETUP_ERR_OUT_OF_MEMORY;
}
c_memset(state, 0, sizeof(enduser_setup_state_t));
state->lua_L = L;
state->lua_connected_cb_ref = LUA_NOREF;
state->lua_err_cb_ref = LUA_NOREF;
state->lua_dbg_cb_ref = LUA_NOREF;
......@@ -890,39 +1235,50 @@ static int enduser_setup_init(lua_State *L)
}
static int enduser_setup_manual (lua_State *L)
{
if (!lua_isnoneornil (L, 1))
manual = lua_toboolean (L, 1);
lua_pushboolean (L, manual);
return 1;
}
static int enduser_setup_start(lua_State *L)
{
ENDUSER_SETUP_DEBUG(L, "enduser_setup_start");
if(enduser_setup_init(L))
goto failed;
if (!manual)
{
enduser_setup_stop(L);
return 0;
enduser_setup_check_station_start();
enduser_setup_ap_start();
}
enduser_setup_check_station_start();
enduser_setup_ap_start();
if(enduser_setup_dns_start())
{
enduser_setup_stop(L);
return 0;
}
goto failed;
if(enduser_setup_http_start())
{
enduser_setup_stop(L);
return 0;
}
goto failed;
goto out;
failed:
enduser_setup_stop(L);
out:
return 0;
}
static int enduser_setup_stop(lua_State* L)
{
enduser_setup_check_station_stop();
enduser_setup_ap_stop();
if (!manual)
{
enduser_setup_check_station_stop();
enduser_setup_ap_stop();
}
enduser_setup_dns_stop();
enduser_setup_http_stop();
enduser_setup_free();
......@@ -932,6 +1288,7 @@ static int enduser_setup_stop(lua_State* L)
static const LUA_REG_TYPE enduser_setup_map[] = {
{ LSTRKEY( "manual" ), LFUNCVAL( enduser_setup_manual )},
{ LSTRKEY( "start" ), LFUNCVAL( enduser_setup_start )},
{ LSTRKEY( "stop" ), LFUNCVAL( enduser_setup_stop )},
{ LNILKEY, LNILVAL}
......
......@@ -6,6 +6,23 @@ This module provides a simple way of configuring ESP8266 chips without using a s
After running [`enduser_setup.start()`](#enduser_setupstart) a portal like the above can be accessed through a wireless network called SetupGadget_XXXXXX. The portal is used to submit the credentials for the WiFi of the enduser.
After an IP address has been successfully obtained this module will stop as if [`enduser_setup.stop()`](#enduser_setupstop) had been called.
## enduser_setup.manual()
Controls whether manual AP configuration is used.
By default the `enduser_setup` module automatically configures an open access point when starting, and stops it when the device has been successfully joined to a WiFi network. If manual mode has been enabled, neither of this is done. The device must be manually configured for `wifi.SOFTAP` mode prior to calling `enduser_setup.start()`. Additionally, the portal is not stopped after the device has successfully joined to a WiFi network.
Most importantly, *the `onConfigured()` callback is not supported in manual mode*. This limitation may disappear in the future.
#### Syntax
`enduser_setup.manual([on_off])`
#### Parameters
- `on_off` a boolean value indicating whether to use manual mode; if not given, the function only returns the current setting.
#### Returns
The current setting, true if manual mode is enabled, false if it is not.
## enduser_setup.start()
Starts the captive portal.
......@@ -44,4 +61,4 @@ Stops the captive portal.
none
#### Returns
`nil`
\ No newline at end of file
`nil`
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment