Merge pull request #30 from MightyPork/wifi-redesign

Settings overhaul & exposed many new wifi settings
pull/111/merge
Ondřej Hruška 8 years ago committed by GitHub
commit 9a9ed1445a
  1. 17
      CMakeLists.txt
  2. 2
      Makefile
  3. 29
      build_web.sh
  4. 3
      esphttpdconfig.mk
  5. 2
      html_orig/.gitignore
  6. 62
      html_orig/_debug_replacements.php
  7. 0
      html_orig/_env.php
  8. 3
      html_orig/_env.php.example
  9. 44
      html_orig/_pages.php
  10. 80
      html_orig/about.html
  11. 71
      html_orig/base.php
  12. 37
      html_orig/build_html.php
  13. 850
      html_orig/css/app.css
  14. 1284
      html_orig/css/app.css.bak
  15. 21
      html_orig/dump_js_lang.php
  16. BIN
      html_orig/fontello/fontello-9ba19eb0.zip
  17. 21
      html_orig/index.php
  18. 536
      html_orig/js/app.js
  19. 82
      html_orig/jssrc/appcommon.js
  20. 5
      html_orig/jssrc/chibi.js
  21. 7
      html_orig/jssrc/lang.js
  22. 32
      html_orig/jssrc/notif.js
  23. 6
      html_orig/jssrc/term.js
  24. 44
      html_orig/jssrc/utils.js
  25. 276
      html_orig/jssrc/wifi.js
  26. 140
      html_orig/lang/en.php
  27. 6
      html_orig/packjs.sh
  28. 20
      html_orig/pages/_cfg_menu.php
  29. 31
      html_orig/pages/_head.php
  30. 9
      html_orig/pages/_tail.php
  31. 69
      html_orig/pages/about.php
  32. 34
      html_orig/pages/cfg_admin.php
  33. 71
      html_orig/pages/cfg_app.php
  34. 95
      html_orig/pages/cfg_network.php
  35. 108
      html_orig/pages/cfg_wifi.php
  36. 53
      html_orig/pages/cfg_wifi_conn.php
  37. 37
      html_orig/pages/help.php
  38. 37
      html_orig/pages/term.php
  39. 57
      html_orig/sass/_fontello-embedded.scss
  40. 178
      html_orig/sass/_layout.scss
  41. 29
      html_orig/sass/app.scss
  42. 11
      html_orig/sass/form/_buttons.scss
  43. 2
      html_orig/sass/form/_fancy_button_mixins.scss
  44. 46
      html_orig/sass/form/_form_elements.scss
  45. 71
      html_orig/sass/form/_form_layout.scss
  46. 2
      html_orig/sass/form/_index.scss
  47. 36
      html_orig/sass/layout/_base.scss
  48. 137
      html_orig/sass/layout/_box.scss
  49. 50
      html_orig/sass/layout/_content.scss
  50. 9
      html_orig/sass/layout/_index.scss
  51. 18
      html_orig/sass/layout/_loader.scss
  52. 110
      html_orig/sass/layout/_menu.scss
  53. 19
      html_orig/sass/layout/_modal.scss
  54. 22
      html_orig/sass/layout/_outer-wrap.scss
  55. 1
      html_orig/sass/pages/_about.scss
  56. 29
      html_orig/sass/pages/_term.scss
  57. 145
      html_orig/sass/pages/_wifi.scss
  58. 68
      html_orig/sass/utils/_background-tiling.scss
  59. 3
      html_orig/sass/utils/_index.scss
  60. 34
      html_orig/sass/utils/_misc.scss
  61. 26
      html_orig/sass/utils/_pointer.scss
  62. 2
      html_orig/server.sh
  63. 53
      html_orig/term.html
  64. 17
      html_orig/term_test.php
  65. 89
      html_orig/wifi.html
  66. 26
      html_orig/wifi_conn.html
  67. 15
      html_orig/wifi_test.php
  68. 25
      include/helpers.h
  69. 2
      libesphttpd
  70. 178
      user/cgi_appcfg.c
  71. 9
      user/cgi_appcfg.h
  72. 32
      user/cgi_main.c
  73. 251
      user/cgi_network.c
  74. 9
      user/cgi_network.h
  75. 74
      user/cgi_persist.c
  76. 10
      user/cgi_persist.h
  77. 585
      user/cgi_wifi.c
  78. 13
      user/cgi_wifi.h
  79. 14
      user/io.c
  80. 171
      user/persist.c
  81. 55
      user/persist.h
  82. 42
      user/routes.c
  83. 51
      user/screen.c
  84. 34
      user/screen.h
  85. 54
      user/user_main.c
  86. 197
      user/wifimgr.c
  87. 57
      user/wifimgr.h

@ -36,7 +36,6 @@ set(SOURCE_FILES
libesphttpd/include/espmissingincludes.h
libesphttpd/include/espfs.h
libesphttpd/include/esp8266.h
libesphttpd/include/cgiwifi.h
libesphttpd/include/cgiwebsocket.h
libesphttpd/include/cgiflash.h
libesphttpd/include/captdns.h
@ -54,7 +53,6 @@ set(SOURCE_FILES
libesphttpd/lib/heatshrink/heatshrink_decoder.c
libesphttpd/lib/heatshrink/heatshrink.c
libesphttpd/mkupgimg/mkupgimg.c
libesphttpd/util/cgiwifi.c
libesphttpd/util/cgiwebsocket.c
libesphttpd/util/cgiflash.c
libesphttpd/util/captdns.c
@ -93,6 +91,14 @@ set(SOURCE_FILES
include/ets_sys_extra.h
user/io.c
user/io.h
user/cgi_wifi.c
user/cgi_wifi.h
user/cgi_persist.c
user/cgi_persist.h
user/cgi_network.c
user/cgi_network.h
user/cgi_appcfg.c
user/cgi_appcfg.h
user/cgi_ping.c
user/cgi_reset.c
user/uart_driver.c
@ -113,7 +119,11 @@ set(SOURCE_FILES
user/cgi_sockets.h
user/ansi_parser_callbacks.c
user/ansi_parser_callbacks.h
user/user_main.h)
user/user_main.h
user/wifimgr.c
user/wifimgr.h
user/persist.c
user/persist.h include/helpers.h)
include_directories(include)
include_directories(user)
@ -132,6 +142,7 @@ add_definitions(
-DICACHE_FLASH_ATTR=
-DICACHE_RODATA_ATTR=
-DFLAG_GZIP=2
-DADMIN_PASSWORD="asdf"
-DESPFS_HEATSHRINK)
add_executable(esp_vt100_firmware ${SOURCE_FILES})

@ -65,7 +65,7 @@ LIBS += esphttpd
# compiler flags using during compilation of source files
CFLAGS = -Os -ggdb -std=gnu99 -Werror -Wpointer-arith -Wundef -Wall -Wl,-EL -fno-inline-functions \
-nostdlib -mlongcalls -mtext-section-literals -D__ets__ -DICACHE_FLASH \
-Wno-address -Wno-unused -DHTTPD_MAX_BACKLOG_SIZE=8192
-Wno-address -Wno-unused -DHTTPD_MAX_BACKLOG_SIZE=8192 -DADMIN_PASSWORD=$(ADMIN_PASSWORD)
# linker flags used to generate the main object file
LDFLAGS = -nostdlib -Wl,--no-check-sections -u call_user_start -Wl,-static

@ -7,24 +7,15 @@ mkdir -p html/img
mkdir -p html/js
mkdir -p html/css
# Join scripts
DD=html_orig/jssrc
cat $DD/chibi.js \
$DD/utils.js \
$DD/modal.js \
$DD/appcommon.js \
$DD/term.js \
$DD/wifi.js > html/js/app.js
cd html_orig
sh ./packjs.sh
php ./build_html.php
cd ..
sass --sourcemap=none html_orig/sass/app.scss html_orig/css/app.css
cp html_orig/js/app.js html/js/
# No need to compress CSS and JS now, we run YUI on it later
cp html_orig/css/app.css html/css/app.css
cp html_orig/term.html html/term.tpl
cp html_orig/wifi.html html/wifi.tpl
cp html_orig/about.html html/about.tpl
cp html_orig/help.html html/help.tpl
cp html_orig/wifi_conn.html html/wifi_conn.tpl
cp html_orig/img/loader.gif html/img/loader.gif
cp html_orig/img/cvut.svg html/img/cvut.svg
cp html_orig/favicon.ico html/favicon.ico
sass html_orig/sass/app.scss html/css/app.css
rm html/css/app.css.map
cp html_orig/img/* html/img/
cp html_orig/favicon.ico html/favicon.ico

@ -37,3 +37,6 @@ OUTPUT_TYPE = combined
# SPI flash size, in K
ESP_SPI_FLASH_SIZE_K = 1024
# Admin password, used to store settings to flash as defaults
ADMIN_PASSWORD = "19738426"

@ -1 +1 @@
_test_env.php
pages/_test_env.php

@ -0,0 +1,62 @@
<?php
/**
* Those replacements are done by the development server to test it locally
* without esphttpd. This is needed mainly for places where the replacements
* are given to JavaScript, to avoid syntax errors with %%
*/
return [
'%term_title%' => 'ESP8266 Wireless Terminal',
'%btn1%' => '1',
'%btn2%' => '2',
'%btn3%' => '3',
'%btn4%' => '4',
'%btn5%' => '5',
'%screenData%' => '{
"w": 26, "h": 10,
"x": 0, "y": 0,
"cv": 1,
"screen": "70 t259"
}',
'%ap_enable%' => '1',
'%tpw%' => '60',
'%ap_channel%' => '7',
'%ap_ssid%' => 'ESP-123456',
'%ap_password%' => 'Passw0rd!',
'%ap_hidden%' => '0',
'%sta_ssid%' => 'Chlivek',
'%sta_password%' => 'windows XP is The Best',
'%sta_active_ip%' => '1.2.3.4',
'%sta_active_ssid%' => 'Chlivek',
'%sta_enable%' => '1',
'%opmode%' => '3',
'%vers_fw%' => '1.2.3',
'%date%' => date('Y-m-d'),
'%time%' => date('G:i'),
'%vers_httpd%' => '4.5.6',
'%vers_sdk%' => '1.52',
'%githubrepo%' => 'https://github.com/MightyPork/esp-vt100-firmware',
'%ap_dhcp_time%' => '120',
'%ap_dhcp_start%' => '192.168.4.100',
'%ap_dhcp_end%' => '192.168.4.200',
'%ap_addr_ip%' => '192.168.4.1',
'%ap_addr_mask%' => '255.255.255.0',
'%sta_dhcp_enable%' => '1',
'%sta_addr_ip%' => '192.168.0.33',
'%sta_addr_mask%' => '255.255.255.0',
'%sta_addr_gw%' => '192.168.0.1',
'%sta_mac%' => 'ab:cd:ef:01:23:45',
'%ap_mac%' => '01:23:45:ab:cd:ef',
'%term_width%' => '26',
'%term_height%' => '10',
'%default_bg%' => '0',
'%default_fg%' => '7',
];

@ -0,0 +1,3 @@
<?php
define("ESP_IP", "192.168.0.19");

@ -0,0 +1,44 @@
<?php
$pages = [];
if (! function_exists('pg')) {
/** Add a page */
function pg($key, $bc, $icon, $path, $titleKey = null)
{
global $pages;
$pages[$key] = (object) [
'key' => $key,
'bodyclass' => $bc,
'path' => $path,
'icon' => $icon ? "icn-$icon" : '',
'label' => tr("menu.$key"),
'title' => $titleKey ? tr($titleKey) : tr("menu.$key"),
];
}
}
pg('cfg_wifi', 'cfg', 'wifi', '/cfg/wifi');
pg('cfg_wifi_conn', '', '', '/cfg/wifi/connecting');
pg('wifi_connstatus', 'api', '', '/cfg/wifi/connstatus');
pg('wifi_set', 'api', '', '/cfg/wifi/set');
pg('wifi_scan', 'api', '', '/cfg/wifi/scan');
pg('cfg_network', 'cfg', 'network', '/cfg/network');
pg('network_set', 'api', '', '/cfg/network/set');
pg('cfg_app', 'cfg', 'terminal', '/cfg/app');
pg('app_set', 'api', '', '/cfg/app/set');
pg('cfg_admin', 'cfg', 'persist', '/cfg/admin');
pg('write_defaults', 'api', '', '/cfg/admin/write_defaults');
pg('restore_defaults', 'api', '', '/cfg/admin/restore_defaults');
pg('restore_hard', 'api', '', '/cfg/admin/restore_hard');
pg('help', 'cfg page-help', 'help', '/help');
pg('about', 'cfg page-about', 'about', '/about');
pg('term', 'term', '', '/', 'title.term');
// ajax API
return $pages;

@ -1,80 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>About - ESP8266 Remote Terminal</title>
<meta name="viewport" content="width=device-width,shrink-to-fit=no,user-scalable=no,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0">
<link rel="stylesheet" href="/css/app.css">
<script src="/js/app.js"></script>
</head>
<body class="page-about">
<h1 onclick="location.href='/'">About</h1>
<div class="Box">
<img src="/img/cvut.svg" id="logo" class="mq-tablet-min">
<h2>ESP8266 Remote Terminal</h2>
<img src="/img/cvut.svg" id="logo2" class="mq-phone">
<p>&copy; Ondřej Hruška, 2017 &lt;<a href="mailto:ondra@ondrovo.com">ondra@ondrovo.com</a>&gt;</p>
<p><a href="http://measure.feld.cvut.cz/" target="blank">Katedra měření, FEL ČVUT</a><br>Department of Measurement, FEE CTU</p>
</div>
<div class="Box">
<h2>Firmware</h2>
<table>
<tr>
<th>Firmware</th>
<td>v%vers_fw%, build <i>%date%</i> at <i>%time%</i></td>
</tr>
<tr>
<th>libesphttpd</th>
<td>v%vers_httpd%</td>
</tr>
<tr>
<th>ESP&nbsp;IoT&nbsp;SDK</th>
<td>v%vers_sdk%</td>
</tr>
</table>
</div>
<div class="Box">
<h2>Issues</h2>
<p>
Please report any issues to the <a href="%githubrepo%/issues">bugtracker</a> or send them by e-mail.
</p>
<p>
Firmware updates can be downloaded from the <a href="%githubrepo%/releases">releases page</a>.
Flash the images using <a href="https://github.com/espressif/esptool">esptool</a>.
</p>
</div>
<div class="Box">
<h2>Contributing</h2>
<p>
Submit your improvements and ideas to the project on <a href="%githubrepo%">GitHub</a>.<br>
</p>
</div>
<div class="Box">
<h2>Acknowledgements</h2>
<p>
The webserver is based on a <a href="https://github.com/MightyPork/libesphttpd">fork</a> of the
<a href="https://github.com/Spritetm/esphttpd">esphttpd</a> library by Jeroen Domburg (Sprite_tm).
</p>
<p>
Using (modified) JS library <a href="https://github.com/kylebarrow/chibi">chibi.js</a> by Kyle Barrow as a lightweight jQuery alternative.
</p>
</div>
<nav id="botnav">
<a href="/">Terminal</a><!--
--><a href="/help">Help</a><!--
--><a href="/wifi">WiFi config</a>
</nav>
</body>
</html>

@ -0,0 +1,71 @@
<?php
/**
* The common stuff required by both index.php and build_html.php
* this must be required_once
*/
if (defined('BASE_INITED')) return;
define('BASE_INITED', true);
if (!empty($argv[1])) {
parse_str($argv[1], $_GET);
}
if (!file_exists(__DIR__ . '/_env.php')) {
die("Copy <b>_env.php.example</b> to <b>_env.php</b> and check the settings inside!");
}
require_once __DIR__ . '/_env.php';
$prod = defined('STDIN');
define('DEBUG', !$prod);
$root = DEBUG ? json_encode(ESP_IP) : 'location.host';
define('JS_WEB_ROOT', $root);
define('LOCALE', isset($_GET['locale']) ? $_GET['locale'] : 'en');
$_messages = require(__DIR__ . '/lang/' . LOCALE . '.php');
$_pages = require(__DIR__ . '/_pages.php');
define('APP_NAME', 'ESPTerm');
/** URL (dev or production) */
function url($name, $relative = false)
{
global $_pages;
if ($relative) return $_pages[$name]->path;
if (DEBUG) return "/index.php?page=$name";
else return $_pages[$name]->path;
}
/** URL label for a button */
function label($name)
{
global $_pages;
return $_pages[$name]->label;
}
function e($s)
{
return htmlspecialchars($s, ENT_HTML5 | ENT_QUOTES);
}
function tr($key)
{
global $_messages;
return isset($_messages[$key]) ? $_messages[$key] : ('??' . $key . '??');
}
/** Like eval, but allows <?php and ?> */
function include_str($code)
{
$tmp = tmpfile();
$tmpf = stream_get_meta_data($tmp);
$tmpf = $tmpf ['uri'];
fwrite($tmp, $code);
$ret = include($tmpf);
fclose($tmp);
return $ret;
}

@ -0,0 +1,37 @@
<?php
require_once __DIR__ . '/base.php';
function process_html($s) {
$pattern = '/<!--(.*)-->/Uis';
$s = preg_replace($pattern, '', $s);
$pattern = '/(?:(?:\/\*(?:[^*]|(?:\*+[^*\/]))*\*+\/)|(?:(?<!\:|\\\|\'|\")\/\/.*))/';
$s = preg_replace($pattern, '', $s);
$pattern = '/\s+/s';
$s = preg_replace($pattern, ' ', $s);
return $s;
}
ob_start();
foreach($_pages as $_k => $p) {
if ($p->bodyclass == 'api') continue;
echo "Generating: $_k ($p->title)\n";
$_GET['page'] = $_k;
ob_flush(); // print the message
ob_clean(); // clean up
include(__DIR__ . '/index.php');
$s = ob_get_contents(); // grab the output
// remove newlines and comments
// as tests have shown, it saves just a couple kilobytes,
// making it not a very big improvement at the expense of ugly html.
// $s = process_html($s);
ob_clean(); // clean up
$of = __DIR__ . '/../html/' . $_k . '.tpl';
file_put_contents($of, $s); // write to a file
}
ob_flush();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,21 @@
<?php
/* This script is run on demand to generate JS version of tr() */
require_once __DIR__ . '/base.php';
$selected = [
'wifi.connected_ip_is',
'wifi.not_conn',
];
$out = [];
foreach ($selected as $key) {
$out[$key] = $_messages[$key];
}
file_put_contents(__DIR__. '/jssrc/lang.js',
"// Generated from PHP locale file\n" .
'var _tr = ' . json_encode($out, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE) . ";\n\n" .
"function tr(key) { return _tr[key] || '?'+key+'?'; }\n"
);

@ -0,0 +1,21 @@
<?php
require_once __DIR__ . '/base.php';
if (!isset($_GET['page'])) $_GET['page'] = 'term';
$_GET['PAGE_TITLE'] = $_pages[$_GET['page']]->title . ' :: ' . APP_NAME;
$_GET['BODYCLASS'] = $_pages[$_GET['page']]->bodyclass;
require __DIR__ . '/pages/_head.php';
$_pf = __DIR__ . '/pages/'.$_GET['page'].'.php';
if (file_exists($_pf)) {
$f = file_get_contents($_pf);
$reps = DEBUG ? require(__DIR__ . '/_debug_replacements.php') : [];
$str = str_replace(array_keys($reps), array_values($reps), $f);
include_str($str);
} else {
echo "404";
}
require __DIR__ . '/pages/_tail.php';

@ -393,8 +393,9 @@
return cb;
};
// Toggle class
cb.toggleClass = function (classes) {
classHelper(classes, 'toggle', nodes);
cb.toggleClass = function (classes, set) {
var method = ((typeof set === 'undefined') ? 'toggle' : (+set ? 'add' : 'remove'));
classHelper(classes, method, nodes);
return cb;
};
// Has class
@ -704,18 +705,27 @@
function mk(e) {return document.createElement(e)}
/** Find one by query */
function qq(s) {return document.querySelector(s)}
function qs(s) {return document.querySelector(s)}
/** Find all by query */
function qa(s) {return document.querySelectorAll(s)}
function qsa(s) {return document.querySelectorAll(s)}
/** Convert any to bool safely */
function bool(x) {
return (x === 1 || x === '1' || x === true || x === 'true');
}
function intval(x) {
return parseInt(x);
/**
* Filter 'spacebar' and 'return' from keypress handler,
* and when they're pressed, fire the callback.
* use $(...).on('keypress', cr(handler))
*/
function cr(hdl) {
return function(e) {
if (e.which == 10 || e.which == 13 || e.which == 32) {
hdl();
}
};
}
/** Extend an objects with options */
@ -738,23 +748,23 @@ function rgxe(str) {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
}
/** Format number to N decimal places, output as string */
function numfmt(x, places) {
var pow = Math.pow(10, places);
return Math.round(x*pow) / pow;
}
function estimateLoadTime(fs, n) {
return (1000/fs)*n+1500;
}
/** Get millisecond timestamp */
function msNow() {
return +(new Date);
}
/** Get ms elapsed since msNow() */
function msElapsed(start) {
return msNow() - start;
}
/** Shim for log base 10 */
Math.log10 = Math.log10 || function(x) {
return Math.log(x) / Math.LN10;
};
@ -795,6 +805,25 @@ String.prototype.format = function () {
return out;
};
/** HTML escape */
function e(str) {
return $.htmlEscape(str);
}
/** Check for undefined */
function undef(x) {
return typeof x == 'undefined';
}
/** Safe json parse */
function jsp() {
try {
return JSON.parse(e);
} catch(e) {
console.error(e);
return null;
}
}
/** Module for toggling a modal overlay */
(function () {
var modal = {};
@ -836,8 +865,73 @@ String.prototype.format = function () {
window.Modal = modal;
})();
(function (nt) {
var sel = '#notif';
var hideTmeo1; // timeout to start hiding (transition)
var hideTmeo2; // timeout to add the hidden class
nt.show = function (message, timeout) {
$(sel).html(message);
Modal.show(sel);
clearTimeout(hideTmeo1);
clearTimeout(hideTmeo2);
if (undef(timeout)) timeout = 2500;
hideTmeo1 = setTimeout(nt.hide, timeout);
};
nt.hide = function () {
var $m = $(sel);
$m.removeClass('visible');
hideTmeo2 = setTimeout(function () {
$m.addClass('hidden');
}, 250); // transition time
};
nt.init = function() {
$(sel).on('click', function() {
nt.hide(this);
});
};
})(window.Notify = {});
/** Global generic init */
$.ready(function () {
// Checkbox UI (checkbox CSS and hidden input with int value)
$('.Row.checkbox').forEach(function(x) {
var inp = x.querySelector('input');
var box = x.querySelector('.box');
$(box).toggleClass('checked', inp.value);
var hdl = function() {
inp.value = 1 - inp.value;
$(box).toggleClass('checked', inp.value)
};
$(x).on('click', hdl).on('keypress', cr(hdl));
});
// Expanding boxes on mobile
$('.Box.mobcol').forEach(function(x) {
var h = x.querySelector('h2');
var hdl = function() {
$(x).toggleClass('expanded');
};
$(h).on('click', hdl).on('keypress', cr(hdl));
});
qsa('form').forEach(function(x) {
$(x).on('keypress', function(e) {
if ((e.keyCode == 10 || e.keyCode == 13) && e.ctrlKey) {
x.submit();
}
})
});
// loader dots...
setInterval(function () {
$('.anim-dots').each(function (x) {
@ -850,12 +944,13 @@ $.ready(function () {
// flipping number boxes with the mouse wheel
$('input[type=number]').on('mousewheel', function(e) {
var val = +$(this).val();
var $this = $(this);
var val = +$this.val();
if (isNaN(val)) val = 1;
var step = +($(this).attr('step') || 1);
var min = +$(this).attr('min');
var max = +$(this).attr('max');
var step = +($this.attr('step') || 1);
var min = +$this.attr('min');
var max = +$this.attr('max');
if(e.wheelDelta > 0) {
val += step;
} else {
@ -864,28 +959,243 @@ $.ready(function () {
if (typeof min != 'undefined') val = Math.max(val, +min);
if (typeof max != 'undefined') val = Math.min(val, +max);
$(this).val(val);
$this.val(val);
if ("createEvent" in document) {
var evt = document.createEvent("HTMLEvents");
evt.initEvent("change", false, true);
$(this)[0].dispatchEvent(evt);
$this[0].dispatchEvent(evt);
} else {
$(this)[0].fireEvent("onchange");
$this[0].fireEvent("onchange");
}
e.preventDefault();
});
var errAt = location.search.indexOf('err=');
if (errAt !== -1 && qs('.Box.errors')) {
var errs = location.search.substr(errAt+4).split(',');
var hres = [];
errs.forEach(function(er) {
var lbl = qs('label[for="'+er+'"]');
if (lbl) {
lbl.classList.add('error');
hres.push(lbl.childNodes[0].textContent.trim().replace(/: ?$/, ''));
} else {
hres.push(er);
}
});
qs('.Box.errors .list').innerHTML = hres.join(', ');
qs('.Box.errors').classList.remove('hidden');
}
Modal.init();
Notify.init();
// remove tabindixes from h2 if wide
if (window.innerWidth > 550) {
qsa('.Box h2').forEach(function (x) {
x.removeAttribute('tabindex');
});
var br = qs('#brand');
br && br.removeAttribute('tabindex');
}
});
$._loader = function(vis) {
if(vis)
$('#loader').addClass('show');
else
$('#loader').removeClass('show');
$('#loader').toggleClass('show', vis);
};
// Generated from PHP locale file
var _tr = {
"wifi.connected_ip_is": "Connected, IP is ",
"wifi.not_conn": "Not connected."
};
function tr(key) { return _tr[key] || '?'+key+'?'; }
(function(w) {
var authStr = ['Open', 'WEP', 'WPA', 'WPA2', 'WPA/WPA2'];
var curSSID;
// Get XX % for a slider input
function rangePt(inp) {
return Math.round(((inp.value / inp.max)*100)) + '%';
}
// Display selected STA SSID etc
function selectSta(name, password, ip) {
$('#sta_ssid').val(name);
$('#sta_password').val(password);
$('#sta-nw').toggleClass('hidden', name.length == 0);
$('#sta-nw-nil').toggleClass('hidden', name.length > 0);
$('#sta-nw .essid').html(e(name));
var nopw = undef(password) || password.length == 0;
$('#sta-nw .x-passwd').html(e(password));
$('#sta-nw .passwd').toggleClass('hidden', nopw);
$('#sta-nw .nopasswd').toggleClass('hidden', !nopw);
$('#sta-nw .ip').html(ip.length>0 ? tr('wifi.connected_ip_is')+ip : tr('wifi.not_conn'));
}
function submitPskModal(e, open) {
var passwd = $('#conn-passwd').val();
var ssid = $('#conn-ssid').val();
if (open || passwd.length) {
$('#sta_password').val(passwd);
$('#sta_ssid').val(ssid);
selectSta(ssid, passwd, '');
}
if (e) e.preventDefault();
Modal.hide('#psk-modal');
return false;
}
/** Update display for received response */
function onScan(resp, status) {
//var ap_json = {
// "result": {
// "inProgress": "0",
// "APs": [
// {"essid": "Chlivek", "bssid": "88:f7:c7:52:b3:99", "rssi": "204", "enc": "4", "channel": "1"},
// {"essid": "TyNikdy", "bssid": "5c:f4:ab:0d:f1:1b", "rssi": "164", "enc": "3", "channel": "1"},
// ]
// }
//};
if (status != 200) {
// bad response
rescan(5000); // wait 5sm then retry
return;
}
try {
resp = JSON.parse(resp);
} catch (e) {
console.log(e);
rescan(5000);
return;
}
var done = !bool(resp.result.inProgress) && (resp.result.APs.length > 0);
rescan(done ? 15000 : 1000);
if (!done) return; // no redraw yet
// clear the AP list
var $list = $('#ap-list');
// remove old APs
$('#ap-list .AP').remove();
$list.toggleClass('hidden', !done);
$('#ap-loader').toggleClass('hidden', done);
// scan done
resp.result.APs.sort(function (a, b) {
return b.rssi - a.rssi;
}).forEach(function (ap) {
ap.enc = parseInt(ap.enc);
if (ap.enc > 4) return; // hide unsupported auths
WiFi.scan_url = '/cfg/wifi/scan';
var item = mk('div');
var $item = $(item)
.data('ssid', ap.essid)
.data('pwd', ap.enc)
.attr('tabindex', 0)
.addClass('AP');
// mark current SSID
if (ap.essid == curSSID) {
$item.addClass('selected');
}
var inner = mk('div');
$(inner).addClass('inner')
.htmlAppend('<div class="rssi">{0}</div>'.format(ap.rssi_perc))
.htmlAppend('<div class="essid" title="{0}">{0}</div>'.format($.htmlEscape(ap.essid)))
.htmlAppend('<div class="auth">{0}</div>'.format(authStr[ap.enc]));
$item.on('click', function () {
var $th = $(this);
var ssid = $th.data('ssid');
$('#conn-ssid').val(ssid);
$('#conn-passwd').val('');
if (+$th.data('pwd')) {
// this AP needs a password
Modal.show('#psk-modal');
$('#conn-passwd')[0].focus();
} else {
//Modal.show('#reset-modal');
submitPskModal(null, true);
}
});
item.appendChild(inner);
$list[0].appendChild(item);
});
}
function startScanning() {
$('#ap-loader').removeClass('hidden');
$('#ap-scan').addClass('hidden');
$('#ap-loader .anim-dots').html('.');
scanAPs();
}
/** Ask the CGI what APs are visible (async) */
function scanAPs() {
$.get('http://'+_root+w.scan_url, onScan);
}
function rescan(time) {
setTimeout(scanAPs, time);
}
/** Set up the WiFi page */
function wifiInit(cfg) {
// Hide what should be hidden in this mode
cfg.mode = +cfg.mode;
$('#ap-noscan').toggleClass('hidden', cfg.mode != 2);
$('#ap-scan').toggleClass('hidden', cfg.mode == 2);
// Update slider value displays
$('.Row.range').forEach(function(x) {
var inp = x.querySelector('input');
var disp1 = x.querySelector('.x-disp1');
var disp2 = x.querySelector('.x-disp2');
var t = rangePt(inp);
$(disp1).html(t);
$(disp2).html(t);
$(inp).on('input', function() {
t = rangePt(inp);
$(disp1).html(t);
$(disp2).html(t);
});
});
// Forget STA credentials
$('#forget-sta').on('click', function() {
selectSta('', '', '');
return false;
});
selectSta(cfg.sta_ssid, cfg.sta_password, cfg.sta_active_ip);
curSSID = cfg.sta_active_ssid;
}
w.init = wifiInit;
w.startScanning = startScanning;
})(window.WiFi = {});
(function() {
/**
* Terminal module
@ -1023,7 +1333,7 @@ $._loader = function(vis) {
H = obj.h;
/* Build screen & show */
var e, cell, scr = qq('#screen');
var e, cell, scr = qs('#screen');
// Empty the screen node
while (scr.firstChild) scr.removeChild(scr.firstChild);
@ -1088,7 +1398,7 @@ $._loader = function(vis) {
console.warn("SOCKET CLOSED, code "+evt.code+". Reconnecting...");
setTimeout(function() {
init();
}, 2000);
}, 1000);
}
function onMessage(evt) {
@ -1169,7 +1479,7 @@ $._loader = function(vis) {
}
});
qa('#buttons button').forEach(function(s) {
qsa('#buttons button').forEach(function(s) {
s.addEventListener('click', function() {
sendBtnMsg(+this.dataset['n']);
});
@ -1187,183 +1497,5 @@ $._loader = function(vis) {
Term.init(obj);
Conn.init();
Input.init();
}
})();
/** Wifi page */
(function () {
var authStr = ['Open', 'WEP', 'WPA', 'WPA2', 'WPA/WPA2'];
var curSSID;
/** Update display for received response */
function onScan(resp, status) {
if (status != 200) {
// bad response
rescan(5000); // wait 5sm then retry
return;
}
resp = JSON.parse(resp);
var done = !bool(resp.result.inProgress) && (resp.result.APs.length > 0);
rescan(done ? 15000 : 1000);
if (!done) return; // no redraw yet
// clear the AP list
var $list = $('#ap-list');
// remove old APs
$('.AP').remove();
$list.toggle(done);
$('#ap-loader').toggle(!done);
// scan done
resp.result.APs.sort(function (a, b) {
return b.rssi - a.rssi;
}).forEach(function (ap) {
ap.enc = intval(ap.enc);
if (ap.enc > 4) return; // hide unsupported auths
var item = document.createElement('div');
var $item = $(item)
.data('ssid', ap.essid)
.data('pwd', ap.enc != 0)
.addClass('AP');
// mark current SSID
if (ap.essid == curSSID) {
$item.addClass('selected');
}
var inner = document.createElement('div');
$(inner).addClass('inner')
.htmlAppend('<div class="rssi">{0}</div>'.format(ap.rssi_perc))
.htmlAppend('<div class="essid" title="{0}">{0}</div>'.format($.htmlEscape(ap.essid)))
.htmlAppend('<div class="auth">{0}</div>'.format(authStr[ap.enc]));
$item.on('click', function () {
var $th = $(this);
// populate the form
$('#conn-essid').val($th.data('ssid'));
$('#conn-passwd').val(''); // clear
if ($th.data('pwd')) {
// this AP needs a password
Modal.show('#psk-modal');
} else {
Modal.show('#reset-modal');
$('#conn-form').submit();
}
});
item.appendChild(inner);
$list[0].appendChild(item);
});
}
/** Ask the CGI what APs are visible (async) */
function scanAPs() {
$.get('http://'+_root+'/wifi/scan', onScan);
}
function rescan(time) {
setTimeout(scanAPs, time);
}
/** Set up the WiFi page */
window.wifiInit = function (obj) {
//var ap_json = {
// "result": {
// "inProgress": "0",
// "APs": [
// {"essid": "Chlivek", "bssid": "88:f7:c7:52:b3:99", "rssi": "204", "enc": "4", "channel": "1"},
// {"essid": "TyNikdy", "bssid": "5c:f4:ab:0d:f1:1b", "rssi": "164", "enc": "3", "channel": "1"},
// ]
// }
//};
// Hide what should be hidden in this mode
$('.x-hide-'+obj.mode).addClass('hidden');
obj.mode = +obj.mode;
// Channel writable only in AP mode
if (obj.mode != 2) $('#channel').attr('readonly', 1);
curSSID = obj.staSSID;
// add SSID to the opmode field
if (curSSID) {
var box = $('#opmodebox');
box.html(box.html() + ' (' + curSSID + ')');
}
// hide IP if IP not received
if (!obj.staIP) $('.x-hide-noip').addClass('hidden');
// scan if not AP
if (obj.mode != 2) {
scanAPs();
}
$('#modeswitch').html([
'<a class="button" href="/wifi/setmode?mode=3">Client+AP</a>&nbsp;<a class="button" href="/wifi/setmode?mode=2">AP only</a>',
'<a class="button" href="/wifi/setmode?mode=3">Client+AP</a>',
'<a class="button" href="/wifi/setmode?mode=1">Client only</a>&nbsp;<a class="button" href="/wifi/setmode?mode=2">AP only</a>'
][obj.mode-1]);
};
window.wifiConn = function () {
var xhr = new XMLHttpRequest();
var abortTmeo;
function getStatus() {
xhr.open("GET", 'http://'+_root+"/wifi/connstatus");
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status >= 200 && xhr.status < 300) {
clearTimeout(abortTmeo);
var data = JSON.parse(xhr.responseText);
var done = false;
var msg = '...';
if (data.status == "idle") {
msg = "Preparing to connect";
}
else if (data.status == "success") {
msg = "Connected! Received IP " + data.ip + ".";
done = true;
}
else if (data.status == "working") {
msg = "Connecting to selected AP";
}
else if (data.status == "fail") {
msg = "Connection failed, check your password and try again.";
done = true;
}
$("#status").html(msg);
if (done) {
$('#backbtn').removeClass('hidden');
$('.anim-dots').addClass('hidden');
} else {
window.setTimeout(getStatus, 1000);
}
}
};
abortTmeo = setTimeout(function () {
xhr.abort();
$("#status").html("Telemetry lost, try reconnecting to the AP.");
$('#backbtn').removeClass('hidden');
$('.anim-dots').addClass('hidden');
}, 4000);
xhr.send();
}
getStatus();
};
})();

@ -1,5 +1,38 @@
/** Global generic init */
$.ready(function () {
// Checkbox UI (checkbox CSS and hidden input with int value)
$('.Row.checkbox').forEach(function(x) {
var inp = x.querySelector('input');
var box = x.querySelector('.box');
$(box).toggleClass('checked', inp.value);
var hdl = function() {
inp.value = 1 - inp.value;
$(box).toggleClass('checked', inp.value)
};
$(x).on('click', hdl).on('keypress', cr(hdl));
});
// Expanding boxes on mobile
$('.Box.mobcol').forEach(function(x) {
var h = x.querySelector('h2');
var hdl = function() {
$(x).toggleClass('expanded');
};
$(h).on('click', hdl).on('keypress', cr(hdl));
});
qsa('form').forEach(function(x) {
$(x).on('keypress', function(e) {
if ((e.keyCode == 10 || e.keyCode == 13) && e.ctrlKey) {
x.submit();
}
})
});
// loader dots...
setInterval(function () {
$('.anim-dots').each(function (x) {
@ -12,12 +45,13 @@ $.ready(function () {
// flipping number boxes with the mouse wheel
$('input[type=number]').on('mousewheel', function(e) {
var val = +$(this).val();
var $this = $(this);
var val = +$this.val();
if (isNaN(val)) val = 1;
var step = +($(this).attr('step') || 1);
var min = +$(this).attr('min');
var max = +$(this).attr('max');
var step = +($this.attr('step') || 1);
var min = +$this.attr('min');
var max = +$this.attr('max');
if(e.wheelDelta > 0) {
val += step;
} else {
@ -26,25 +60,51 @@ $.ready(function () {
if (typeof min != 'undefined') val = Math.max(val, +min);
if (typeof max != 'undefined') val = Math.min(val, +max);
$(this).val(val);
$this.val(val);
if ("createEvent" in document) {
var evt = document.createEvent("HTMLEvents");
evt.initEvent("change", false, true);
$(this)[0].dispatchEvent(evt);
$this[0].dispatchEvent(evt);
} else {
$(this)[0].fireEvent("onchange");
$this[0].fireEvent("onchange");
}
e.preventDefault();
});
var errAt = location.search.indexOf('err=');
if (errAt !== -1 && qs('.Box.errors')) {
var errs = location.search.substr(errAt+4).split(',');
var hres = [];
errs.forEach(function(er) {
var lbl = qs('label[for="'+er+'"]');
if (lbl) {
lbl.classList.add('error');
hres.push(lbl.childNodes[0].textContent.trim().replace(/: ?$/, ''));
} else {
hres.push(er);
}
});
qs('.Box.errors .list').innerHTML = hres.join(', ');
qs('.Box.errors').classList.remove('hidden');
}
Modal.init();
Notify.init();
// remove tabindixes from h2 if wide
if (window.innerWidth > 550) {
qsa('.Box h2').forEach(function (x) {
x.removeAttribute('tabindex');
});
var br = qs('#brand');
br && br.removeAttribute('tabindex');
}
});
$._loader = function(vis) {
if(vis)
$('#loader').addClass('show');
else
$('#loader').removeClass('show');
$('#loader').toggleClass('show', vis);
};

@ -393,8 +393,9 @@
return cb;
};
// Toggle class
cb.toggleClass = function (classes) {
classHelper(classes, 'toggle', nodes);
cb.toggleClass = function (classes, set) {
var method = ((typeof set === 'undefined') ? 'toggle' : (+set ? 'add' : 'remove'));
classHelper(classes, method, nodes);
return cb;
};
// Has class

@ -0,0 +1,7 @@
// Generated from PHP locale file
var _tr = {
"wifi.connected_ip_is": "Connected, IP is ",
"wifi.not_conn": "Not connected."
};
function tr(key) { return _tr[key] || '?'+key+'?'; }

@ -0,0 +1,32 @@
(function (nt) {
var sel = '#notif';
var hideTmeo1; // timeout to start hiding (transition)
var hideTmeo2; // timeout to add the hidden class
nt.show = function (message, timeout) {
$(sel).html(message);
Modal.show(sel);
clearTimeout(hideTmeo1);
clearTimeout(hideTmeo2);
if (undef(timeout)) timeout = 2500;
hideTmeo1 = setTimeout(nt.hide, timeout);
};
nt.hide = function () {
var $m = $(sel);
$m.removeClass('visible');
hideTmeo2 = setTimeout(function () {
$m.addClass('hidden');
}, 250); // transition time
};
nt.init = function() {
$(sel).on('click', function() {
nt.hide(this);
});
};
})(window.Notify = {});

@ -135,7 +135,7 @@
H = obj.h;
/* Build screen & show */
var e, cell, scr = qq('#screen');
var e, cell, scr = qs('#screen');
// Empty the screen node
while (scr.firstChild) scr.removeChild(scr.firstChild);
@ -281,7 +281,7 @@
}
});
qa('#buttons button').forEach(function(s) {
qsa('#buttons button').forEach(function(s) {
s.addEventListener('click', function() {
sendBtnMsg(+this.dataset['n']);
});
@ -299,5 +299,5 @@
Term.init(obj);
Conn.init();
Input.init();
}
};
})();

@ -2,18 +2,27 @@
function mk(e) {return document.createElement(e)}
/** Find one by query */
function qq(s) {return document.querySelector(s)}
function qs(s) {return document.querySelector(s)}
/** Find all by query */
function qa(s) {return document.querySelectorAll(s)}
function qsa(s) {return document.querySelectorAll(s)}
/** Convert any to bool safely */
function bool(x) {
return (x === 1 || x === '1' || x === true || x === 'true');
}
function intval(x) {
return parseInt(x);
/**
* Filter 'spacebar' and 'return' from keypress handler,
* and when they're pressed, fire the callback.
* use $(...).on('keypress', cr(handler))
*/
function cr(hdl) {
return function(e) {
if (e.which == 10 || e.which == 13 || e.which == 32) {
hdl();
}
};
}
/** Extend an objects with options */
@ -36,23 +45,23 @@ function rgxe(str) {
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
}
/** Format number to N decimal places, output as string */
function numfmt(x, places) {
var pow = Math.pow(10, places);
return Math.round(x*pow) / pow;
}
function estimateLoadTime(fs, n) {
return (1000/fs)*n+1500;
}
/** Get millisecond timestamp */
function msNow() {
return +(new Date);
}
/** Get ms elapsed since msNow() */
function msElapsed(start) {
return msNow() - start;
}
/** Shim for log base 10 */
Math.log10 = Math.log10 || function(x) {
return Math.log(x) / Math.LN10;
};
@ -93,3 +102,22 @@ String.prototype.format = function () {
return out;
};
/** HTML escape */
function e(str) {
return $.htmlEscape(str);
}
/** Check for undefined */
function undef(x) {
return typeof x == 'undefined';
}
/** Safe json parse */
function jsp() {
try {
return JSON.parse(e);
} catch(e) {
console.error(e);
return null;
}
}

@ -1,17 +1,68 @@
/** Wifi page */
(function () {
(function(w) {
var authStr = ['Open', 'WEP', 'WPA', 'WPA2', 'WPA/WPA2'];
var curSSID;
// Get XX % for a slider input
function rangePt(inp) {
return Math.round(((inp.value / inp.max)*100)) + '%';
}
// Display selected STA SSID etc
function selectSta(name, password, ip) {
$('#sta_ssid').val(name);
$('#sta_password').val(password);
$('#sta-nw').toggleClass('hidden', name.length == 0);
$('#sta-nw-nil').toggleClass('hidden', name.length > 0);
$('#sta-nw .essid').html(e(name));
var nopw = undef(password) || password.length == 0;
$('#sta-nw .x-passwd').html(e(password));
$('#sta-nw .passwd').toggleClass('hidden', nopw);
$('#sta-nw .nopasswd').toggleClass('hidden', !nopw);
$('#sta-nw .ip').html(ip.length>0 ? tr('wifi.connected_ip_is')+ip : tr('wifi.not_conn'));
}
function submitPskModal(e, open) {
var passwd = $('#conn-passwd').val();
var ssid = $('#conn-ssid').val();
if (open || passwd.length) {
$('#sta_password').val(passwd);
$('#sta_ssid').val(ssid);
selectSta(ssid, passwd, '');
}
if (e) e.preventDefault();
Modal.hide('#psk-modal');
return false;
}
/** Update display for received response */
function onScan(resp, status) {
//var ap_json = {
// "result": {
// "inProgress": "0",
// "APs": [
// {"essid": "Chlivek", "bssid": "88:f7:c7:52:b3:99", "rssi": "204", "enc": "4", "channel": "1"},
// {"essid": "TyNikdy", "bssid": "5c:f4:ab:0d:f1:1b", "rssi": "164", "enc": "3", "channel": "1"},
// ]
// }
//};
if (status != 200) {
// bad response
rescan(5000); // wait 5sm then retry
return;
}
resp = JSON.parse(resp);
try {
resp = JSON.parse(resp);
} catch (e) {
console.log(e);
rescan(5000);
return;
}
var done = !bool(resp.result.inProgress) && (resp.result.APs.length > 0);
rescan(done ? 15000 : 1000);
@ -20,62 +71,73 @@
// clear the AP list
var $list = $('#ap-list');
// remove old APs
$('.AP').remove();
$('#ap-list .AP').remove();
$list.toggle(done);
$('#ap-loader').toggle(!done);
$list.toggleClass('hidden', !done);
$('#ap-loader').toggleClass('hidden', done);
// scan done
resp.result.APs.sort(function (a, b) {
return b.rssi - a.rssi;
}).forEach(function (ap) {
ap.enc = intval(ap.enc);
if (ap.enc > 4) return; // hide unsupported auths
var item = document.createElement('div');
var $item = $(item)
.data('ssid', ap.essid)
.data('pwd', ap.enc != 0)
.addClass('AP');
// mark current SSID
if (ap.essid == curSSID) {
$item.addClass('selected');
return b.rssi - a.rssi;
}).forEach(function (ap) {
ap.enc = parseInt(ap.enc);
if (ap.enc > 4) return; // hide unsupported auths
WiFi.scan_url = '/cfg/wifi/scan';
var item = mk('div');
var $item = $(item)
.data('ssid', ap.essid)
.data('pwd', ap.enc)
.attr('tabindex', 0)
.addClass('AP');
// mark current SSID
if (ap.essid == curSSID) {
$item.addClass('selected');
}
var inner = mk('div');
$(inner).addClass('inner')
.htmlAppend('<div class="rssi">{0}</div>'.format(ap.rssi_perc))
.htmlAppend('<div class="essid" title="{0}">{0}</div>'.format($.htmlEscape(ap.essid)))
.htmlAppend('<div class="auth">{0}</div>'.format(authStr[ap.enc]));
$item.on('click', function () {
var $th = $(this);
var ssid = $th.data('ssid');
$('#conn-ssid').val(ssid);
$('#conn-passwd').val('');
if (+$th.data('pwd')) {
// this AP needs a password
Modal.show('#psk-modal');
$('#conn-passwd')[0].focus();
} else {
//Modal.show('#reset-modal');
submitPskModal(null, true);
}
});
var inner = document.createElement('div');
$(inner).addClass('inner')
.htmlAppend('<div class="rssi">{0}</div>'.format(ap.rssi_perc))
.htmlAppend('<div class="essid" title="{0}">{0}</div>'.format($.htmlEscape(ap.essid)))
.htmlAppend('<div class="auth">{0}</div>'.format(authStr[ap.enc]));
$item.on('click', function () {
var $th = $(this);
// populate the form
$('#conn-essid').val($th.data('ssid'));
$('#conn-passwd').val(''); // clear
if ($th.data('pwd')) {
// this AP needs a password
Modal.show('#psk-modal');
} else {
Modal.show('#reset-modal');
$('#conn-form').submit();
}
});
item.appendChild(inner);
$list[0].appendChild(item);
});
}
item.appendChild(inner);
$list[0].appendChild(item);
});
function startScanning() {
$('#ap-loader').removeClass('hidden');
$('#ap-scan').addClass('hidden');
$('#ap-loader .anim-dots').html('.');
scanAPs();
}
/** Ask the CGI what APs are visible (async) */
function scanAPs() {
$.get('http://'+_root+'/wifi/scan', onScan);
$.get('http://'+_root+w.scan_url, onScan);
}
function rescan(time) {
@ -83,96 +145,38 @@
}
/** Set up the WiFi page */
window.wifiInit = function (obj) {
//var ap_json = {
// "result": {
// "inProgress": "0",
// "APs": [
// {"essid": "Chlivek", "bssid": "88:f7:c7:52:b3:99", "rssi": "204", "enc": "4", "channel": "1"},
// {"essid": "TyNikdy", "bssid": "5c:f4:ab:0d:f1:1b", "rssi": "164", "enc": "3", "channel": "1"},
// ]
// }
//};
function wifiInit(cfg) {
// Hide what should be hidden in this mode
$('.x-hide-'+obj.mode).addClass('hidden');
obj.mode = +obj.mode;
// Channel writable only in AP mode
if (obj.mode != 2) $('#channel').attr('readonly', 1);
curSSID = obj.staSSID;
// add SSID to the opmode field
if (curSSID) {
var box = $('#opmodebox');
box.html(box.html() + ' (' + curSSID + ')');
}
// hide IP if IP not received
if (!obj.staIP) $('.x-hide-noip').addClass('hidden');
// scan if not AP
if (obj.mode != 2) {
scanAPs();
}
$('#modeswitch').html([
'<a class="button" href="/wifi/setmode?mode=3">Client+AP</a>&nbsp;<a class="button" href="/wifi/setmode?mode=2">AP only</a>',
'<a class="button" href="/wifi/setmode?mode=3">Client+AP</a>',
'<a class="button" href="/wifi/setmode?mode=1">Client only</a>&nbsp;<a class="button" href="/wifi/setmode?mode=2">AP only</a>'
][obj.mode-1]);
};
window.wifiConn = function () {
var xhr = new XMLHttpRequest();
var abortTmeo;
function getStatus() {
xhr.open("GET", 'http://'+_root+"/wifi/connstatus");
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status >= 200 && xhr.status < 300) {
clearTimeout(abortTmeo);
var data = JSON.parse(xhr.responseText);
var done = false;
var msg = '...';
if (data.status == "idle") {
msg = "Preparing to connect";
}
else if (data.status == "success") {
msg = "Connected! Received IP " + data.ip + ".";
done = true;
}
else if (data.status == "working") {
msg = "Connecting to selected AP";
}
else if (data.status == "fail") {
msg = "Connection failed, check your password and try again.";
done = true;
}
$("#status").html(msg);
if (done) {
$('#backbtn').removeClass('hidden');
$('.anim-dots').addClass('hidden');
} else {
window.setTimeout(getStatus, 1000);
}
}
};
cfg.mode = +cfg.mode;
$('#ap-noscan').toggleClass('hidden', cfg.mode != 2);
$('#ap-scan').toggleClass('hidden', cfg.mode == 2);
// Update slider value displays
$('.Row.range').forEach(function(x) {
var inp = x.querySelector('input');
var disp1 = x.querySelector('.x-disp1');
var disp2 = x.querySelector('.x-disp2');
var t = rangePt(inp);
$(disp1).html(t);
$(disp2).html(t);
$(inp).on('input', function() {
t = rangePt(inp);
$(disp1).html(t);
$(disp2).html(t);
});
});
abortTmeo = setTimeout(function () {
xhr.abort();
$("#status").html("Telemetry lost, try reconnecting to the AP.");
$('#backbtn').removeClass('hidden');
$('.anim-dots').addClass('hidden');
}, 4000);
// Forget STA credentials
$('#forget-sta').on('click', function() {
selectSta('', '', '');
return false;
});
xhr.send();
}
selectSta(cfg.sta_ssid, cfg.sta_password, cfg.sta_active_ip);
curSSID = cfg.sta_active_ssid;
}
getStatus();
};
})();
w.init = wifiInit;
w.startScanning = startScanning;
})(window.WiFi = {});

@ -0,0 +1,140 @@
<?php
return [
'appname' => 'ESPTerm',
'menu.cfg_wifi' => 'WiFi Settings',
'menu.cfg_network' => 'Network Configuration',
'menu.cfg_app' => 'Terminal Settings',
'menu.about' => 'About ESPTerm',
'menu.help' => 'Quick Reference',
'menu.term' => 'Back to Terminal',
'menu.cfg_admin' => 'Reset & Restore',
'menu.cfg_wifi_conn' => 'Connecting to External Network',
'menu.settings' => 'Settings',
'title.term' => 'Terminal',
'net.ap' => 'DHCP Server (AP)',
'net.sta' => 'DHCP Client (Station)',
'net.sta_mac' => 'Station MAC',
'net.ap_mac' => 'AP MAC',
'net.details' => 'MAC addresses',
'app.defaults' => 'Initial settings',
'app.explain_initials' => '
Those are the initial settings used after ESPTerm restarts, and they
will also be applied immediately after you submit this form.
They can be subsequently changed by ESC commands, but those changes
aren\'t persistent and will be lost when the device powers off.',
'app.term_title' => 'Header text',
'app.term_width' => 'Screen width',
'app.term_height' => 'Screen height',
'app.default_fg' => 'Base text color',
'app.default_bg' => 'Base background',
'app.btn1' => 'Button 1 text',
'app.btn2' => 'Button 2 text',
'app.btn3' => 'Button 3 text',
'app.btn4' => 'Button 4 text',
'app.btn5' => 'Button 5 text',
'color.0' => 'Black',
'color.1' => 'Dark Red',
'color.2' => 'Dark Green',
'color.3' => 'Dim Yellow',
'color.4' => 'Deep Blue',
'color.5' => 'Dark Violet',
'color.6' => 'Dark Cyan',
'color.7' => 'Silver',
'color.8' => 'Gray',
'color.9' => 'Light Red',
'color.10' => 'Light Green',
'color.11' => 'Light Yellow',
'color.12' => 'Light Blue',
'color.13' => 'Light Violet',
'color.14' => 'Light Cyan',
'color.15' => 'White',
'net.explain_sta' => '
Those settings affect the built-in DHCP client used for
connecting to an external network. Switching DHCP (dynamic IP) off
makes ESPTerm use the configured static IP. Please double-check
those settings before submitting, setting them incorrectly may
make it hard to access ESPTerm via the external network.',
'net.explain_ap' => '
Those settings affect the built-in DHCP server in AP mode.
Please double-check those settings before submitting, setting them
incorrectly may render ESPTerm inaccessible via the AP.',
'net.ap_dhcp_time' => 'Lease time',
'net.ap_dhcp_start' => 'Pool start IP',
'net.ap_dhcp_end' => 'Pool end IP',
'net.ap_addr_ip' => 'Own IP address',
'net.ap_addr_mask' => 'Subnet mask',
'net.sta_dhcp_enable' => 'Use dynamic IP',
'net.sta_addr_ip' => 'ESPTerm static IP',
'net.sta_addr_mask' => 'Subnet mask',
'net.sta_addr_gw' => 'Gateway IP',
'wifi.ap' => 'Built-in Access Point',
'wifi.sta' => 'Connect to External Network',
'wifi.enable' => 'Enabled',
'wifi.tpw' => 'Transmit power',
'wifi.ap_channel' => 'Channel',
'wifi.ap_ssid' => 'AP SSID',
'wifi.ap_password' => 'Password',
'wifi.ap_hidden' => 'Hide SSID',
'wifi.sta_info' => 'Selected',
'wifi.not_conn' => 'Not connected.',
'wifi.sta_none' => 'None',
'wifi.sta_active_pw' => '🔒',
'wifi.sta_active_nopw' => '🔓 Open access',
'wifi.connected_ip_is' => 'Connected, IP is ',
'wifi.sta_password' => 'Password:',
'wifi.scanning' => 'Scanning',
'wifi.scan_now' => 'Start scanning!',
'wifi.cant_scan_no_sta' => 'Can\'t scan with Client mode disabled.',
'wifi.select_ssid' => 'Available networks:',
'wifi.conn.status' => 'Status:',
'wifi.conn.back_to_config' => 'Back to WiFi config',
'wifi.conn.telemetry_lost' => 'Telemetry lost, something went wrong. Try again...',
'wifi.conn.disabled' =>"Station mode is disabled.",
'wifi.conn.idle' =>"Idle, not connected and with no IP.",
'wifi.conn.success' => "Connected! Received IP ",
'wifi.conn.working' => "Connecting to selected AP",
'wifi.conn.fail' => "Connection failed, check settings & try again. Cause: ",
'admin.confirm_restore' => 'Restore all settings to their default values?',
'admin.confirm_restore_hard' =>
'Restore to firmware default settings? This will reset ' .
'all active settings and switch to AP mode with the default SSID.',
'admin.confirm_store_defaults' =>
'Enter admin password to confirm you want to store the current settings as defaults.',
'admin.password' => 'Admin password:',
'admin.restore_defaults' => 'Reset to default settings',
'admin.write_defaults' => 'Save current settings as default',
'admin.restore_hard' => 'Reset to firmware default settings',
'admin.explain' => '
ESPTerm contains two persistent memory banks, one for default and
one for active settings. Active settings can be stored as defaults
by the administrator. Use the following button to revert all
active settings to their stored default values.
',
'apply' => 'Apply!',
'enabled' => 'Enabled',
'disabled' => 'Disabled',
'yes' => 'Yes',
'no' => 'No',
'confirm' => 'OK',
'form_errors' => 'Validation errors for:',
];

@ -5,6 +5,8 @@ echo "Packing js..."
cat jssrc/chibi.js \
jssrc/utils.js \
jssrc/modal.js \
jssrc/notif.js \
jssrc/appcommon.js \
jssrc/term.js \
jssrc/wifi.js > js/app.js
jssrc/lang.js \
jssrc/wifi.js \
jssrc/term.js > js/app.js

@ -0,0 +1,20 @@
<nav id="menu">
<div id="brand" tabindex=0><?= tr('appname') ?></div>
<a href="<?= e(url('term')) ?>" class="icn-back"><?= tr('menu.term') ?></a>
<?php
// generate the menu
foreach ($_pages as $k => $page) {
if (strpos($page->bodyclass, 'cfg') === false) continue;
$sel = ($_GET['page'] == $k) ? 'selected' : '';
$text = $page->label;
$url = e(url($k));
echo "<a href=\"$url\" class=\"$page->icon $sel\">$text</a>";
}
?>
</nav>
<script>
function menuOpen() { $('#menu').toggleClass('expanded') }
$('#brand').on('click', menuOpen).on('keypress', cr(menuOpen));
</script>

@ -0,0 +1,31 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title><?= $_GET['PAGE_TITLE'] ?></title>
<link href="/css/app.css" rel="stylesheet">
<script src="/js/app.js"></script>
<script>var _root = <?= JS_WEB_ROOT ?>;</script>
</head>
<body class="<?= $_GET['BODYCLASS'] ?>">
<div id="outer">
<?php
$cfg = false;
if (strpos($_GET['BODYCLASS'], 'cfg') !== false) {
$cfg = true;
require __DIR__ . '/_cfg_menu.php';
}
?>
<div id="content">
<img src="/img/loader.gif" alt="Loading…" id="loader">
<?php if ($cfg): ?>
<h1><?= tr('menu.' . $_GET['page']) ?></h1>
<div class="Box errors hidden">
<span class="lead"><?= tr('form_errors') ?></span>&nbsp;<span class="list"></span>
</div>
<?php endif; ?>

@ -0,0 +1,9 @@
<div class="NotifyMsg hidden" id="notif"></div>
</div>
</div>
</body>
</html>

@ -0,0 +1,69 @@
<div class="Box">
<img src="/img/cvut.svg" id="logo" class="mq-tablet-min">
<h2>ESP8266 Remote Terminal</h2>
<img src="/img/cvut.svg" id="logo2" class="mq-phone">
<p>
&copy; Ondřej Hruška, 2016-2017
&lt;<a href="mailto:ondra@ondrovo.com">ondra@ondrovo.com</a>&gt;
</p>
<p>
<a href="http://measure.feld.cvut.cz/" target="blank">Katedra měření, FEL ČVUT</a><br>
Department of Measurement, FEE CTU
</p>
</div>
<div class="Box">
<h2>Version</h2>
<table>
<tr>
<th>ESPTerm</th>
<td>v%vers_fw%, build %date% at %time%</td>
</tr>
<tr>
<th>libesphttpd</th>
<td>v%vers_httpd%</td>
</tr>
<tr>
<th>ESP&nbsp;IoT&nbsp;SDK</th>
<td>v%vers_sdk%</td>
</tr>
</table>
</div>
<div class="Box">
<h2>Issues</h2>
<p>
Please report any issues to the <a href="%githubrepo%/issues">bugtracker</a> or send them by e-mail (see above).
</p>
<p>
Firmware updates can be downloaded from the <a href="%githubrepo%/releases">releases page</a> and flashed
with <a href="https://github.com/espressif/esptool">esptool.py</a>.
</p>
</div>
<div class="Box">
<h2>Contributing</h2>
<p>
<i class="icn-github"></i> You're welcome to submit your improvements and ideas to our <a href="%githubrepo%">GitHub repository</a>!
</p>
<p>
<i class="icn-donate"></i> If you'd like to donate, please try <a href="https://paypal.me/mightypork">PayPal</a> or
<a href="https://liberapay.com/MightyPork/">LiberaPay</a>.
</p>
</div>
<div class="Box">
<h2>Thanks</h2>
<p>
The webserver is based on a <a href="https://github.com/MightyPork/libesphttpd">fork</a> of the
<a href="https://github.com/Spritetm/esphttpd">esphttpd</a> library by Jeroen Domburg (Sprite_tm).
</p>
<p>
Using (modified) JS library <a href="https://github.com/kylebarrow/chibi">chibi.js</a> by
Kyle Barrow as a lightweight jQuery alternative.
</p>
</div>

@ -0,0 +1,34 @@
<div class="Box">
<div class="Row explain">
<?= tr('admin.explain') ?>
</div>
<div class="Row buttons">
<a class="button icn-restore"
onclick="return confirm('<?= tr('admin.confirm_restore') ?>');"
href="<?= e(url('restore_defaults')) ?>"
>
<?= tr('admin.restore_defaults') ?>
</a>
</div>
<div class="Row buttons">
<a onclick="writeDefaults(); return false;" href="#"><?= tr('admin.write_defaults') ?></a>
</div>
<div class="Row buttons">
<a onclick="return confirm('<?= tr('admin.confirm_restore_hard') ?>');"
href="<?= e(url('restore_hard')) ?>"
>
<?= tr('admin.restore_hard') ?>
</a>
</div>
</div>
<script>
function writeDefaults() {
var pw = prompt('<?= tr('admin.confirm_store_defaults') ?>');
if (!pw) return;
location.href = <?=json_encode(url('write_defaults')) ?> + '?pw=' + pw;
}
</script>

@ -0,0 +1,71 @@
<form class="Box mobopen str" action="<?= e(url('app_set')) ?>" method="GET" id='form-1'>
<h2><?= tr('app.defaults') ?></h2>
<div class="Row explain">
<?= tr('app.explain_initials') ?>
</div>
<div class="Row">
<label for="term_width"><?= tr('app.term_width') ?></label>
<input type="number" step=1 min=1 max=255 name="term_width" id="term_width" value="%term_width%" required>
</div>
<div class="Row">
<label for="term_height"><?= tr('app.term_height') ?></label>
<input type="number" step=1 min=1 max=255 name="term_height" id="term_height" value="%term_height%" required>
</div>
<div class="Row">
<label for="default_fg"><?= tr("app.default_fg") ?></label>
<select name="default_fg" id="default_fg">
<?php for($i=0; $i<16; $i++): ?>
<option value="<?=$i?>"><?= tr("color.$i") ?></option>
<?php endfor; ?>
</select>
<script>qs('#default_fg').selectedIndex = %default_fg%;</script>
</div>
<div class="Row">
<label for="default_bg"><?= tr("app.default_bg") ?></label>
<select name="default_bg" id="default_bg">
<?php for($i=0; $i<16; $i++): ?>
<option value="<?=$i?>"><?= tr("color.$i") ?></option>
<?php endfor; ?>
</select>
<script>qs('#default_bg').selectedIndex = %default_bg%;</script>
</div>
<div class="Row">
<label for="term_title"><?= tr('app.term_title') ?></label>
<input type="text" name="term_title" id="term_title" value="%term_title%" required>
</div>
<div class="Row">
<label for="btn1"><?= tr("app.btn1") ?></label>
<input class="short" type="text" name="btn1" id="btn1" value="%btn1%">
</div>
<div class="Row">
<label for="btn2"><?= tr("app.btn2") ?></label>
<input class="short" type="text" name="btn2" id="btn2" value="%btn2%">
</div>
<div class="Row">
<label for="btn3"><?= tr("app.btn3") ?></label>
<input class="short" type="text" name="btn3" id="btn3" value="%btn3%">
</div>
<div class="Row">
<label for="btn4"><?= tr("app.btn4") ?></label>
<input class="short" type="text" name="btn4" id="btn4" value="%btn4%">
</div>
<div class="Row">
<label for="btn5"><?= tr("app.btn5") ?></label>
<input class="short" type="text" name="btn5" id="btn5" value="%btn5%">
</div>
<div class="Row buttons">
<a class="button icn-ok" href="#" onclick="qs('#form-1').submit()"><?= tr('apply') ?></a>
</div>
</form>

@ -0,0 +1,95 @@
<?php
$ipmask='pattern="^([0-9]{1,3}\.){3}[0-9]{1,3}$"';
?>
<form class="Box str mobcol" action="<?= e(url('network_set')) ?>" method="GET" id="form-2">
<h2 tabindex=0><?= tr('net.sta') ?></h2>
<div class="Row explain">
<?= tr('net.explain_sta') ?>
</div>
<div class="Row checkbox x-static-toggle" >
<label><?= tr('net.sta_dhcp_enable') ?></label><!--
--><span class="box" tabindex=0 role=checkbox></span>
<input type="hidden" name="sta_dhcp_enable" value="%sta_dhcp_enable%">
</div>
<div class="Row x-static">
<label for="sta_addr_ip"><?= tr('net.sta_addr_ip') ?></label>
<input type="text" name="sta_addr_ip" id="sta_addr_ip" value="%sta_addr_ip%" <?=$ipmask?> required>
</div>
<div class="Row x-static">
<label for="sta_addr_mask"><?= tr('net.sta_addr_mask') ?></label>
<input type="text" name="sta_addr_mask" id="sta_addr_mask" value="%sta_addr_mask%" <?=$ipmask?> required>
</div>
<div class="Row x-static">
<label for="sta_addr_gw"><?= tr('net.sta_addr_gw') ?></label>
<input type="text" name="sta_addr_gw" id="sta_addr_gw" value="%sta_addr_gw%" <?=$ipmask?> required>
</div>
<div class="Row buttons">
<a class="button icn-ok" href="#" onclick="qs('#form-2').submit()"><?= tr('apply') ?></a>
</div>
</form>
<form class="Box str mobcol" action="<?= e(url('network_set')) ?>" method="GET" id="form-1">
<h2 tabindex=0><?= tr('net.ap') ?></h2>
<div class="Row explain">
<?= tr('net.explain_ap') ?>
</div>
<div class="Row">
<label for="ap_dhcp_time"><?= tr('net.ap_dhcp_time') ?><span class="mq-phone">&nbsp;(min)</span></label>
<input type="number" step=1 min=1 max=2880 name="ap_dhcp_time" id="ap_dhcp_time" value="%ap_dhcp_time%" required>
<span class="mq-no-phone">&nbsp;(min)</span>
</div>
<div class="Row">
<label for="ap_dhcp_start"><?= tr('net.ap_dhcp_start') ?></label>
<input type="text" name="ap_dhcp_start" id="ap_dhcp_start" value="%ap_dhcp_start%" <?=$ipmask?> required>
</div>
<div class="Row">
<label for="ap_dhcp_end"><?= tr('net.ap_dhcp_end') ?></label>
<input type="text" name="ap_dhcp_end" id="ap_dhcp_end" value="%ap_dhcp_end%" <?=$ipmask?> required>
</div>
<div class="Row">
<label for="ap_addr_ip"><?= tr('net.ap_addr_ip') ?></label>
<input type="text" name="ap_addr_ip" id="ap_addr_ip" value="%ap_addr_ip%" <?=$ipmask?> required>
</div>
<div class="Row">
<label for="ap_addr_mask"><?= tr('net.ap_addr_mask') ?></label>
<input type="text" name="ap_addr_mask" id="ap_addr_mask" value="%ap_addr_mask%" <?=$ipmask?> required>
</div>
<div class="Row buttons">
<a class="button icn-ok" href="#" onclick="qs('#form-1').submit()"><?= tr('apply') ?></a>
</div>
</form>
<div class="Box mobcol">
<h2><?= tr('net.details') ?></h2>
<div class="Row">
<label><?= tr('net.sta_mac') ?></label><input type="text" readonly value="%sta_mac%">
</div>
<div class="Row">
<label><?= tr('net.ap_mac') ?></label><input type="text" readonly value="%ap_mac%">
</div>
</div>
<script>
function updateStaticDisp() {
$('.x-static').toggleClass('hidden', $('#sta_dhcp_enable').val());
}
$('.x-static-toggle').on('click', function() {
updateStaticDisp();
});
updateStaticDisp();
</script>

@ -0,0 +1,108 @@
<form class="Box str mobcol" action="<?= e(url('wifi_set')) ?>" method="GET" id="form-1">
<h2 tabindex=0><?= tr('wifi.ap') ?></h2>
<div class="Row checkbox">
<label><?= tr('wifi.enable') ?></label><!--
--><span class="box" tabindex=0></span>
<input type="hidden" name="ap_enable" value="%ap_enable%">
</div>
<div class="Row">
<label for="ap_ssid"><?= tr('wifi.ap_ssid') ?></label>
<input type="text" name="ap_ssid" id="ap_ssid" value="%ap_ssid%" required>
</div>
<div class="Row">
<label for="ap_password"><?= tr('wifi.ap_password') ?></label>
<input type="text" name="ap_password" id="ap_password" value="%ap_password%">
</div>
<div class="Row">
<label for="ap_channel"><?= tr('wifi.ap_channel') ?></label>
<input type="number" name="ap_channel" id="ap_channel" min=1 max=14 value="%ap_channel%" required>
</div>
<div class="Row range">
<label for="tpw">
<?= tr('wifi.tpw') ?>
<span class="display x-disp1 mq-phone"></span>
</label>
<input type="range" name="tpw" id="tpw" step=1 min=0 max=82 value="%tpw%">
<span class="display x-disp2 mq-no-phone"></span>
</div>
<div class="Row checkbox">
<label><?= tr('wifi.ap_hidden') ?></label><!--
--><span class="box" tabindex=0></span>
<input type="hidden" name="ap_hidden" value="%ap_hidden%">
</div>
<div class="Row buttons">
<a class="button icn-ok" href="#" onclick="qs('#form-1').submit()"><?= tr('apply') ?></a>
</div>
</form>
<form class="Box str mobcol" action="<?= e(url('wifi_set')) ?>" method="GET" id="form-2">
<h2 tabindex=0><?= tr('wifi.sta') ?></h2>
<div class="Row checkbox">
<label><?= tr('wifi.enable') ?></label><!--
--><span class="box" tabindex=0></span>
<input type="hidden" name="sta_enable" value="%sta_enable%">
</div>
<input type="hidden" name="sta_ssid" id="sta_ssid" value="">
<input type="hidden" name="sta_password" id="sta_password" value="">
<div class="Row sta-info">
<label><?= tr('wifi.sta_info') ?></label>
<div class="AP-preview hidden" id="sta-nw">
<div class="wrap">
<div class="inner">
<div class="essid"></div>
<div class="passwd"><?= tr('wifi.sta_active_pw') ?>&nbsp;<span class="x-passwd"></span></div>
<div class="nopasswd"><?= tr('wifi.sta_active_nopw') ?></div>
<div class="ip"></div>
</div>
<a class="forget" href="#" id="forget-sta">×</a>
</div>
</div>
<div class="AP-preview-nil" id="sta-nw-nil">
<?= tr('wifi.sta_none') ?>
</div>
</div>
<div id="ap-box">
<label><?= tr('wifi.select_ssid') ?></label>
<div id="ap-scan"><a href="#" onclick="WiFi.startScanning(); return false"><?= tr('wifi.scan_now') ?></a></div>
<div id="ap-loader" class="hidden"><?= tr('wifi.scanning') ?><span class="anim-dots">.</span></div>
<div id="ap-noscan" class="hidden"><?= tr('wifi.cant_scan_no_sta') ?></div>
<div id="ap-list" class="hidden"></div>
</div>
<div class="Row buttons">
<a class="button icn-ok" href="#" onclick="qs('#form-2').submit()"><?= tr('apply') ?></a>
</div>
</form>
<div class="Modal hidden" id="psk-modal">
<div class="Dialog">
<form id="conn-form" onsubmit="submitPskModal(); return false;">
<input type="hidden" id="conn-ssid"><!--
--><label for="conn-passwd"><?= tr('wifi.sta_password') ?></label><!--
--><input type="text" id="conn-passwd"><!--
--><input type="submit" value="<?= tr('confirm') ?>">
</form>
</div>
</div>
<script>
WiFi.scan_url = '<?= url('wifi_scan', true) ?>';
WiFi.init({
mode: '%opmode%',
sta_ssid: '%sta_ssid%',
sta_password: '%sta_password%',
sta_active_ip: '%sta_active_ip%',
sta_active_ssid: '%sta_active_ssid%',
});
</script>

@ -0,0 +1,53 @@
<h1><?= tr('menu.cfg_wifi_conn') ?></h1>
<div class="Box">
<p><b><?= tr('wifi.conn.status') ?></b> <span id="status"></span><span class="anim-dots">.</span></p>
<a href="<?= e(url('cfg_wifi')) ?>" id="backbtn" class="button"><?= tr('wifi.conn.back_to_config') ?></a>
</div>
<script>
var xhr = new XMLHttpRequest();
var abortTmeo;
var messages = <?= json_encode([
'disabled' => tr('wifi.conn.disabled'),
'idle' => tr('wifi.conn.idle'),
'success' => tr('wifi.conn.success'),
'working' => tr('wifi.conn.working'),
'fail' => tr('wifi.conn.fail'),
]) ?>;
function getStatus() {
xhr.open("GET", 'http://'+_root+'<?= url('wifi_connstatus', true) ?>');
xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status >= 200 && xhr.status < 300) {
clearTimeout(abortTmeo);
var data = JSON.parse(xhr.responseText);
var done = false;
var msg = messages[data.status] || '...';
if (data.status == 'success') msg += data.ip;
if (data.status == 'fail') msg += data.cause;
$("#status").html(msg);
if (done) {
// $('#backbtn').removeClass('hidden');
$('.anim-dots').addClass('hidden');
} else {
window.setTimeout(getStatus, 1000);
}
}
};
abortTmeo = setTimeout(function () {
xhr.abort();
$("#status").html(<?= json_encode(tr('wifi.conn.telemetry_lost')) ?>);
// $('#backbtn').removeClass('hidden');
$('.anim-dots').addClass('hidden');
}, 4000);
xhr.send();
}
getStatus();
</script>

@ -1,29 +1,15 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Help - ESP8266 Remote Terminal</title>
<meta name="viewport" content="width=device-width,shrink-to-fit=no,user-scalable=no,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0">
<link rel="stylesheet" href="/css/app.css">
<script src="/js/app.js"></script>
</head>
<body class="page-help">
<h1 onclick="location.href='/'">Quick Reference</h1>
<div class="Box">
<h2>Wiring</h2>
<ul>
<li>Communication UART on pins <b>Rx, Tx</b> at 115200-8-1-N</li>
<li>Debug log on pin <b>GPIO2</b> at 115200-8-1-N</li>
<li>Use 3.3V logic, or 5V with protection resistors (470R or more)</li>
<li>If the "LVD" LED on the ESP Term board lights up, the module doesn't get enough power. Check your connections.</li>
<li>Communication UART is on pins <b>Rx, Tx</b> at 115200-8-1-N. The baud rate can be changed in Terminal Settings.
<li>Debug log is on pin <b>GPIO2</b> (P2) at 115200-8-1-N. This baud rate is fixed.
<li>Compatible with 3.3&nbsp;V and 5&nbsp;V logic. For 5&nbsp;V, 470&nbsp;R protection resistors are recommended.
<li>If the "LVD" LED on the ESPTerm module lights up, it doesn't get enough power to run correctly. Check your connections.
<li>Connect Rx and Tx with a piece of wire to test the terminal alone, you should see what you type in the browser.
NOTE: This won't work if your ESP8266 board has a built-in USB-serial (like NodeMCU).</li>
<li>For best performance, use the module in the Client mode. In AP mode, check that the channel used is clear;
interference may cause lag in the terminal.</li>
<i>NOTE: This won't work if your ESP8266 board has a built-in USB-serial converter (like NodeMCU).</i>
<li>For best performance, use the module in Client mode (connected to external network).
<li>In AP mode, check that the channel used is clear; interference may cause a flaky connection.
</ul>
</div>
@ -328,12 +314,3 @@
</tbody>
</table>
</div>
<nav id="botnav">
<a href="/">Terminal</a><!--
--><a href="/wifi">WiFi config</a><!--
--><a href="/about">About</a>
</nav>
</body>
</html>

@ -0,0 +1,37 @@
<script>
// Workaround for badly loaded page
setTimeout(function() {
if (typeof termInit == 'undefined' || typeof $ == 'undefined') {
location.reload(true);
}
}, 2000);
</script>
<h1>%term_title%</h1>
<div id="termwrap">
<div id="screen"></div>
<div id="buttons">
<button data-n="1" class="btn-blue">%btn1%</button><!--
--><button data-n="2" class="btn-blue">%btn2%</button><!--
--><button data-n="3" class="btn-blue">%btn3%</button><!--
--><button data-n="4" class="btn-blue">%btn4%</button><!--
--><button data-n="5" class="btn-blue">%btn5%</button>
</div>
</div>
<nav id="botnav">
<a href="<?= url('cfg_wifi') ?>"><?= tr('menu.settings') ?></a><!--
--><a href="<?= url('help') ?>">Help</a><!--
--><a href="<?= url('about') ?>">About</a>
</nav>
<script>
try {
termInit(%screenData%);
} catch(e) {
console.error("Fail, reloading...");
location.reload(true);
}
</script>

File diff suppressed because one or more lines are too long

@ -1,178 +0,0 @@
html {
font-family: Arial, sans-serif;
color: #D0D0D0;
background: #131315;
}
html, body {
@include naked();
width: 100%;
height: 100%;
overflow: hidden;
}
a, a:visited, a:link {
cursor: pointer;
color: #5abfff;
text-decoration: none;
}
a:hover {
color: #5abfff;
text-decoration: underline;
}
.Box {
display: block;
max-width: 900px;
margin-top: dist(0);
padding: dist(-1) dist(0);
@include media($phone) {
margin-top: dist(-1);
padding: dist(-3) dist(-2);
}
//
//h1 + & {
// margin-top: 0;
//}
//
//h2 {
// margin-top: 0;
//}
p:first-child {
margin-top:0;
}
border-radius: 3px;
background-color: rgba(white, .07);
//&.wide {
// width: initial;
// max-width: initial;
//}
//
//&.medium {
// max-width: 1200px;
//}
//
//.Valfield {
// display: inline-block;
// min-width: 10em;
//}
}
body {
position: relative;
padding: dist(0);
@include media($phone) {
padding: dist(-1);
}
overflow-y: auto;
& > * {
margin-left: auto;
margin-right: auto;
}
}
h1,h2 {
@include noselect();
}
h1 {
text-align: center;
font-size: fsize(6);
margin-top: 0;
margin-bottom: dist(0);
@include media($phone) {
font-size: fsize(3);
margin-bottom: dist(-1);
}
@include media($tablet) {
font-size: fsize(5);
}
}
h2 {
font-size: fsize(2);
margin-bottom: dist(-1);
//&:first-child{margin-top:0}
}
td, th {
padding: dist(-2);
white-space: nowrap;
@include media($phone) {
padding: dist(-3);
}
}
tbody th {
text-align: right;
width: $form-label-w;
color: $c-form-label-fg;
@include media($phone) {
width: auto;
}
}
tbody td {
input[type="text"], input[type="number"] {
width: 10em;
@include media($phone) {
width: 8em;
}
}
}
// Loader wheel in top right corner
#loader {
position: absolute;
right: dist(1);
top: dist(1);
transition: opacity .2s;
opacity: 0;
@include media($phone) {
top: dist(0);
right: dist(0);
}
&.show {
opacity:1;
}
}
ul > * {
padding-top: .1em;
padding-bottom: .1em;
}
#botnav {
padding-top: 1.5em;
text-align: center;
a {
padding: 0 dist(-2);
text-decoration: underline;
&, &:visited, &:link {
color: #2e4d6e;
}
&:hover {
color: #5abfff;
}
}
}

@ -1,4 +1,5 @@
@import "normalize";
@import "fontello-embedded";
@import "lib/bourbon/bourbon";
@import "grid-settings";
@ -6,15 +7,16 @@
@import "utils";
$form-label-w: 130px;
$form-label-w: 160px;
$form-label-gap: 8px;
$form-field-w: 250px;
$c-red-outline: #ff0099;
$c-form-label-fg: white;
$c-form-field-bg: #303030;
$c-form-field-bg: #3c3c3c;
$c-form-field-fg: white;
$c-form-highlight: #214e7a;
$c-form-highlight-a: #2972ba;
$c-form-highlight: #2972ba;
$c-form-highlight-a: #2ea1f9;
@function dist($x) {
@return modular-scale($x, 1rem, $golden);
@ -24,16 +26,7 @@ $c-form-highlight-a: #2972ba;
@return modular-scale($x, 1em, $major-second);
}
.hidden {
display: none !important;
}
[onclick] {
cursor: pointer;
}
@import "modal";
@import "layout";
@import "layout/index";
@import "form/index";
// import all our pages
@ -45,17 +38,17 @@ $c-form-highlight-a: #2972ba;
// media queries
@include media($tablet-min) {
.mq-phone { display: none; }
.mq-phone { display: none!important; }
}
@include media($phone) {
.mq-tablet-min { display: none; }
.mq-tablet-min, .mq-no-phone { display: none !important; }
}
@include media($normal-min) {
.mq-tablet-max { display: none; }
.mq-tablet-max { display: none !important; }
}
@include media($tablet-max) {
.mq-normal-min { display: none; }
.mq-normal-min { display: none !important; }
}

@ -31,9 +31,18 @@ button, input[type=submit], .button {
min-width: initial;
}
text-shadow: 1.5px 1.5px 2px rgba(black, 0.6);
&::before {
vertical-align: -1px;
margin-left: 0;
}
text-shadow: 1.5px 1.5px 2px rgba(black, 0.4);
@include fancy-btn-colors($btn-blue-f, $btn-blue-b, $btn-blue-fa, $btn-blue-ba);
&:focus {
outline-color: $c-red-outline;
}
}
//input[type="submit"], .btn-green {

@ -7,7 +7,7 @@
border-radius: 2px;
padding: 0 0.6em;
border: 0 none;
outline: 0 none !important;
//outline: 0 none !important;
line-height: 1.8em;
font-size: 1.1em;
margin-bottom: 3px;

@ -7,8 +7,8 @@
color: $c-form-field-fg;
padding: 6px;
line-height: 1em;
outline: 0 none !important;
-moz-outline: 0 none !important;
//outline: 0 none !important;
//-moz-outline: 0 none !important;
font-weight: normal;
&:focus, &:hover {
@ -16,6 +16,48 @@
}
}
.Row.checkbox {
$h: 27px;
line-height: $h;
.box {
overflow: hidden;
width: $h;
height: $h;
border: 1px solid #808080;
border-radius: 3px;
background: $c-form-field-bg;
display: inline-block;
position: relative;
cursor: pointer;
color: $c-form-highlight-a;
&::before {
font-weight: bold;
position: absolute;
content: '×';
left: 0; top: 0; right: 0; bottom: 0;
line-height: $h - 1px;
text-align: center;
font-size: $h;
vertical-align: middle;
display: none;
}
&.checked::before {
display: block;
}
}
}
.Row.range {
.display {
margin-left: 1ex;
}
label .display { font-weight: normal; }
}
//#{$all-text-inputs} {
// @include can-select();
//}

@ -5,13 +5,29 @@ form { @include naked(); }
width: $form-field-w;
}
form .Row {
input[type="number"], input.short {
width: $form-field-w/2;
}
.Box.errors {
.list {
color: crimson;
font-weight: bold;
}
.lead {
color: white;
}
}
.Row {
vertical-align: middle;
margin: 14px auto;
margin: 12px auto;
text-align: left;
display: flex;
flex-direction: row;
align-items: center;
&:first-child {
margin-top: 0;
@ -30,6 +46,7 @@ form .Row {
}
&.buttons {
margin: 16px auto;
input, .button {
margin-right: dist(-1);
}
@ -80,34 +97,44 @@ form .Row {
align-self: flex-start;
@include noselect;
@include nowrap;
}
.checkbox-wrap {
display: inline-block;
width: $form-label-w;
padding: $form-label-gap;
text-align: right;
align-self: flex-start;
input[type=checkbox] {
margin: auto;
width: auto;
height: auto;
}
label.error {
color: crimson;
}
& + label {
width: $form-field-w;
padding-left: 0;
text-align: left;
cursor: pointer;
}
//.checkbox-wrap {
// display: inline-block;
// width: $form-label-w;
// padding: $form-label-gap;
// text-align: right;
// align-self: flex-start;
//
// input[type=checkbox] {
// margin: auto;
// width: auto;
// height: auto;
// }
//
// & + label {
// width: $form-field-w;
// padding-left: 0;
// text-align: left;
// cursor: pointer;
// }
//}
input[type="range"] {
width: 200px;
}
// special phone style
@include media($phone) {
flex-direction: column;
margin: 6px auto;
&.buttons, &.centered {
&.buttons, &.centered, &.checkbox {
flex-direction: row;
}
@ -139,7 +166,7 @@ form .Row {
}
}
#{$all-text-inputs}, textarea {
#{$all-text-inputs}, input[type="range"], textarea, select {
width: 100%;
}
}

@ -9,5 +9,5 @@
}
}
//@import 'form_layout';
@import 'form_layout';
//@import 'select';

@ -0,0 +1,36 @@
html {
font-family: Arial, sans-serif;
color: #D0D0D0;
background: #131315;
}
html, body {
@include naked();
width: 100%;
height: 100%;
overflow: hidden;
}
a, a:visited, a:link {
cursor: pointer;
color: #5abfff;
text-decoration: none;
}
a:hover {
color: #5abfff;
text-decoration: underline;
}
.hidden {
display: none !important;
}
[onclick] {
cursor: pointer;
}
ul > * {
padding-top: .2em;
padding-bottom: .2em;
}

@ -0,0 +1,137 @@
.Box {
display: block;
max-width: 900px;
margin-top: dist(0);
padding: dist(-1) dist(0);
// clear floats
&::after {
content: '';
display: block;
clear: both
}
@include media($phone) {
margin-top: dist(-1);
}
h1, h2 {
overflow: hidden;
}
h1 + & {
margin-top: 0;
}
h2 {
margin-top: 0;
margin-bottom: 0 !important;
}
p:last-child {
margin-bottom: 0.5em;
}
border-radius: 3px;
background-color: rgba(white, .07);
box-shadow: 0 0 4px black;
border: 1px solid #4f4f4f;
&.wide {
width: initial;
max-width: initial;
}
&.medium {
max-width: 1200px;
}
//.Valfield {
// display: inline-block;
// min-width: 10em;
//}
// Submit Top Right
&.str {
position: relative;
.Row.buttons {
position: absolute;
@include media($phone) {
right: dist(0);
top: 1.8em;
margin: 1rem auto;
}
@include media($tablet-min) {
right: 0;
top: 0;
margin-top: dist(-1);
}
}
}
&.str.mobopen .Row.buttons {
top: 0;
margin-top: dist(-1);
}
.Row.explain {
max-width: 600px; margin-left: 0;
@include media($phone) {
margin-top: 60px;
}
}
&.mobopen .Row.explain {
margin-top: 12px; // default from .Row
@include media($phone) {
margin-top: 18px;
}
}
}
@include media($phone) {
.Box.mobcol {
h2 {
position: relative;
cursor: pointer;
padding-right: 1.3rem;
&::after {
position: absolute;
right: 0;
content: '';
top:50%;
font-size: 120%;
font-weight: bold;
transform: translate(0,-50%) rotate(90deg);
}
}
&.expanded h2::after {
transform: translate(-25%,-50%) rotate(-90deg);
margin-bottom: dist(0);
}
.Row {
display: none;
}
#ap-box {
display: none;
}
&.expanded {
.Row {
display: flex;
}
#ap-box {
display: block;
}
}
}
}

@ -0,0 +1,50 @@
#content {
flex-grow: 1;
position: relative;
padding: dist(0);
@include media($phone) {
padding: dist(-1);
}
overflow-y: auto;
& > * {
margin-left: auto;
margin-right: auto;
}
h1 {
text-align: center;
font-size: fsize(7);
margin-top: 0;
margin-bottom: dist(0);
}
h2 {
font-size: fsize(3);
margin-bottom: dist(-1);
}
@include media($phone) {
h1 {
font-size: fsize(5);
margin-bottom: dist(-1);
}
h2 {
font-size: fsize(2);
margin-bottom: dist(-1);
}
}
td, th {
padding: dist(-2);
}
tbody th {
text-align: right;
width: $form-label-w;
color: $c-form-label-fg;
}
}

@ -0,0 +1,9 @@
@import "base";
@import "outer-wrap";
@import "menu";
@import "content";
@import "loader";
@import "box";
@import "modal";

@ -0,0 +1,18 @@
// Loader wheel in top right corner
#loader {
position: absolute;
right: dist(1);
top: dist(1);
transition: opacity .2s;
opacity: 0;
@include media($phone) {
top: dist(0);
right: dist(0);
}
&.show {
opacity:1;
}
}

@ -0,0 +1,110 @@
#menu {
$menu-bg: #3983CD;
$menu-hl: #5badff; //#1bd886;
$menu-fg: white;
flex: 0 0 15rem;
background: $menu-bg;
& > * {
display: block;
text-decoration: none;
padding: dist(-1) dist(0);
@include nowrap;
@include noselect;
}
#brand {
color: $menu-fg;
background: darken($menu-bg, 10%);
font-size: 120%;
text-align: center;
position:relative;
transition: none;
font-weight: bold;
margin-bottom: dist(0);
@include media($phone) {
background: $menu-bg;
cursor: pointer;
margin-bottom: dist(-2);
&::after {
position: absolute;
color: rgba(black, .4);
right: dist(0);
content: '';
top:50%;
font-size: 120%;
font-weight: bold;
transform: translate(0,-50%) rotate(90deg);
}
}
}
&.expanded #brand {
background: darken($menu-bg, 10%);
@include media($phone) {
&:after { transform: translate(-25%,-50%) rotate(-90deg) }
}
}
a {
font-size: 130%;
color: $menu-fg;
transition: background-color 0.2s;
text-shadow: 0 0 5px rgba(black, .4);
&:hover, &.selected {
background: $menu-hl;
text-shadow: 0 0 5px rgba(black, .6);
}
&.selected {
position: relative;
box-shadow: 0 0 5px rgba(black, .5);
}
&:focus {
outline-color: $c-red-outline;
}
//&::before {
// content: "";
// padding-right: .5rem;
// position: relative;
// top: -0.1rem;
//}
// Fontello
&::before {
vertical-align: -2px;
margin-left: 0;
margin-right: 15px;
}
@include media($phone) {
display: none;
&::before {margin-left: 10px;}
}
}
&.expanded a { display:block }
@include media($tablet) {
#brand {
font-size: 95%;
margin-bottom: dist(-1);
}
a { font-size: 105%; }
flex-basis: 10rem;
& > * { padding: dist(-2) dist(-1); }
}
}

@ -25,9 +25,11 @@
//min-height: 15rem;
background: #1c1c1e;
border-left: 6px solid $c-form-highlight-a;
border-right: 6px solid $c-form-highlight-a;
box-shadow: 0 0 2px 0 #434349, 0 0 6px 0 black;
border-left: 6px solid $c-form-highlight;
border-right: 6px solid $c-form-highlight;
border-top: 1px solid $c-form-highlight;
border-bottom: 1px solid $c-form-highlight;
box-shadow: 0 0 6px 0 black;
border-radius: 6px;
@ -40,21 +42,21 @@
}
}
/*
// "toast"
.NotifyMsg {
position: fixed;
bottom: dist(2);
top: dist(1);
right: dist(2);
padding: dist(-1) dist(0);
// center horizontally
left: 50%;
@include translate(-50%,0);
//left: 50%;
//@include translate(-50%,0);
// hack to remove blur in chrome
-webkit-font-smoothing: subpixel-antialiased;
-webkit-transform: translateZ(0) scale(1.0, 1.0);
background: #37a349;
background: #3887d0;
&.error {
background: #d03e42;
}
@ -75,4 +77,3 @@
&.visible { opacity: 1 }
&.hidden { display: none }
}
*/

@ -0,0 +1,22 @@
/* Main outer container */
#outer {
display: flex;
position: absolute;
width: 100%;
height: 100%;
left: 0;
right: 0;
top: 0;
bottom: 0;
overflow: hidden;
flex-direction: row;
}
@include media($phone) {
#outer {
display: block;
overflow-y: scroll;
}
}

@ -14,6 +14,7 @@
// mobile friendly
#logo2 {
max-width: 100%;
margin: 1rem;
}
td {

@ -1,4 +1,4 @@
.page-term {
body.term {
h1 {
font-size: fsize(5);
@include media($phone) {
@ -34,14 +34,33 @@
button {
margin: 0 3px;
padding: 10px 0;
width: 18%;
max-width: 65px;
min-width: initial;
padding: 8px 5px;
//width: 18%;
min-width: 65px;
//max-width: 65px;
//min-width: initial;
cursor: pointer;
font-weight: bold;
}
}
#botnav {
padding-top: 1.5em;
text-align: center;
a {
padding: 0 dist(-2);
text-decoration: underline;
&, &:visited, &:link {
color: #2e4d6e;
}
&:hover {
color: #5abfff;
}
}
}
}
#termwrap {

@ -13,11 +13,12 @@
margin: 0 (- dist(-3));
}
#ap-loader, #ap-noscan {
#ap-loader, #ap-noscan, #ap-scan {
background: rgba(white, .1);
border-radius: 5px;
padding: dist(-2);
margin-bottom: dist(-2);
margin-top: dist(-2);
}
#ap-noscan {
@ -25,7 +26,14 @@
}
#ap-box {
padding-bottom: dist(-2);
padding-top: dist(-2);
label {
display: block;
color: white;
font-weight: bold;
margin-bottom: dist(-3);
}
}
#psk-modal form {
@ -43,6 +51,50 @@
@extend %form-row-spacing;
}
%ap-inner {
cursor: pointer;
@include noselect;
position: relative;
&:active {
left: 0;
top: 1px;
}
border-radius: 3px;
color: #222;
background: #afafaf;
transition: background-color 0.5s;
&:hover { background: white }
display: flex;
.rssi {
min-width: 2.5rem;
flex: 0 0 15%;
text-align: right;
&:after {
padding-left: dist(-5);
content: '%';
font-size: fsize(-1);
}
}
.essid {
flex: 1 1 70%;
min-width: 0;
text-overflow: ellipsis;
overflow: hidden;
font-weight: bold;
}
.auth {
flex: 0 0 15%;
}
}
.AP {
// can't use margins inside a column
@ -56,53 +108,78 @@
top: 0 !important; // no click effect
}
// the actual silver box
.inner {
cursor: pointer;
@include noselect;
@extend %ap-inner;
position: relative;
&:active {
left: 0;
top: 1px;
& > * {
padding: dist(-1);
@include nowrap;
}
}
}
border-radius: 3px;
color: #222;
.AP-preview-nil {
padding: 8px;
border-radius: 5px;
border: 1px dashed #ddd;
width: 250px;
height: 94px;
}
background: #afafaf;
transition: background-color 0.5s;
&:hover { background: white }
.AP-preview {
.wrap {
@extend %ap-inner;
display: flex;
flex-direction: row;
background: #ddd !important; // override the hover effect #43de81
cursor: default;
top: 0 !important; // no click effect
overflow: hidden;
& > * {
padding: dist(-1);
@include nowrap;
.inner {
display: flex;
flex-direction: column;
& > * {
padding: dist(-1);
@include nowrap;
}
}
.forget {
align-self: stretch;
line-height: 100%;
padding: dist(-1);
border-left: 1px solid #bbb;
display: flex;
align-items: center;
.rssi {
min-width: 2.5rem;
flex: 0 0 15%;
text-align: right;
&, &:hover {
color: black;
text-decoration: none;
}
font-size: 28px;
&:hover {
background: #dc4a6a;
color: white;
border-left: 1px solid #666;
border-bottom-right-radius: 3px;
border-top-right-radius: 3px;
}
&:after {
padding-left: dist(-5);
content: '%';
font-size: fsize(-1);
&:active {
position: relative;
padding-top: calc(#{dist(-1)} + 1px);
}
}
.essid {
flex: 1 1 70%;
min-width: 0;
text-overflow: ellipsis;
overflow: hidden;
font-weight: bold;
.essid, .passwd, .nopasswd {
padding-bottom: 0;
}
.auth {
flex: 0 0 15%;
.x-passwd {
font-family: monospace;
}
}
}

@ -0,0 +1,68 @@
// Utilities for background tiling
// Use a tile as background (w, h - size of time)
@mixin tile_xy($w, $h, $x, $y) {
background-position: (-$x*$w) (-$y*$h);
}
// Use a square tile as background (size - w & h of time)
@mixin tile($size, $x, $y) {
@include tile_xy($size, $size, $x, $y);
}
// Button with sprite-sheet
// A B
// B:hover B:hover
@mixin tile_btn_h($w, $h, $x) {
@include tile_xy($w, $h, $x, 0);
&:hover {
@include tile_xy($w, $h, $x, 1);
}
}
// active the same as hover
@mixin tile_btn_h_act($w, $h, $x) {
@include tile_xy($w, $h, $x, 0);
&:hover, &.active {
@include tile_xy($w, $h, $x, 1);
}
}
// Button with sprite-sheet
// A A:hover
// B B:hover
@mixin tile_btn_v($w, $h, $y) {
@include tile_xy($w, $h, 0, $y);
&:hover {
@include tile_xy($w, $h, 1, $y);
}
}
// active the same as hover
@mixin tile_btn_v_act($w, $h, $y) {
@include tile_xy($w, $h, 0, $y);
&:hover, &.active {
@include tile_xy($w, $h, 1, $y);
}
}
@mixin inset-shadow-top($w, $c) {
box-shadow: inset 0 $w ($w*2) (-$w) $c;
}
@mixin inset-shadow-bottom($w, $c) {
box-shadow: inset 0 (-$w) ($w*2) (-$w) $c;
}
@mixin inset-shadow-left($w, $c) {
box-shadow: inset $w 0 ($w*2) (-$w) $c;
}
@mixin inset-shadow-right($w, $c) {
box-shadow: inset (-$w) 0 ($w*2) (-$w) $c;
}

@ -0,0 +1,3 @@
@import "background-tiling";
@import "pointer";
@import "misc";

@ -0,0 +1,34 @@
// Add a highlight for debugging
@mixin highlight($color) {
outline: 1px solid $color;
background: rgba($color, .05);
box-shadow: 0 0 2px 2px rgba($color, .2), inset 0 0 2px 2px rgba($color, .2);
}
// Ellipsis, but for block elements
@mixin block-ellipsis($width: 100%) {
display: block;
max-width: $width;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
word-wrap: normal;
}
// No margins, padding, borders
@mixin naked() {
border: 0 none;
margin: 0;
padding: 0;
text-decoration: none;
}
@mixin translate($x, $y) {
@include transform(translate($x, $y));
}
// Disallow wrapping
@mixin nowrap() {
white-space: nowrap;
word-wrap: normal;
}

@ -0,0 +1,26 @@
@mixin click-through() {
pointer-events: none;
}
// Disallow text selection
@mixin noselect() {
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
// Allow text selection
@mixin can-select() {
-webkit-user-select: text;
-khtml-user-select: text;
-moz-user-select: text;
-ms-user-select: text;
user-select: text;
cursor: text;
}

@ -1,3 +1,3 @@
#!/bin/bash
xterm -e "php -S localhost:2000"
xterm -e "php -S 0.0.0.0:2000"

@ -1,53 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ESP8266 Remote Terminal</title>
<meta name="viewport" content="width=device-width,shrink-to-fit=no,user-scalable=no,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0">
<script>
// Workaround for badly loaded page
setTimeout(function() {
if (typeof termInit == 'undefined' || typeof $ == 'undefined') {
location.reload(true);
}
}, 2000);
</script>
<link rel="stylesheet" href="/css/app.css">
<script src="/js/app.js"></script>
</head>
<body class="page-term">
<h1 onclick="location.href='/wifi'">ESP8266 Remote Terminal</h1>
<div id="termwrap">
<div id="screen"></div>
<div id="buttons">
<button data-n="1" class="btn-blue">1</button><!--
--><button data-n="2" class="btn-blue">2</button><!--
--><button data-n="3" class="btn-blue">3</button><!--
--><button data-n="4" class="btn-blue">4</button><!--
--><button data-n="5" class="btn-blue">5</button>
</div>
</div>
<nav id="botnav">
<a href="/wifi">WiFi config</a><!--
--><a href="/help">Help</a><!--
--><a href="/about">About</a>
</nav>
<script>
try {
_root = window.location.host;
termInit(%screenData%);
} catch(e) {
console.error("Fail, reloading...");
location.reload(true);
}
</script>
</body>
</html>

@ -1,17 +0,0 @@
<?php
require '_test_env.php';
$f = file_get_contents('term.html');
$f = str_replace('%screenData%',
'{
"w": 26, "h": 10,
"x": 0, "y": 0,
"cv": 1,
"screen": "70 t259"
}', $f);
$f = str_replace('window.location.host', json_encode(ESP_IP), $f);
echo $f;

@ -1,89 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,shrink-to-fit=no,user-scalable=no,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0">
<title>WiFi Settings - ESP8266 Remote Terminal</title>
<link rel="stylesheet" href="/css/app.css">
<script src="/js/app.js"></script>
</head>
<body class="page-wifi">
<img src="/img/loader.gif" alt="Loading…" id="loader">
<h1 onclick="location.href='/'">WiFi settings</h1>
<div class="Box" id="wificonfbox">
<table>
<tr>
<th>WiFi mode</th>
<td id="opmodebox">%WiFiMode%</td>
</tr>
<tr class="x-hide-noip x-hide-2">
<th>IP</th>
<td>%StaIP%</td>
</tr>
<tr>
<th>Switch to</th>
<td id="modeswitch"></td>
</tr>
<tr class="x-hide-1">
<th><label for="channel">AP channel</label></th>
<td>
<form action="/wifi/setchannel" method="GET">
<input name="ch" id="channel" type="number" step=1 min=1 max=14 value="%WiFiChannel%"><!--
--><input type="submit" value="Set" class="narrow btn-green x-hide-3">
</form>
</td>
</tr>
<tr class="x-hide-1">
<th><label for="channel">AP name</label></th>
<td>
<form action="/wifi/setname" method="GET">
<input name="name" type="text" value="%APName%"><!--
--><input type="submit" value="Set" class="narrow btn-green">
</form>
</td>
</tr>
<tr><td colspan=2 style="white-space: normal;">
<p>Some changes require a reboot, dropping connection. It can take a while to re-connect.</p>
<p>
<b>If you lose access</b>, hold the BOOT button for 2 seconds (the Tx LED starts blinking) to re-enable AP mode.
If that fails, hold the BOOT button for over 5 seconds (rapid Tx LED flashing) to perform a factory reset.
<p>
</td></tr>
</table>
</div>
<div class="Box" id="ap-box">
<h2>Select AP to join</h2>
<div id="ap-loader" class="x-hide-2">Scanning<span class="anim-dots">.</span></div>
<div id="ap-noscan" class="x-hide-1 x-hide-3">Can't scan in AP-only mode.</div>
<div id="ap-list" style="display:none"></div>
</div>
<nav id="botnav">
<a href="/">Terminal</a><!--
--><a href="/help">Help</a><!--
--><a href="/about">About</a>
</nav>
<div class="Modal hidden" id="psk-modal">
<div class="Dialog">
<form action="/wifi/connect" method="post" id="conn-form">
<input type="hidden" id="conn-essid" name="essid"><!--
--><label for="conn-passwd">Password:</label><!--
--><input type="password" id="conn-passwd" name="passwd"><!--
--><input type="submit" value="Connect!">
</form>
</div>
</div>
<script>
_root = window.location.host;
wifiInit({staSSID: '%StaSSID%', staIP: '%StaIP%', mode: '%WiFiModeNum%'});
</script>
</body>
</html>

@ -1,26 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,shrink-to-fit=no,user-scalable=no,initial-scale=1.0,maximum-scale=1.0,minimum-scale=1.0">
<title>Connecting…</title>
<link rel="stylesheet" href="css/app.css">
<script src="js/app.js"></script>
</head>
<body>
<h1>Connecting to network</h1>
<div class="Box">
<p><b>Status:</b><br><span id="status"></span><span class="anim-dots">.</span></p>
<a href="/wifi" id="backbtn" class="hidden button">Back to WiFi config</a>
</div>
<script>
_root = window.location.host;
wifiConn();
</script>
</body>
</html>

@ -1,15 +0,0 @@
<?php
require '_test_env.php';
$f = file_get_contents('wifi.html');
$f = str_replace('%StaSSID%', 'Chlivek', $f);
$f = str_replace('%StaIP%', json_encode(ESP_IP), $f);
$f = str_replace('%WiFiModeNum%', '1', $f);
$f = str_replace('%WiFiMode%', 'Client', $f);
$f = str_replace('%WiFiChannel%', '1', $f);
$f = str_replace('window.location.host', json_encode(ESP_IP), $f);
echo $f;

@ -0,0 +1,25 @@
//
// Created by MightyPork on 2017/07/23.
//
#ifndef ESP_VT100_FIRMWARE_HELPERS_H
#define ESP_VT100_FIRMWARE_HELPERS_H
// strcpy that adds 0 at the end of the buffer. Returns void.
#define strncpy_safe(dst, src, n) do { strncpy((char *)(dst), (char *)(src), (n)); (dst)[(n)-1]=0; } while (0)
/**
* Convert IP hex to arguments for printf.
* Library IP2STR(ip) does not work correctly due to unaligned memory access.
*/
#define GOOD_IP2STR(ip) ((ip)>>0)&0xff, ((ip)>>8)&0xff, ((ip)>>16)&0xff, ((ip)>>24)&0xff
/**
* Helper that retrieves an arg from `connData->getArgs` and stores it in `buff`. Returns 1 on success
*/
#define GET_ARG(key) (httpdFindArg(connData->getArgs, key, buff, sizeof(buff)) > 0)
#define STR_HELPER(x) #x
#define STR(x) STR_HELPER(x)
#endif //ESP_VT100_FIRMWARE_HELPERS_H

@ -1 +1 @@
Subproject commit 03003ea591a272df50159ba52f84ca84c5cad78e
Subproject commit f3dd1a25993775bec062a1906ced7b07a7fc9db1

@ -0,0 +1,178 @@
/*
Cgi/template routines for configuring non-wifi settings
*/
#include <esp8266.h>
#include "cgi_appcfg.h"
#include "persist.h"
#include "screen.h"
#include "helpers.h"
#define SET_REDIR_SUC "/cfg/app"
#define SET_REDIR_ERR SET_REDIR_SUC"?err="
/**
* Universal CGI endpoint to set Terminal params.
*/
httpd_cgi_state ICACHE_FLASH_ATTR
cgiAppCfgSetParams(HttpdConnData *connData)
{
char buff[50];
char redir_url_buf[300];
char *redir_url = redir_url_buf;
redir_url += sprintf(redir_url, SET_REDIR_ERR);
// we'll test if anything was printed by looking for \0 in failed_keys_buf
if (connData->conn == NULL) {
//Connection aborted. Clean up.
return HTTPD_CGI_DONE;
}
// width and height must always go together so we can do max size validation
if (GET_ARG("term_width")) {
dbg("Default screen width: %s", buff);
int w = atoi(buff);
if (w > 1) {
if (GET_ARG("term_height")) {
dbg("Default screen height: %s", buff);
int h = atoi(buff);
if (h > 1) {
if (w * h <= MAX_SCREEN_SIZE) {
termconf->width = w;
termconf->height = h;
} else {
warn("Bad dimensions: %d x %d (total %d)", w, h, w*h);
redir_url += sprintf(redir_url, "term_width,term_height,");
}
} else {
warn("Bad height: \"%s\"", buff);
redir_url += sprintf(redir_url, "term_width,");
}
} else {
warn("Missing height arg!");
// this wont happen normally when the form is used
redir_url += sprintf(redir_url, "term_width,term_height,");
}
} else {
warn("Bad width: \"%s\"", buff);
redir_url += sprintf(redir_url, "term_width,");
}
}
if (GET_ARG("default_bg")) {
dbg("Screen default BG: %s", buff);
int color = atoi(buff);
if (color >= 0 && color < 16) {
termconf->default_bg = (u8) color;
} else {
warn("Bad color %s", buff);
redir_url += sprintf(redir_url, "default_bg,");
}
}
if (GET_ARG("default_fg")) {
dbg("Screen default FG: %s", buff);
int color = atoi(buff);
if (color >= 0 && color < 16) {
termconf->default_fg = (u8) color;
} else {
warn("Bad color %s", buff);
redir_url += sprintf(redir_url, "default_fg,");
}
}
if (GET_ARG("term_title")) {
dbg("Terminal title default text: \"%s\"", buff);
strncpy_safe(termconf->title, buff, 64); // ATTN those must match the values in
}
if (GET_ARG("btn1")) {
dbg("Button1 default text: \"%s\"", buff);
strncpy_safe(termconf->btn1, buff, 10);
}
if (GET_ARG("btn2")) {
dbg("Button1 default text: \"%s\"", buff);
strncpy_safe(termconf->btn2, buff, 10);
}
if (GET_ARG("btn3")) {
dbg("Button1 default text: \"%s\"", buff);
strncpy_safe(termconf->btn3, buff, 10);
}
if (GET_ARG("btn4")) {
dbg("Button1 default text: \"%s\"", buff);
strncpy_safe(termconf->btn4, buff, 10);
}
if (GET_ARG("btn5")) {
dbg("Button1 default text: \"%s\"", buff);
strncpy_safe(termconf->btn5, buff, 10);
}
if (redir_url_buf[strlen(SET_REDIR_ERR)] == 0) {
// All was OK
info("Set app params - success, saving...");
terminal_apply_settings();
persist_store();
httpdRedirect(connData, SET_REDIR_SUC);
} else {
warn("Some settings did not validate, asking for correction");
// Some errors, appended to the URL as ?err=
httpdRedirect(connData, redir_url_buf);
}
return HTTPD_CGI_DONE;
}
httpd_cgi_state ICACHE_FLASH_ATTR
tplAppCfg(HttpdConnData *connData, char *token, void **arg)
{
#define BUFLEN 100
char buff[BUFLEN];
if (token == NULL) {
// We're done
return HTTPD_CGI_DONE;
}
strcpy(buff, ""); // fallback
if (streq(token, "term_width")) {
sprintf(buff, "%d", termconf->width);
}
else if (streq(token, "term_height")) {
sprintf(buff, "%d", termconf->height);
}
else if (streq(token, "default_bg")) {
sprintf(buff, "%d", termconf->default_bg);
}
else if (streq(token, "default_fg")) {
sprintf(buff, "%d", termconf->default_fg);
}
else if (streq(token, "term_title")) {
strncpy_safe(buff, termconf->title, BUFLEN);
}
else if (streq(token, "btn1")) {
strncpy_safe(buff, termconf->btn1, BUFLEN);
}
else if (streq(token, "btn2")) {
strncpy_safe(buff, termconf->btn2, BUFLEN);
}
else if (streq(token, "btn3")) {
strncpy_safe(buff, termconf->btn3, BUFLEN);
}
else if (streq(token, "btn4")) {
strncpy_safe(buff, termconf->btn4, BUFLEN);
}
else if (streq(token, "btn5")) {
strncpy_safe(buff, termconf->btn5, BUFLEN);
}
httpdSend(connData, buff, -1);
return HTTPD_CGI_DONE;
}

@ -0,0 +1,9 @@
#ifndef CGIAPPCFG_H
#define CGIAPPCFG_H
#include "httpd.h"
httpd_cgi_state cgiAppCfgSetParams(HttpdConnData *connData);
httpd_cgi_state tplAppCfg(HttpdConnData *connData, char *token, void **arg);
#endif

@ -5,9 +5,7 @@
#include "cgi_main.h"
#include "screen.h"
#include "user_main.h"
#define STR_HELPER(x) #x
#define STR(x) STR_HELPER(x)
#include "helpers.h"
/**
* Main page template substitution
@ -28,7 +26,33 @@ httpd_cgi_state ICACHE_FLASH_ATTR tplScreen(HttpdConnData *connData, char *token
const int bufsiz = 512;
char buff[bufsiz];
if (streq(token, "screenData")) {
if (streq(token, "term_title")) {
httpdSend(connData, termconf->title, -1);
}
else if (streq(token, "btn1")) {
httpdSend(connData, termconf->btn1, -1);
}
else if (streq(token, "btn2")) {
httpdSend(connData, termconf->btn2, -1);
}
else if (streq(token, "btn3")) {
httpdSend(connData, termconf->btn3, -1);
}
else if (streq(token, "btn4")) {
httpdSend(connData, termconf->btn4, -1);
}
else if (streq(token, "btn5")) {
httpdSend(connData, termconf->btn5, -1);
}
else if (streq(token, "default_bg")) {
sprintf(buff, "%d", termconf->default_bg);
httpdSend(connData, buff, -1);
}
else if (streq(token, "default_fg")) {
sprintf(buff, "%d", termconf->default_fg);
httpdSend(connData, buff, -1);
}
else if (streq(token, "screenData")) {
httpd_cgi_state cont = screenSerializeToBuffer(buff, bufsiz, arg);
httpdSend(connData, buff, -1);
return cont;

@ -0,0 +1,251 @@
/*
configuring the network settings
*/
#include <esp8266.h>
#include "cgi_network.h"
#include "wifimgr.h"
#include "persist.h"
#include "helpers.h"
#define SET_REDIR_SUC "/cfg/network"
#define SET_REDIR_ERR SET_REDIR_SUC"?err="
/**
* Callback for async timer
*/
static void ICACHE_FLASH_ATTR applyNetSettingsLaterCb(void *arg)
{
wifimgr_apply_settings();
}
/**
* Universal CGI endpoint to set network params.
* Those affect DHCP etc, may cause a disconnection.
*/
httpd_cgi_state ICACHE_FLASH_ATTR cgiNetworkSetParams(HttpdConnData *connData)
{
static ETSTimer timer;
char buff[50];
char redir_url_buf[300];
char *redir_url = redir_url_buf;
redir_url += sprintf(redir_url, SET_REDIR_ERR);
// we'll test if anything was printed by looking for \0 in failed_keys_buf
if (connData->conn == NULL) {
//Connection aborted. Clean up.
return HTTPD_CGI_DONE;
}
// ---- AP DHCP server lease time ----
if (GET_ARG("ap_dhcp_time")) {
dbg("Setting DHCP lease time to: %s min.", buff);
int min = atoi(buff);
if (min >= 1 && min <= 2880) {
if (wificonf->ap_dhcp_time != min) {
wificonf->ap_dhcp_time = (u16) min;
wifi_change_flags.ap = true;
}
} else {
warn("Lease time %s out of allowed range 1-2880.", buff);
redir_url += sprintf(redir_url, "ap_dhcp_time,");
}
}
// ---- AP DHCP start and end IP ----
if (GET_ARG("ap_dhcp_start")) {
dbg("Setting DHCP range start IP to: \"%s\"", buff);
u32 ip = ipaddr_addr(buff);
if (ip != 0) {
if (wificonf->ap_dhcp_range.start_ip.addr != ip) {
wificonf->ap_dhcp_range.start_ip.addr = ip;
wifi_change_flags.ap = true;
}
} else {
warn("Bad IP: %s", buff);
redir_url += sprintf(redir_url, "ap_dhcp_start,");
}
}
if (GET_ARG("ap_dhcp_end")) {
dbg("Setting DHCP range end IP to: \"%s\"", buff);
u32 ip = ipaddr_addr(buff);
if (ip != 0) {
if (wificonf->ap_dhcp_range.end_ip.addr != ip) {
wificonf->ap_dhcp_range.end_ip.addr = ip;
wifi_change_flags.ap = true;
}
} else {
warn("Bad IP: %s", buff);
redir_url += sprintf(redir_url, "ap_dhcp_end,");
}
}
// ---- AP local address & config ----
if (GET_ARG("ap_addr_ip")) {
dbg("Setting AP local IP to: \"%s\"", buff);
u32 ip = ipaddr_addr(buff);
if (ip != 0) {
if (wificonf->ap_addr.ip.addr != ip) {
wificonf->ap_addr.ip.addr = ip;
wificonf->ap_addr.gw.addr = ip; // always the same, we're the router here
wifi_change_flags.ap = true;
}
} else {
warn("Bad IP: %s", buff);
redir_url += sprintf(redir_url, "ap_addr_ip,");
}
}
if (GET_ARG("ap_addr_mask")) {
dbg("Setting AP local IP netmask to: \"%s\"", buff);
u32 ip = ipaddr_addr(buff);
if (ip != 0) {
if (wificonf->ap_addr.netmask.addr != ip) {
// ideally this should be checked to match the IP.
// Let's hope users know what they're doing
wificonf->ap_addr.netmask.addr = ip;
wifi_change_flags.ap = true;
}
} else {
warn("Bad IP mask: %s", buff);
redir_url += sprintf(redir_url, "ap_addr_mask,");
}
}
// ---- Station enable/disable DHCP ----
// DHCP enable / disable (disable means static IP is enabled)
if (GET_ARG("sta_dhcp_enable")) {
dbg("DHCP enable = %s", buff);
int enable = atoi(buff);
if (wificonf->sta_dhcp_enable != enable) {
wificonf->sta_dhcp_enable = (bool)enable;
wifi_change_flags.sta = true;
}
}
// ---- Station IP config (Static IP) ----
if (GET_ARG("sta_addr_ip")) {
dbg("Setting Station mode static IP to: \"%s\"", buff);
u32 ip = ipaddr_addr(buff);
if (ip != 0) {
if (wificonf->sta_addr.ip.addr != ip) {
wificonf->sta_addr.ip.addr = ip;
wifi_change_flags.sta = true;
}
} else {
warn("Bad IP: %s", buff);
redir_url += sprintf(redir_url, "sta_addr_ip,");
}
}
if (GET_ARG("sta_addr_mask")) {
dbg("Setting Station mode static IP netmask to: \"%s\"", buff);
u32 ip = ipaddr_addr(buff);
if (ip != 0 && ip != 0xFFFFFFFFUL) {
if (wificonf->sta_addr.netmask.addr != ip) {
wificonf->sta_addr.netmask.addr = ip;
wifi_change_flags.sta = true;
}
} else {
warn("Bad IP mask: %s", buff);
redir_url += sprintf(redir_url, "sta_addr_mask,");
}
}
if (GET_ARG("sta_addr_gw")) {
dbg("Setting Station mode static IP default gateway to: \"%s\"", buff);
u32 ip = ipaddr_addr(buff);
if (ip != 0) {
if (wificonf->sta_addr.gw.addr != ip) {
wificonf->sta_addr.gw.addr = ip;
wifi_change_flags.sta = true;
}
} else {
warn("Bad gw IP: %s", buff);
redir_url += sprintf(redir_url, "sta_addr_gw,");
}
}
if (redir_url_buf[strlen(SET_REDIR_ERR)] == 0) {
// All was OK
info("Set network params - success, applying in 1000 ms");
// Settings are applied only if all was OK
persist_store();
// Delayed settings apply, so the response page has a chance to load.
// If user connects via the Station IF, they may not even notice the connection reset.
os_timer_disarm(&timer);
os_timer_setfn(&timer, applyNetSettingsLaterCb, NULL);
os_timer_arm(&timer, 1000, false);
httpdRedirect(connData, SET_REDIR_SUC);
} else {
warn("Some WiFi settings did not validate, asking for correction");
// Some errors, appended to the URL as ?err=
httpdRedirect(connData, redir_url_buf);
}
return HTTPD_CGI_DONE;
}
//Template code for the WLAN page.
httpd_cgi_state ICACHE_FLASH_ATTR tplNetwork(HttpdConnData *connData, char *token, void **arg)
{
char buff[100];
u8 mac[6];
if (token == NULL) {
// We're done
return HTTPD_CGI_DONE;
}
strcpy(buff, ""); // fallback
if (streq(token, "ap_dhcp_time")) {
sprintf(buff, "%d", wificonf->ap_dhcp_time);
}
else if (streq(token, "ap_dhcp_start")) {
sprintf(buff, IPSTR, GOOD_IP2STR(wificonf->ap_dhcp_range.start_ip.addr));
}
else if (streq(token, "ap_dhcp_end")) {
sprintf(buff, IPSTR, GOOD_IP2STR(wificonf->ap_dhcp_range.end_ip.addr));
}
else if (streq(token, "ap_addr_ip")) {
sprintf(buff, IPSTR, GOOD_IP2STR(wificonf->ap_addr.ip.addr));
}
else if (streq(token, "ap_addr_mask")) {
sprintf(buff, IPSTR, GOOD_IP2STR(wificonf->ap_addr.netmask.addr));
}
else if (streq(token, "sta_dhcp_enable")) {
sprintf(buff, "%d", wificonf->sta_dhcp_enable);
}
else if (streq(token, "sta_addr_ip")) {
sprintf(buff, IPSTR, GOOD_IP2STR(wificonf->sta_addr.ip.addr));
}
else if (streq(token, "sta_addr_mask")) {
sprintf(buff, IPSTR, GOOD_IP2STR(wificonf->sta_addr.netmask.addr));
}
else if (streq(token, "sta_addr_gw")) {
sprintf(buff, IPSTR, GOOD_IP2STR(wificonf->sta_addr.gw.addr));
}
else if (streq(token, "sta_mac")) {
wifi_get_macaddr(STATION_IF, mac);
sprintf(buff, MACSTR, MAC2STR(mac));
}
else if (streq(token, "ap_mac")) {
wifi_get_macaddr(SOFTAP_IF, mac);
sprintf(buff, MACSTR, MAC2STR(mac));
}
httpdSend(connData, buff, -1);
return HTTPD_CGI_DONE;
}

@ -0,0 +1,9 @@
#ifndef CGINET_H
#define CGINET_H
#include "httpd.h"
httpd_cgi_state cgiNetworkSetParams(HttpdConnData *connData);
httpd_cgi_state tplNetwork(HttpdConnData *connData, char *token, void **arg);
#endif

@ -0,0 +1,74 @@
/*
Cgi/template routines for configuring non-wifi settings
*/
#include <esp8266.h>
#include "cgi_persist.h"
#include "persist.h"
#include "helpers.h"
#define SET_REDIR_SUC "/cfg/admin"
static bool ICACHE_FLASH_ATTR
verify_admin_pw(const char *pw)
{
// This is not really for security, but to prevent someone who
// shouldn't touch those settings from fucking it up.
return streq(pw, STR(ADMIN_PASSWORD)); // the PW comes from the makefile
}
httpd_cgi_state ICACHE_FLASH_ATTR
cgiPersistWriteDefaults(HttpdConnData *connData)
{
char buff[50];
if (connData->conn == NULL) {
//Connection aborted. Clean up.
return HTTPD_CGI_DONE;
}
// width and height must always go together so we can do max size validation
if (GET_ARG("pw")) {
dbg("Entered password for admin: %s", buff);
if (verify_admin_pw(buff)) {
dbg("pw is OK");
persist_set_as_default();
httpdRedirect(connData, SET_REDIR_SUC);
return HTTPD_CGI_DONE;
}
// if pw failed, show the same error as if it's wrong
}
httpdRedirect(connData, SET_REDIR_SUC "?err=Password"); // this will show in the "validation errors" box
return HTTPD_CGI_DONE;
}
httpd_cgi_state ICACHE_FLASH_ATTR
cgiPersistRestoreDefaults(HttpdConnData *connData)
{
if (connData->conn == NULL) {
//Connection aborted. Clean up.
return HTTPD_CGI_DONE;
}
persist_restore_default();
httpdRedirect(connData, SET_REDIR_SUC);
return HTTPD_CGI_DONE;
}
httpd_cgi_state ICACHE_FLASH_ATTR
cgiPersistRestoreHard(HttpdConnData *connData)
{
if (connData->conn == NULL) {
//Connection aborted. Clean up.
return HTTPD_CGI_DONE;
}
persist_restore_hard_default();
httpdRedirect(connData, SET_REDIR_SUC);
return HTTPD_CGI_DONE;
}

@ -0,0 +1,10 @@
#ifndef CGIPERSIST_H
#define CGIPERSIST_H
#include "httpd.h"
httpd_cgi_state cgiPersistWriteDefaults(HttpdConnData *connData);
httpd_cgi_state cgiPersistRestoreDefaults(HttpdConnData *connData);
httpd_cgi_state cgiPersistRestoreHard(HttpdConnData *connData);
#endif

@ -0,0 +1,585 @@
/*
Cgi/template routines for the /wifi url.
*/
/*
* ----------------------------------------------------------------------------
* "THE BEER-WARE LICENSE" (Revision 42):
* Jeroen Domburg <jeroen@spritesmods.com> wrote this file. As long as you retain
* this notice you can do whatever you want with this stuff. If we meet some day,
* and you think this stuff is worth it, you can buy me a beer in return.
* ----------------------------------------------------------------------------
*
* File adapted and improved by Ondřej Hruška <ondra@ondrovo.com>
*/
#include <esp8266.h>
#include "cgi_wifi.h"
#include "wifimgr.h"
#include "persist.h"
#include "helpers.h"
#define SET_REDIR_SUC "/cfg/wifi"
#define SET_REDIR_ERR SET_REDIR_SUC"?err="
/** WiFi access point data */
typedef struct {
char ssid[32];
char bssid[8];
int channel;
char rssi;
char enc;
} ApData;
/** Scan result type */
typedef struct {
char scanInProgress; //if 1, don't access the underlying stuff from the webpage.
ApData **apData;
int noAps;
} ScanResultData;
/** Static scan status storage. */
static ScanResultData cgiWifiAps;
/** Connection to AP periodic check timer */
static os_timer_t staCheckTimer;
/**
* Calculate approximate signal strength % from RSSI
*/
int ICACHE_FLASH_ATTR rssi2perc(int rssi)
{
int r;
if (rssi > 200)
r = 100;
else if (rssi < 100)
r = 0;
else
r = 100 - 2 * (200 - rssi); // approx.
if (r > 100) r = 100;
if (r < 0) r = 0;
return r;
}
/**
* Convert Auth type to string
*/
const ICACHE_FLASH_ATTR char *auth2str(AUTH_MODE auth)
{
switch (auth) {
case AUTH_OPEN:
return "Open";
case AUTH_WEP:
return "WEP";
case AUTH_WPA_PSK:
return "WPA";
case AUTH_WPA2_PSK:
return "WPA2";
case AUTH_WPA_WPA2_PSK:
return "WPA/WPA2";
default:
return "Unknown";
}
}
/**
* Convert WiFi opmode to string
*/
const ICACHE_FLASH_ATTR char *opmode2str(WIFI_MODE opmode)
{
switch (opmode) {
case NULL_MODE:
return "Disabled";
case STATION_MODE:
return "Client";
case SOFTAP_MODE:
return "AP only";
case STATIONAP_MODE:
return "Client+AP";
default:
return "Unknown";
}
}
/**
* Callback the code calls when a wlan ap scan is done. Basically stores the result in
* the static cgiWifiAps struct.
*
* @param arg - a pointer to {struct bss_info}, which is a linked list of the found APs
* @param status - OK if the scan succeeded
*/
void ICACHE_FLASH_ATTR wifiScanDoneCb(void *arg, STATUS status)
{
int n;
struct bss_info *bss_link = (struct bss_info *) arg;
dbg("wifiScanDoneCb %d", status);
if (status != OK) {
cgiWifiAps.scanInProgress = 0;
return;
}
// Clear prev ap data if needed.
if (cgiWifiAps.apData != NULL) {
for (n = 0; n < cgiWifiAps.noAps; n++) free(cgiWifiAps.apData[n]);
free(cgiWifiAps.apData);
}
// Count amount of access points found.
n = 0;
while (bss_link != NULL) {
bss_link = bss_link->next.stqe_next;
n++;
}
// Allocate memory for access point data
cgiWifiAps.apData = (ApData **) malloc(sizeof(ApData *) * n);
if (cgiWifiAps.apData == NULL) {
error("Out of memory allocating apData");
return;
}
cgiWifiAps.noAps = n;
info("Scan done: found %d APs", n);
// Copy access point data to the static struct
n = 0;
bss_link = (struct bss_info *) arg;
while (bss_link != NULL) {
if (n >= cgiWifiAps.noAps) {
// This means the bss_link changed under our nose. Shouldn't happen!
// Break because otherwise we will write in unallocated memory.
error("Huh? I have more than the allocated %d aps!", cgiWifiAps.noAps);
break;
}
// Save the ap data.
cgiWifiAps.apData[n] = (ApData *) malloc(sizeof(ApData));
if (cgiWifiAps.apData[n] == NULL) {
error("Can't allocate mem for ap buff.");
cgiWifiAps.scanInProgress = 0;
return;
}
cgiWifiAps.apData[n]->rssi = bss_link->rssi;
cgiWifiAps.apData[n]->channel = bss_link->channel;
cgiWifiAps.apData[n]->enc = bss_link->authmode;
strncpy(cgiWifiAps.apData[n]->ssid, (char *) bss_link->ssid, 32);
strncpy(cgiWifiAps.apData[n]->bssid, (char *) bss_link->bssid, 6);
bss_link = bss_link->next.stqe_next;
n++;
}
// We're done.
cgiWifiAps.scanInProgress = 0;
}
/**
* Routine to start a WiFi access point scan.
*/
static void ICACHE_FLASH_ATTR wifiStartScan(void)
{
if (cgiWifiAps.scanInProgress) return;
cgiWifiAps.scanInProgress = 1;
wifi_station_scan(NULL, wifiScanDoneCb);
}
/**
* This CGI is called from the bit of AJAX-code in wifi.tpl. It will initiate a
* scan for access points and if available will return the result of an earlier scan.
* The result is embedded in a bit of JSON parsed by the javascript in wifi.tpl.
*/
httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiScan(HttpdConnData *connData)
{
int pos = (int) connData->cgiData;
int len;
char buff[256];
// 2nd and following runs of the function via MORE:
if (!cgiWifiAps.scanInProgress && pos != 0) {
// Fill in json code for an access point
if (pos - 1 < cgiWifiAps.noAps) {
int rssi = cgiWifiAps.apData[pos - 1]->rssi;
len = sprintf(buff, "{\"essid\": \"%s\", \"bssid\": \""
MACSTR
"\", \"rssi\": %d, \"rssi_perc\": %d, \"enc\": %d, \"channel\": %d}%s",
cgiWifiAps.apData[pos - 1]->ssid,
MAC2STR(cgiWifiAps.apData[pos - 1]->bssid),
rssi,
rssi2perc(rssi),
cgiWifiAps.apData[pos - 1]->enc,
cgiWifiAps.apData[pos - 1]->channel,
(pos - 1 == cgiWifiAps.noAps - 1) ? "\n " : ",\n "); //<-terminator
httpdSend(connData, buff, len);
}
pos++;
if ((pos - 1) >= cgiWifiAps.noAps) {
len = sprintf(buff, " ]\n }\n}"); // terminate the whole object
httpdSend(connData, buff, len);
// Also start a new scan.
wifiStartScan();
return HTTPD_CGI_DONE;
}
else {
connData->cgiData = (void *) pos;
return HTTPD_CGI_MORE;
}
}
// First run of the function
httpdStartResponse(connData, 200);
httpdHeader(connData, "Content-Type", "application/json");
httpdEndHeaders(connData);
if (cgiWifiAps.scanInProgress == 1) {
// We're still scanning. Tell Javascript code that.
len = sprintf(buff, "{\n \"result\": {\n \"inProgress\": 1\n }\n}");
httpdSend(connData, buff, len);
return HTTPD_CGI_DONE;
}
else {
// We have a scan result. Pass it on.
len = sprintf(buff, "{\n \"result\": {\n \"inProgress\": 0,\n \"APs\": [\n ");
httpdSend(connData, buff, len);
if (cgiWifiAps.apData == NULL) cgiWifiAps.noAps = 0;
connData->cgiData = (void *) 1;
return HTTPD_CGI_MORE;
}
}
/**
* Cgi to get connection status.
*
* This endpoint returns JSON with keys:
* - status = 'idle', 'working' or 'fail',
* - ip = IP address, after connection succeeds
*/
httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiConnStatus(HttpdConnData *connData)
{
char buff[100];
struct ip_info info;
httpdStartResponse(connData, 200);
httpdHeader(connData, "Content-Type", "application/json");
httpdEndHeaders(connData);
// if bad opmode or no SSID configured, skip any checks
if (!(wificonf->opmode & STATION_MODE) || wificonf->sta_ssid[0] == 0) {
httpdSend(connData, "{\"status\": \"disabled\"}", -1);
return HTTPD_CGI_DONE;
}
STATION_STATUS st = wifi_station_get_connect_status();
switch(st) {
case STATION_IDLE:
sprintf(buff, "{\"status\": \"idle\"}"); // unclear when this is used
break;
case STATION_CONNECTING:
sprintf(buff, "{\"status\": \"working\"}");
break;
case STATION_WRONG_PASSWORD:
sprintf(buff, "{\"status\": \"fail\", \"cause\": \"WRONG_PASSWORD\"}");
break;
case STATION_NO_AP_FOUND:
sprintf(buff, "{\"status\": \"fail\", \"cause\": \"AP_NOT_FOUND\"}");
break;
case STATION_CONNECT_FAIL:
sprintf(buff, "{\"status\": \"fail\", \"cause\": \"CONNECTION_FAILED\"}");
break;
case STATION_GOT_IP:
wifi_get_ip_info(STATION_IF, &info);
sprintf(buff, "{\"status\": \"success\", \"ip\": \""IPSTR"\"}", GOOD_IP2STR(info.ip.addr));
break;
}
httpdSend(connData, buff, -1);
return HTTPD_CGI_DONE;
}
/**
* Callback for async timer
*/
static void ICACHE_FLASH_ATTR applyWifiSettingsLaterCb(void *arg)
{
(void*)arg;
wifimgr_apply_settings();
}
/**
* Universal CGI endpoint to set WiFi params.
* Note that some may cause a (delayed) restart.
*/
httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiSetParams(HttpdConnData *connData)
{
static ETSTimer timer;
char buff[50];
char redir_url_buf[300];
char *redir_url = redir_url_buf;
redir_url += sprintf(redir_url, SET_REDIR_ERR);
// we'll test if anything was printed by looking for \0 in failed_keys_buf
if (connData->conn == NULL) {
//Connection aborted. Clean up.
return HTTPD_CGI_DONE;
}
// ---- WiFi opmode ----
if (GET_ARG("opmode")) {
dbg("Setting WiFi opmode to: %s", buff);
int mode = atoi(buff);
if (mode > NULL_MODE && mode < MAX_MODE) {
wificonf->opmode = (WIFI_MODE) mode;
} else {
warn("Bad opmode value \"%s\"", buff);
redir_url += sprintf(redir_url, "opmode,");
}
}
if (GET_ARG("ap_enable")) {
dbg("Enable AP: %s", buff);
int enable = atoi(buff);
if (enable) {
wificonf->opmode |= SOFTAP_MODE;
} else {
wificonf->opmode &= ~SOFTAP_MODE;
}
}
if (GET_ARG("sta_enable")) {
dbg("Enable STA: %s", buff);
int enable = atoi(buff);
if (enable) {
wificonf->opmode |= STATION_MODE;
} else {
wificonf->opmode &= ~STATION_MODE;
}
}
// ---- AP transmit power ----
if (GET_ARG("tpw")) {
dbg("Setting AP power to: %s", buff);
int tpw = atoi(buff);
if (tpw >= 0 && tpw <= 82) { // 0 actually isn't 0 but quite low. 82 is very strong
if (wificonf->tpw != tpw) {
wificonf->tpw = (u8) tpw;
wifi_change_flags.ap = true;
}
} else {
warn("tpw %s out of allowed range 0-82.", buff);
redir_url += sprintf(redir_url, "tpw,");
}
}
// ---- AP channel (applies in AP-only mode) ----
if (GET_ARG("ap_channel")) {
info("ap_channel = %s", buff);
int channel = atoi(buff);
if (channel > 0 && channel < 15) {
if (wificonf->ap_channel != channel) {
wificonf->ap_channel = (u8) channel;
wifi_change_flags.ap = true;
}
} else {
warn("Bad channel value \"%s\", allowed 1-14", buff);
redir_url += sprintf(redir_url, "ap_channel,");
}
}
// ---- SSID name in AP mode ----
if (GET_ARG("ap_ssid")) {
// Replace all invalid ASCII with underscores
int i;
for (i = 0; i < 32; i++) {
char c = buff[i];
if (c == 0) break;
if (c < 32 || c >= 127) buff[i] = '_';
}
buff[i] = 0;
if (strlen(buff) > 0) {
if (!streq(wificonf->ap_ssid, buff)) {
info("Setting SSID to \"%s\"", buff);
strncpy_safe(wificonf->ap_ssid, buff, SSID_LEN);
wifi_change_flags.ap = true;
}
} else {
warn("Bad SSID len.");
redir_url += sprintf(redir_url, "ap_ssid,");
}
}
// ---- AP password ----
if (GET_ARG("ap_password")) {
// Users are free to use any stupid shit in ther password,
// but it may lock them out.
if (strlen(buff) == 0 || (strlen(buff) >= 8 && strlen(buff) < PASSWORD_LEN-1)) {
if (!streq(wificonf->ap_password, buff)) {
info("Setting AP password to \"%s\"", buff);
strncpy_safe(wificonf->ap_password, buff, PASSWORD_LEN);
wifi_change_flags.ap = true;
}
} else {
warn("Bad password len.");
redir_url += sprintf(redir_url, "ap_password,");
}
}
// ---- Hide AP network (do not announce) ----
if (GET_ARG("ap_hidden")) {
dbg("AP hidden = %s", buff);
int hidden = atoi(buff);
if (hidden != wificonf->ap_hidden) {
wificonf->ap_hidden = (hidden != 0);
wifi_change_flags.ap = true;
}
}
// ---- Station SSID (to connect to) ----
if (GET_ARG("sta_ssid")) {
if (!streq(wificonf->sta_ssid, buff)) {
// No verification needed, at worst it fails to connect
info("Setting station SSID to: \"%s\"", buff);
strncpy_safe(wificonf->sta_ssid, buff, SSID_LEN);
wifi_change_flags.sta = true;
}
}
// ---- Station password (empty for none is allowed) ----
if (GET_ARG("sta_password")) {
if (!streq(wificonf->sta_password, buff)) {
// No verification needed, at worst it fails to connect
info("Setting station password to: \"%s\"", buff);
strncpy_safe(wificonf->sta_password, buff, PASSWORD_LEN);
wifi_change_flags.sta = true;
}
}
if (redir_url_buf[strlen(SET_REDIR_ERR)] == 0) {
// All was OK
info("Set WiFi params - success, applying in 1000 ms");
// Settings are applied only if all was OK
//
// This is so that options that consist of multiple keys sent together are not applied
// only partially if set wrong, which could lead to eg. user losing access and having
// to reset to defaults.
persist_store();
// Delayed settings apply, so the response page has a chance to load.
// If user connects via the Station IF, they may not even notice the connection reset.
os_timer_disarm(&timer);
os_timer_setfn(&timer, applyWifiSettingsLaterCb, NULL);
os_timer_arm(&timer, 1000, false);
httpdRedirect(connData, SET_REDIR_SUC);
} else {
warn("Some WiFi settings did not validate, asking for correction");
// Some errors, appended to the URL as ?err=
httpdRedirect(connData, redir_url_buf);
}
return HTTPD_CGI_DONE;
}
//Template code for the WLAN page.
httpd_cgi_state ICACHE_FLASH_ATTR tplWlan(HttpdConnData *connData, char *token, void **arg)
{
char buff[100];
int x;
int connectStatus;
if (token == NULL) {
// We're done
return HTTPD_CGI_DONE;
}
strcpy(buff, ""); // fallback
if (streq(token, "opmode_name")) {
strcpy(buff, opmode2str(wificonf->opmode));
}
else if (streq(token, "opmode")) {
sprintf(buff, "%d", wificonf->opmode);
}
else if (streq(token, "sta_enable")) {
sprintf(buff, "%d", (wificonf->opmode & STATION_MODE) != 0);
}
else if (streq(token, "ap_enable")) {
sprintf(buff, "%d", (wificonf->opmode & SOFTAP_MODE) != 0);
}
else if (streq(token, "tpw")) {
sprintf(buff, "%d", wificonf->tpw);
}
else if (streq(token, "ap_channel")) {
sprintf(buff, "%d", wificonf->ap_channel);
}
else if (streq(token, "ap_ssid")) {
sprintf(buff, "%s", wificonf->ap_ssid);
}
else if (streq(token, "ap_password")) {
sprintf(buff, "%s", wificonf->ap_password);
}
else if (streq(token, "ap_hidden")) {
sprintf(buff, "%d", wificonf->ap_hidden);
}
else if (streq(token, "sta_ssid")) {
sprintf(buff, "%s", wificonf->sta_ssid);
}
else if (streq(token, "sta_password")) {
sprintf(buff, "%s", wificonf->sta_password);
}
else if (streq(token, "sta_rssi")) {
sprintf(buff, "%d", wifi_station_get_rssi());
}
else if (streq(token, "sta_active_ssid")) {
// For display of our current SSID
connectStatus = wifi_station_get_connect_status();
x = wifi_get_opmode();
if (x == SOFTAP_MODE || connectStatus != STATION_GOT_IP) {
strcpy(buff, "");
}
else {
struct station_config staconf;
wifi_station_get_config(&staconf);
strcpy(buff, (char *) staconf.ssid);
}
}
else if (streq(token, "sta_active_ip")) {
x = wifi_get_opmode();
connectStatus = wifi_station_get_connect_status();
if (x == SOFTAP_MODE || connectStatus != STATION_GOT_IP) {
strcpy(buff, "");
}
else {
struct ip_info info;
wifi_get_ip_info(STATION_IF, &info);
sprintf(buff, IPSTR, GOOD_IP2STR(info.ip.addr));
// sprintf(buff, "ip: "IPSTR", mask: "IPSTR", gw: "IPSTR,
// GOOD_IP2STR(info.ip.addr),
// GOOD_IP2STR(info.netmask.addr),
// GOOD_IP2STR(info.gw.addr));
}
}
httpdSend(connData, buff, -1);
return HTTPD_CGI_DONE;
}

@ -0,0 +1,13 @@
#ifndef CGIWIFI_H
#define CGIWIFI_H
#include "httpd.h"
#include "helpers.h"
httpd_cgi_state cgiWiFiScan(HttpdConnData *connData);
httpd_cgi_state cgiWiFiSetParams(HttpdConnData *connData);
httpd_cgi_state tplWlan(HttpdConnData *connData, char *token, void **arg);
httpd_cgi_state cgiWiFiConnStatus(HttpdConnData *connData);
#endif

@ -11,6 +11,8 @@
#include <esp8266.h>
#include "ansi_parser_callbacks.h"
#include "wifimgr.h"
#include "persist.h"
#define BTNGPIO 0
@ -74,16 +76,16 @@ static void ICACHE_FLASH_ATTR resetBtnTimerCb(void *arg) {
PIN_FUNC_SELECT(PERIPHS_IO_MUX_U0TXD_U, FUNC_U0TXD);
PIN_FUNC_SELECT(PERIPHS_IO_MUX_GPIO2_U, FUNC_U1TXD_BK);
if (resetCnt>=10) { //5 secs pressed - FR
if (resetCnt>=10) { //5 secs pressed - FR (timer is at 500 ms)
info("BOOT-button triggered FACTORY RESET!");
apars_handle_OSC_FactoryReset();
persist_restore_default();
}
else if (resetCnt>=2) { //1 sec pressed
wifi_station_disconnect();
wifi_set_opmode(STATIONAP_MODE); //reset to AP+STA mode
info("BOOT-button triggered reset to AP mode, restarting...");
info("BOOT-button triggered reset to AP mode...");
system_restart();
wificonf->opmode = STATIONAP_MODE;
persist_store();
wifimgr_apply_settings();
}
resetCnt=0;
}

@ -0,0 +1,171 @@
//
// Created by MightyPork on 2017/07/09.
//
#include "persist.h"
#include <esp8266.h>
#include "wifimgr.h"
#include "screen.h"
PersistBlock persist;
#define PERSIST_SECTOR_ID 0x3D
//region Persist and restore individual modules
static void ICACHE_FLASH_ATTR
apply_live_settings(void)
{
dbg("[Persist] Applying live settings...");
terminal_apply_settings();
wifimgr_apply_settings();
// ...
}
static void ICACHE_FLASH_ATTR
restore_live_settings_to_hard_defaults(void)
{
wifimgr_restore_defaults();
terminal_restore_defaults();
// ...
}
//endregion
/**
* Compute CRC32. Adapted from https://github.com/esp8266/Arduino
* @param data
* @param length
* @return crc32
*/
static uint32_t ICACHE_FLASH_ATTR
calculateCRC32(const uint8_t *data, size_t length)
{
uint32_t crc = 0xffffffff;
while (length--) {
uint8_t c = *data++;
for (uint32_t i = 0x80; i > 0; i >>= 1) {
bool bit = (bool) (crc & 0x80000000UL);
if (c & i) {
bit = !bit;
}
crc <<= 1;
if (bit) {
crc ^= 0x04c11db7UL;
}
}
}
return crc;
}
/**
* Compute a persist bundle checksum
*
* @param bundle
* @return
*/
static uint32_t ICACHE_FLASH_ATTR
compute_checksum(AppConfigBundle *bundle)
{
return calculateCRC32((uint8_t *) bundle, sizeof(AppConfigBundle) - 4) ^ CHECKSUM_SALT;
}
/**
* Load, verify and apply persistent config
*/
void ICACHE_FLASH_ATTR
persist_load(void)
{
info("[Persist] Loading stored settings from FLASH...");
dbg("sizeof(AppConfigBundle) = %d bytes", sizeof(AppConfigBundle));
dbg("sizeof(PersistBlock) = %d bytes", sizeof(PersistBlock));
dbg("sizeof(WiFiConfigBundle) = %d bytes", sizeof(WiFiConfigBundle));
dbg("sizeof(TerminalConfigBundle) = %d bytes", sizeof(TerminalConfigBundle));
bool hard_reset = false;
// Try to load
hard_reset |= !system_param_load(PERSIST_SECTOR_ID, 0, &persist, sizeof(PersistBlock));
// Verify checksums
if (hard_reset ||
(compute_checksum(&persist.defaults) != persist.defaults.checksum) ||
(compute_checksum(&persist.current) != persist.current.checksum)) {
error("[Persist] Checksum verification: FAILED");
hard_reset = true;
} else {
info("[Persist] Checksum verification: PASSED");
}
if (hard_reset) {
persist_restore_hard_default();
// this also stores them to flash and applies to modules
} else {
apply_live_settings();
}
info("[Persist] All settings loaded and applied.");
}
void ICACHE_FLASH_ATTR
persist_store(void)
{
info("[Persist] Storing all settings to FLASH...");
// Update checksums before write
persist.current.checksum = compute_checksum(&persist.current);
persist.defaults.checksum = compute_checksum(&persist.defaults);
if (!system_param_save_with_protect(PERSIST_SECTOR_ID, &persist, sizeof(PersistBlock))) {
error("[Persist] Store to flash failed!");
}
info("[Persist] All settings persisted.");
}
/**
* Restore to built-in defaults
*/
void ICACHE_FLASH_ATTR
persist_restore_hard_default(void)
{
info("[Persist] Restoring all settings to hard defaults...");
// Set live config to default values
restore_live_settings_to_hard_defaults();
// Store current -> default
memcpy(&persist.defaults, &persist.current, sizeof(AppConfigBundle));
persist_store();
info("[Persist] All settings restored to hard defaults.");
apply_live_settings(); // apply
}
/**
* Restore default settings & apply
*/
void ICACHE_FLASH_ATTR
persist_restore_default(void)
{
info("[Persist] Restoring live settings to stored defaults...");
memcpy(&persist.current, &persist.defaults, sizeof(AppConfigBundle));
apply_live_settings();
info("[Persist] Settings restored to stored defaults.");
}
/**
* Store current settings as defaults & write to flash
*/
void ICACHE_FLASH_ATTR
persist_set_as_default(void)
{
info("[Persist] Storing live settings as defaults..");
// current -> defaults
memcpy(&persist.defaults, &persist.current, sizeof(AppConfigBundle));
persist_store();
info("[Persist] Default settings updated.");
}

@ -0,0 +1,55 @@
//
// Created by MightyPork on 2017/07/09.
//
// There are 4 sets of settings.
// - hard defaults - hardcoded in firmware, used for init defaults after flash or if stored data are corrupt
// - defaults - persisted by privileged user
// - current - persistent current config state, can be restored to defaults any time
// - live - non-persistent settings valid only for the current runtime
#ifndef ESP_VT100_FIRMWARE_PERSIST_H
#define ESP_VT100_FIRMWARE_PERSIST_H
#include "wifimgr.h"
#include "screen.h"
// Changing this could be used to force-erase the config area
// after a firmware upgrade
#define CHECKSUM_SALT 0x5F5F5F5F
/** Struct for current or default settings */
typedef struct {
WiFiConfigBundle wificonf;
TerminalConfigBundle termconf;
// --- Space for future settings ---
// Original size: 1024
//
// The size must be appropriately reduced each time something is added,
// and boolean flags defaulting to 0 should be used to detect unpopulated
// sections that must be restored to defaults on load.
//
// This ensures user settings are not lost each time they upgrade the firmware,
// which would lead to a checksum mismatch if the structure was changed and
// it grew to a different memory area.
uint8_t filler[1024];
uint32_t checksum; // computed before write and tested on load. If it doesn't match, values are reset to hard defaults.
} AppConfigBundle;
/** This is the entire data block stored in FLASH */
typedef struct {
AppConfigBundle defaults; // defaults are stored here
AppConfigBundle current; // active settings adjusted by the user
} PersistBlock;
// Persist holds the data currently loaded from the flash
extern PersistBlock persist;
void persist_load(void);
void persist_restore_hard_default(void);
void persist_restore_default(void);
void persist_set_as_default(void);
void persist_store(void);
#endif //ESP_VT100_FIRMWARE_PERSIST_H

@ -2,15 +2,17 @@
#include <httpd.h>
#include <cgiwebsocket.h>
#include <httpdespfs.h>
#include <cgiwifi.h>
#include <auth.h>
#include "routes.h"
#include "cgi_wifi.h"
#include "cgi_reset.h"
#include "cgi_ping.h"
#include "cgi_main.h"
#include "cgi_sockets.h"
#include "cgi_network.h"
#include "cgi_appcfg.h"
#include "cgi_persist.h"
#define WIFI_PROTECT 0
#define WIFI_AUTH_NAME "wifi"
@ -27,30 +29,38 @@ HttpdBuiltInUrl routes[] = {
// --- Web pages ---
ROUTE_TPL_FILE("/", tplScreen, "/term.tpl"),
ROUTE_TPL_FILE("/about", tplAbout, "/about.tpl"),
ROUTE_FILE("/help", "/help.tpl"),
ROUTE_TPL_FILE("/about/?", tplAbout, "/about.tpl"),
ROUTE_FILE("/help/?", "/help.tpl"),
// --- Sockets ---
ROUTE_WS(URL_WS_UPDATE, updateSockConnect),
// --- System control ---
ROUTE_CGI("/system/reset", cgiResetDevice),
ROUTE_CGI("/system/ping", cgiPing),
ROUTE_CGI("/system/reset/?", cgiResetDevice),
ROUTE_CGI("/system/ping/?", cgiPing),
// --- WiFi config ---
// --- WiFi config --- (TODO make this conditional and configurable)
#if WIFI_PROTECT
ROUTE_AUTH("/wifi*", wifiPassFn),
#endif
ROUTE_REDIRECT("/wifi/", "/wifi"),
ROUTE_TPL_FILE("/wifi", tplWlan, "/wifi.tpl"),
ROUTE_REDIRECT("/cfg/?", "/cfg/wifi"),
ROUTE_TPL_FILE("/cfg/wifi/?", tplWlan, "/cfg_wifi.tpl"),
ROUTE_FILE("/cfg/wifi/connecting/?", "/cfg_wifi_conn.tpl"),
ROUTE_CGI("/cfg/wifi/scan", cgiWiFiScan),
ROUTE_CGI("/cfg/wifi/connstatus", cgiWiFiConnStatus),
ROUTE_CGI("/cfg/wifi/set", cgiWiFiSetParams),
ROUTE_TPL_FILE("/cfg/network/?", tplNetwork, "/cfg_network.tpl"),
ROUTE_CGI("/cfg/network/set", cgiNetworkSetParams),
ROUTE_TPL_FILE("/cfg/app/?", tplAppCfg, "/cfg_app.tpl"),
ROUTE_CGI("/cfg/app/set", cgiAppCfgSetParams),
ROUTE_CGI("/wifi/scan", cgiWiFiScan),
ROUTE_CGI("/wifi/connect", cgiWiFiConnect),
ROUTE_CGI("/wifi/connstatus", cgiWiFiConnStatus),
ROUTE_FILE("/wifi/connecting", "/wifi_conn.tpl"),
ROUTE_CGI("/wifi/setmode", cgiWiFiSetMode),
ROUTE_CGI("/wifi/setchannel", cgiWiFiSetChannel),
ROUTE_CGI("/wifi/setname", cgiWiFiSetSSID),
ROUTE_FILE("/cfg/admin/?", "/cfg_admin.tpl"),
ROUTE_CGI("/cfg/admin/write_defaults", cgiPersistWriteDefaults),
ROUTE_CGI("/cfg/admin/restore_defaults", cgiPersistRestoreDefaults),
ROUTE_CGI("/cfg/admin/restore_hard", cgiPersistRestoreHard),
ROUTE_FILESYSTEM(),
ROUTE_END(),

@ -1,9 +1,42 @@
#include <esp8266.h>
#include <httpd.h>
#include "screen.h"
#include "persist.h"
//region Data structures
TerminalConfigBundle * const termconf = &persist.current.termconf;
TerminalConfigBundle termconf_scratch;
/**
* Restore hard defaults
*/
void terminal_restore_defaults(void)
{
termconf->default_bg = 0;
termconf->default_fg = 7;
termconf->width = 26;
termconf->height = 10;
sprintf(termconf->title, "ESPTerm");
sprintf(termconf->btn1, "1");
sprintf(termconf->btn2, "2");
sprintf(termconf->btn3, "3");
sprintf(termconf->btn4, "4");
sprintf(termconf->btn5, "5");
}
/**
* Apply settings after eg. restore from defaults
*/
void terminal_apply_settings(void)
{
memcpy(&termconf_scratch, termconf, sizeof(TerminalConfigBundle));
screen_init();
}
#define W termconf_scratch.width
#define H termconf_scratch.height
/**
* Highest permissible value of the color attribute
*/
@ -50,16 +83,6 @@ static struct {
Color bg;
} cursor_sav;
/**
* Active screen width
*/
static int W = SCREEN_DEF_W;
/**
* Active screen height
*/
static int H = SCREEN_DEF_H;
// XXX volatile is probably not needed
static volatile int notifyLock = 0;
@ -99,8 +122,8 @@ cursor_reset(void)
{
cursor.x = 0;
cursor.y = 0;
cursor.fg = SCREEN_DEF_FG;
cursor.bg = SCREEN_DEF_BG;
cursor.fg = termconf_scratch.default_fg;
cursor.bg = termconf_scratch.default_bg;
cursor.visible = 1;
cursor.inverse = 0;
cursor.autowrap = 1;
@ -363,8 +386,8 @@ screen_cursor_save(bool withAttrs)
cursor_sav.bg = cursor.bg;
cursor_sav.inverse = cursor.inverse;
} else {
cursor_sav.fg = SCREEN_DEF_FG;
cursor_sav.bg = SCREEN_DEF_BG;
cursor_sav.fg = termconf_scratch.default_fg;
cursor_sav.bg = termconf_scratch.default_bg;
cursor_sav.inverse = 0;
}
}

@ -34,6 +34,31 @@
*
*/
typedef struct {
u32 width;
u32 height;
u8 default_bg;
u8 default_fg;
char title[64];
char btn1[10];
char btn2[10];
char btn3[10];
char btn4[10];
char btn5[10];
} TerminalConfigBundle;
// Live config
extern TerminalConfigBundle * const termconf;
/**
* Transient live config with no persist, can be modified via esc sequences.
* terminal_apply_settings() copies termconf to this struct, erasing old scratch changes
*/
extern TerminalConfigBundle termconf_scratch;
void terminal_restore_defaults(void);
void terminal_apply_settings(void);
/**
* Maximum screen size (determines size of the static data array)
*
@ -42,20 +67,13 @@
*/
#define MAX_SCREEN_SIZE (80*25)
#define SCREEN_DEF_W 26 //!< Default screen width
#define SCREEN_DEF_H 10 //!< Default screen height
#define SCREEN_DEF_BG 0 //!< Default screen background
#define SCREEN_DEF_FG 7 //!< Default screen foreground
typedef enum {
CLEAR_TO_CURSOR=0, CLEAR_FROM_CURSOR=1, CLEAR_ALL=2
} ClearMode;
typedef uint8_t Color;
httpd_cgi_state ICACHE_FLASH_ATTR
screenSerializeToBuffer(char *buffer, size_t buf_len, void **data);
httpd_cgi_state screenSerializeToBuffer(char *buffer, size_t buf_len, void **data);
/** Init the screen */
void screen_init(void);

@ -26,6 +26,8 @@
#include "user_main.h"
#include "uart_driver.h"
#include "ansi_parser_callbacks.h"
#include "wifimgr.h"
#include "persist.h"
#ifdef ESPFS_POS
CgiUploadFlashDef uploadParams={
@ -47,8 +49,6 @@ CgiUploadFlashDef uploadParams={
#define INCLUDE_FLASH_FNS
#endif
static ETSTimer prHeapTimer;
/** Periodically show heap usage */
static void ICACHE_FLASH_ATTR prHeapTimerCb(void *arg)
{
@ -77,11 +77,22 @@ static void ICACHE_FLASH_ATTR prHeapTimerCb(void *arg)
cnt++;
}
// Deferred init
static void user_start(void *unused);
//Main routine. Initialize stdout, the I/O, filesystem and the webserver and we're done.
void ICACHE_FLASH_ATTR user_init(void)
{
static ETSTimer userStartTimer;
static ETSTimer prHeapTimer;
serialInit();
// Prevent WiFi starting and connecting by default
// let wifi manager handle it
wifi_station_set_auto_connect(false);
wifi_set_opmode(NULL_MODE); // save to flash if changed - this might avoid the current spike on startup?
printf("\r\n");
banner("====== ESP8266 Remote Terminal ======");
banner_info("Firmware (c) Ondrej Hruska, 2017");
@ -94,15 +105,6 @@ void ICACHE_FLASH_ATTR user_init(void)
ioInit();
// Change AP name if AI-THINKER found (means un-initialized device)
struct softap_config apconf;
wifi_softap_get_config(&apconf);
if (strstarts((char*)apconf.ssid, "AI-THINKER")) {
warn("Un-initialized device, performing factory reset.");
apars_handle_OSC_FactoryReset();
return;
}
// 0x40200000 is the base address for spi flash memory mapping, ESPFS_POS is the position
// where image is written in flash that is defined in Makefile.
#ifdef ESPFS_POS
@ -111,17 +113,35 @@ void ICACHE_FLASH_ATTR user_init(void)
espFsInit((void *) (webpages_espfs_start));
#endif
// Captive portal
captdnsInit();
// Server
httpdInit(routes, 80);
// Heap use timer & blink
os_timer_disarm(&prHeapTimer);
os_timer_setfn(&prHeapTimer, prHeapTimerCb, NULL);
os_timer_arm(&prHeapTimer, 1000, 1);
// do later (some functions do not work if called from user_init)
os_timer_disarm(&userStartTimer);
os_timer_setfn(&userStartTimer, user_start, NULL);
os_timer_arm(&userStartTimer, 10, 0);
}
static void user_start(void *unused)
{
// Change AP name if AI-THINKER found (means un-initialized device)
// struct softap_config apconf;
// wifi_softap_get_config(&apconf);
// if (strstarts((char *) apconf.ssid, "AI-THINKER")) {
// warn("Un-initialized device, performing factory reset.");
// apars_handle_OSC_FactoryReset();
// return;
// }
// Load and apply stored settings, or defaults if stored settings are invalid
persist_load();
// Captive portal (DNS redirector)
captdnsInit();
// Server
httpdInit(routes, 80);
// The terminal screen
screen_init();

@ -0,0 +1,197 @@
//
// Created by MightyPork on 2017/07/08.
//
#include "wifimgr.h"
#include "persist.h"
WiFiConfigBundle * const wificonf = &persist.current.wificonf;
WiFiConfChangeFlags wifi_change_flags;
/**
* Restore defaults in the WiFi config block.
* This is to be called if the WiFi config is corrupted on startup,
* before applying the config.
*/
void ICACHE_FLASH_ATTR
wifimgr_restore_defaults(void)
{
u8 mac[6];
wifi_get_macaddr(SOFTAP_IF, mac);
wificonf->opmode = SOFTAP_MODE;
wificonf->tpw = 20;
wificonf->ap_channel = 1;
sprintf((char *) wificonf->ap_ssid, "TERM-%02X%02X%02X", mac[3], mac[4], mac[5]);
wificonf->ap_password[0] = 0; // PSK2 always if password is not null.
wificonf->ap_hidden = false;
IP4_ADDR(&wificonf->ap_addr.ip, 192, 168, 4, 1);
IP4_ADDR(&wificonf->ap_addr.netmask, 255, 255, 255, 0);
wificonf->ap_addr.gw.addr = wificonf->ap_addr.gw.addr;
IP4_ADDR(&wificonf->ap_dhcp_range.start_ip, 192, 168, 4, 100);
IP4_ADDR(&wificonf->ap_dhcp_range.end_ip, 192, 168, 4, 200);
wificonf->ap_dhcp_range.enable = 1; // this will never get changed, idk why it's even there
wificonf->ap_dhcp_time = 120;
// --- Client config ---
wificonf->sta_ssid[0] = 0;
wificonf->sta_password[0] = 0;
wificonf->sta_dhcp_enable = true;
IP4_ADDR(&wificonf->sta_addr.ip, 192, 168, 0, (mac[5] == 1 ? 2 : mac[5])); // avoid being the same as "default gw"
IP4_ADDR(&wificonf->sta_addr.netmask, 255, 255, 255, 0);
IP4_ADDR(&wificonf->sta_addr.gw, 192, 168, 0, 1);
// DEBUG ONLY - TODO remove for release
wificonf->opmode = STATION_MODE;
sprintf((char*)wificonf->sta_ssid, "Chlivek");
sprintf((char*)wificonf->sta_password, "prase chrochta");
}
static void ICACHE_FLASH_ATTR
configure_station(void)
{
info("[WiFi] Configuring Station mode...");
struct station_config conf;
strcpy((char *) conf.ssid, (char *) wificonf->sta_ssid);
strcpy((char *) conf.password, (char *) wificonf->sta_password);
dbg("[WiFi] Connecting to \"%s\", password \"%s\"", conf.ssid, conf.password);
conf.bssid_set = 0;
conf.bssid[0] = 0;
wifi_station_disconnect();
wifi_station_set_config_current(&conf);
if (wificonf->sta_dhcp_enable) {
dbg("[WiFi] Starting DHCP...");
if (!wifi_station_dhcpc_start()) {
error("[WiFi] DHCP failed to start!");
return;
}
}
else {
info("[WiFi] Setting up static IP...");
dbg("[WiFi] Client.ip = "IPSTR, GOOD_IP2STR(wificonf->sta_addr.ip.addr));
dbg("[WiFi] Client.mask = "IPSTR, GOOD_IP2STR(wificonf->sta_addr.netmask.addr));
dbg("[WiFi] Client.gw = "IPSTR, GOOD_IP2STR(wificonf->sta_addr.gw.addr));
wifi_station_dhcpc_stop();
// Load static IP config
if (!wifi_set_ip_info(STATION_IF, &wificonf->sta_addr)) {
error("[WiFi] Error setting static IP!");
return;
}
}
info("[WiFi] Trying to connect to AP...");
wifi_station_connect();
}
static void ICACHE_FLASH_ATTR
configure_ap(void)
{
bool suc;
info("[WiFi] Configuring SoftAP mode...");
// AP is enabled
struct softap_config conf;
conf.channel = wificonf->ap_channel;
strcpy((char *) conf.ssid, (char *) wificonf->ap_ssid);
strcpy((char *) conf.password, (char *) wificonf->ap_password);
conf.authmode = (wificonf->ap_password[0] == 0 ? AUTH_OPEN : AUTH_WPA2_PSK);
conf.ssid_len = (uint8_t) strlen((char *) conf.ssid);
conf.ssid_hidden = wificonf->ap_hidden;
conf.max_connection = 4; // default 4 (max possible)
conf.beacon_interval = 100; // default 100 ms
// Set config
//ETS_UART_INTR_DISABLE();
suc = wifi_softap_set_config_current(&conf);
//ETS_UART_INTR_ENABLE();
if (!suc) {
error("[WiFi] AP config set fail!");
return;
}
// Set IP
info("[WiFi] Configuring SoftAP local IP...");
dbg("[WiFi] SoftAP.ip = "IPSTR, GOOD_IP2STR(wificonf->ap_addr.ip.addr));
dbg("[WiFi] SoftAP.mask = "IPSTR, GOOD_IP2STR(wificonf->ap_addr.netmask.addr));
dbg("[WiFi] SoftAP.gw = "IPSTR, GOOD_IP2STR(wificonf->ap_addr.gw.addr));
wifi_softap_dhcps_stop();
// Configure DHCP
if (!wifi_set_ip_info(SOFTAP_IF, &wificonf->ap_addr)) {
error("[WiFi] IP set fail!");
return;
}
info("[WiFi] Configuring SoftAP DHCP server...");
dbg("[WiFi] DHCP.start = "IPSTR, GOOD_IP2STR(wificonf->ap_dhcp_range.start_ip.addr));
dbg("[WiFi] DHCP.end = "IPSTR, GOOD_IP2STR(wificonf->ap_dhcp_range.end_ip.addr));
dbg("[WiFi] DHCP.lease = %d minutes", wificonf->ap_dhcp_time);
if (!wifi_softap_set_dhcps_lease(&wificonf->ap_dhcp_range)) {
error("[WiFi] DHCP address range set fail!");
return;
}
if (!wifi_softap_set_dhcps_lease_time(wificonf->ap_dhcp_time)) {
error("[WiFi] DHCP lease time set fail!");
return;
}
// some weird magic shit about router
uint8 mode = 1;
wifi_softap_set_dhcps_offer_option(OFFER_ROUTER, &mode);
if (!wifi_softap_dhcps_start()) {
error("[WiFi] Failed to start DHCP server!");
return;
}
}
/**
* Register the WiFi event listener, cycle WiFi, apply settings
*/
void ICACHE_FLASH_ATTR
wifimgr_apply_settings(void)
{
info("[WiFi] Initializing...");
// Force wifi cycle
// Disconnect - may not be needed?
WIFI_MODE opmode = wifi_get_opmode();
bool is_sta = wificonf->opmode & STATION_MODE;
bool is_ap = wificonf->opmode & SOFTAP_MODE;
if ((wificonf->opmode & STATION_MODE) && !(opmode & STATION_MODE)) {
wifi_change_flags.sta = true;
}
if ((wificonf->opmode & SOFTAP_MODE) && !(opmode & SOFTAP_MODE)) {
wifi_change_flags.ap = true;
}
if (opmode != wificonf->opmode) {
wifi_set_opmode_current(wificonf->opmode);
}
// Configure the client
if (is_sta && wifi_change_flags.sta) {
configure_station();
}
// Configure the AP
if (is_ap && wifi_change_flags.ap) {
configure_ap();
}
wifi_change_flags.ap = false;
wifi_change_flags.sta = false;
info("[WiFi] WiFi settings applied.");
}

@ -0,0 +1,57 @@
//
// Created by MightyPork on 2017/07/08.
// This module handles all WiFi configuration and is interfaced
// by the cgi_wifi functions.
//
#ifndef ESP_VT100_FIRMWARE_WIFI_MANAGER_H
#define ESP_VT100_FIRMWARE_WIFI_MANAGER_H
#include <esp8266.h>
#include "cgi_wifi.h"
#define SSID_LEN 32
#define PASSWORD_LEN 64
/**
* A structure holding all configured WiFi parameters
* and the active state.
*
* This block can be used eg. for WiFi config backup.
*/
typedef struct {
WIFI_MODE opmode : 8;
u8 tpw;
// --- AP config ---
u8 ap_channel;
u8 ap_ssid[SSID_LEN];
u8 ap_password[PASSWORD_LEN];
bool ap_hidden;
u16 ap_dhcp_time; // in minutes
struct dhcps_lease ap_dhcp_range;
struct ip_info ap_addr;
// --- Client config ---
u8 sta_ssid[SSID_LEN];
u8 sta_password[PASSWORD_LEN];
bool sta_dhcp_enable;
struct ip_info sta_addr;
} WiFiConfigBundle;
typedef struct {
bool sta;
bool ap;
} WiFiConfChangeFlags;
extern WiFiConfChangeFlags wifi_change_flags;
extern WiFiConfigBundle * const wificonf;
void wifimgr_restore_defaults(void);
void wifimgr_apply_settings(void);
#endif //ESP_VT100_FIRMWARE_WIFI_MANAGER_H
Loading…
Cancel
Save