diff --git a/CMakeLists.txt b/CMakeLists.txt index ee1d441..6131385 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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}) diff --git a/Makefile b/Makefile index 5310111..edde199 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/build_web.sh b/build_web.sh index 1495c74..b794ccd 100755 --- a/build_web.sh +++ b/build_web.sh @@ -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 diff --git a/esphttpdconfig.mk b/esphttpdconfig.mk index 6f294de..edc6980 100644 --- a/esphttpdconfig.mk +++ b/esphttpdconfig.mk @@ -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" diff --git a/html_orig/.gitignore b/html_orig/.gitignore index dec88d1..0a0f73a 100644 --- a/html_orig/.gitignore +++ b/html_orig/.gitignore @@ -1 +1 @@ -_test_env.php +pages/_test_env.php diff --git a/html_orig/_debug_replacements.php b/html_orig/_debug_replacements.php new file mode 100644 index 0000000..e693ea1 --- /dev/null +++ b/html_orig/_debug_replacements.php @@ -0,0 +1,62 @@ + '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', +]; diff --git a/html_orig/_test_env.php.example b/html_orig/_env.php similarity index 100% rename from html_orig/_test_env.php.example rename to html_orig/_env.php diff --git a/html_orig/_env.php.example b/html_orig/_env.php.example new file mode 100644 index 0000000..4d38f09 --- /dev/null +++ b/html_orig/_env.php.example @@ -0,0 +1,3 @@ + $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; diff --git a/html_orig/about.html b/html_orig/about.html deleted file mode 100644 index d83f202..0000000 --- a/html_orig/about.html +++ /dev/null @@ -1,80 +0,0 @@ - - - - - About - ESP8266 Remote Terminal - - - - - - - -

About

- -
- -

ESP8266 Remote Terminal

- - - -

© Ondřej Hruška, 2017 <ondra@ondrovo.com>

- -

Katedra měření, FEL ČVUT
Department of Measurement, FEE CTU

-
- -
-

Firmware

- - - - - - - - - - - - - -
Firmwarev%vers_fw%, build %date% at %time%
libesphttpdv%vers_httpd%
ESP IoT SDKv%vers_sdk%
-
- -
-

Issues

-

- Please report any issues to the bugtracker or send them by e-mail. -

-

- Firmware updates can be downloaded from the releases page. - Flash the images using esptool. -

-
- -
-

Contributing

-

- Submit your improvements and ideas to the project on GitHub.
-

-
- -
-

Acknowledgements

-

- The webserver is based on a fork of the - esphttpd library by Jeroen Domburg (Sprite_tm). -

-

- Using (modified) JS library chibi.js by Kyle Barrow as a lightweight jQuery alternative. -

-
- - - - - diff --git a/html_orig/base.php b/html_orig/base.php new file mode 100644 index 0000000..925d057 --- /dev/null +++ b/html_orig/base.php @@ -0,0 +1,71 @@ +_env.php.example to _env.php 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 */ +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; +} diff --git a/html_orig/build_html.php b/html_orig/build_html.php new file mode 100755 index 0000000..d123e1e --- /dev/null +++ b/html_orig/build_html.php @@ -0,0 +1,37 @@ +/Uis'; + $s = preg_replace($pattern, '', $s); + + $pattern = '/(?:(?:\/\*(?:[^*]|(?:\*+[^*\/]))*\*+\/)|(?:(? $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(); diff --git a/html_orig/css/app.css b/html_orig/css/app.css index d058ead..7063fea 100644 --- a/html_orig/css/app.css +++ b/html_orig/css/app.css @@ -1,3 +1,4 @@ +@charset "UTF-8"; /* normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ /** * 1. Set default font family to sans-serif. @@ -307,90 +308,93 @@ td, th { padding: 0; } +@font-face { + font-family: 'fontello'; + src: url("data:application/octet-stream;base64,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") format("woff"), url("data:application/octet-stream;base64,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I7AEQ2CwASuwBSVhsAUlsAJiILAAUFiwQGBZZrABY7AEJmEgsAQlYGQjsAMlYGRQWCEbIyFZIyAgsAQmI0ZhOFktsDcssAAWICAgsAUmIC5HI0cjYSM8OC2wOCywABYgsAgjQiAgIEYjR7ABKyNhOC2wOSywABawAyWwAiVHI0cjYbAAVFguIDwjIRuwAiWwAiVHI0cjYSCwBSWwBCVHI0cjYbAGJbAFJUmwAiVhuQgACABjYyMgWGIbIVljuAQAYiCwAFBYsEBgWWawAWNgIy4jICA8ijgjIVktsDossAAWILAIQyAuRyNHI2EgYLAgYGawAmIgsABQWLBAYFlmsAFjIyAgPIo4LbA7LCMgLkawAiVGUlggPFkusSsBFCstsDwsIyAuRrACJUZQWCA8WS6xKwEUKy2wPSwjIC5GsAIlRlJYIDxZIyAuRrACJUZQWCA8WS6xKwEUKy2wPiywNSsjIC5GsAIlRlJYIDxZLrErARQrLbA/LLA2K4ogIDywBCNCijgjIC5GsAIlRlJYIDxZLrErARQrsARDLrArKy2wQCywABawBCWwBCYgLkcjRyNhsAlDKyMgPCAuIzixKwEUKy2wQSyxCAQlQrAAFrAEJbAEJSAuRyNHI2EgsAQjQrAJQysgsGBQWCCwQFFYswIgAyAbswImAxpZQkIjIEewBEOwAmIgsABQWLBAYFlmsAFjYCCwASsgiophILACQ2BkI7ADQ2FkUFiwAkNhG7ADQ2BZsAMlsAJiILAAUFiwQGBZZrABY2GwAiVGYTgjIDwjOBshICBGI0ewASsjYTghWbErARQrLbBCLLA1Ky6xKwEUKy2wQyywNishIyAgPLAEI0IjOLErARQrsARDLrArKy2wRCywABUgR7AAI0KyAAEBFRQTLrAxKi2wRSywABUgR7AAI0KyAAEBFRQTLrAxKi2wRiyxAAEUE7AyKi2wRyywNCotsEgssAAWRSMgLiBGiiNhOLErARQrLbBJLLAII0KwSCstsEossgAAQSstsEsssgABQSstsEwssgEAQSstsE0ssgEBQSstsE4ssgAAQistsE8ssgABQistsFAssgEAQistsFEssgEBQistsFIssgAAPistsFMssgABPistsFQssgEAPistsFUssgEBPistsFYssgAAQCstsFcssgABQCstsFgssgEAQCstsFkssgEBQCstsFossgAAQystsFsssgABQystsFwssgEAQystsF0ssgEBQystsF4ssgAAPystsF8ssgABPystsGAssgEAPystsGEssgEBPystsGIssDcrLrErARQrLbBjLLA3K7A7Ky2wZCywNyuwPCstsGUssAAWsDcrsD0rLbBmLLA4Ky6xKwEUKy2wZyywOCuwOystsGgssDgrsDwrLbBpLLA4K7A9Ky2waiywOSsusSsBFCstsGsssDkrsDsrLbBsLLA5K7A8Ky2wbSywOSuwPSstsG4ssDorLrErARQrLbBvLLA6K7A7Ky2wcCywOiuwPCstsHEssDorsD0rLbByLLMJBAIDRVghGyMhWUIrsAhlsAMkUHiwARUwLQBLuADIUlixAQGOWbABuQgACABjcLEABUKyAAEAKrEABUKzCgIBCCqxAAVCsw4AAQgqsQAGQroCwAABAAkqsQAHQroAQAABAAkqsQMARLEkAYhRWLBAiFixA2REsSYBiFFYugiAAAEEQIhjVFixAwBEWVlZWbMMAgEMKrgB/4WwBI2xAgBEAAA=") format("truetype"); } +/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ +/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ +/* +@media screen and (-webkit-min-device-pixel-ratio:0) { + @font-face { + font-family: 'fontello'; + src: url('../font/fontello.svg?60007293#fontello') format('svg'); + } +} +*/ +[class^="icn-"]:before, [class*=" icn-"]:before { + font-family: "fontello"; + font-style: normal; + font-weight: normal; + speak: none; + display: inline-block; + text-decoration: inherit; + width: 1em; + margin-right: .2em; + text-align: center; + /* opacity: .8; */ + /* For safety - reset parent styles, that can break glyph codes*/ + font-variant: normal; + text-transform: none; + /* fix buttons height, for twitter bootstrap */ + line-height: 1em; + /* Animation center compensation - margins should be symmetric */ + /* remove if not needed */ + margin-left: .2em; + /* you can be more comfortable with increased icons size */ + /* font-size: 120%; */ + /* Uncomment for 3D effect */ + /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ } + +.icn-persist:before { + content: '\e800'; } + +/* '' */ +.icn-ok:before { + content: '\e801'; } + +/* '' */ +.icn-donate:before { + content: '\e802'; } + +/* '' */ +.icn-back:before { + content: '\e80b'; } + +/* '' */ +.icn-about:before { + content: '\e81f'; } + +/* '' */ +.icn-help:before { + content: '\e826'; } + +/* '' */ +.icn-network:before { + content: '\e82e'; } + +/* '' */ +.icn-restore:before { + content: '\e836'; } + +/* '' */ +.icn-terminal:before { + content: '\f108'; } + +/* '' */ +.icn-github:before { + content: '\f113'; } + +/* '' */ +.icn-wifi:before { + content: '\f1eb'; } + +/* '' */ html { box-sizing: border-box; } *, *::after, *::before { box-sizing: inherit; } -.hidden { - display: none !important; } - -[onclick] { - cursor: pointer; } - -.Modal { - position: fixed; - width: 100%; - height: 100%; - left: 0; - top: 0; - right: 0; - bottom: 0; - display: flex; - justify-content: center; - align-items: center; - transition: opacity .5s; - background: rgba(0, 0, 0, 0.65); - opacity: 0; } - .Modal.visible { - opacity: 1; } - .Modal.hidden { - display: none; } - -.Dialog { - margin: 0.61805rem; - padding: 1rem; - overflow: hidden; - max-width: 100%; - max-height: 100%; - flex: 0 1 30rem; - background: #1c1c1e; - border-left: 6px solid #2972ba; - border-right: 6px solid #2972ba; - box-shadow: 0 0 2px 0 #434349, 0 0 6px 0 black; - border-radius: 6px; } - .Dialog h1, .Dialog h2 { - margin-top: 0; } - .Dialog p:last-child { - margin-bottom: 0; } - -/* -// "toast" -.NotifyMsg { - position: fixed; - bottom: dist(2); - padding: dist(-1) dist(0); - - // center horizontally - 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; - &.error { - background: #d03e42; - } - - color: white; - text-shadow: 0 0 2px black; - box-shadow: 0 0 6px 0 rgba(black, .6); - border-radius: 5px; - - max-width: 80%; - - @include media($phone) { - width: calc(100% - 1rem); - } - - transition: opacity .5s; - opacity: 0; - &.visible { opacity: 1 } - &.hidden { display: none } -} -*/ html { font-family: Arial, sans-serif; color: #D0D0D0; @@ -414,74 +418,141 @@ a:hover { color: #5abfff; text-decoration: underline; } -.Box { - display: block; - max-width: 900px; - margin-top: 1rem; - padding: 0.61805rem 1rem; - border-radius: 3px; - background-color: rgba(255, 255, 255, 0.07); } - @media screen and (max-width: 544px) { - .Box { - margin-top: 0.61805rem; - padding: 0.23608rem 0.38198rem; } } - .Box p:first-child { - margin-top: 0; } +.hidden { + display: none !important; } -body { +[onclick] { + cursor: pointer; } + +ul > * { + padding-top: .2em; + padding-bottom: .2em; } + +/* 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; } + +@media screen and (max-width: 544px) { + #outer { + display: block; + overflow-y: scroll; } } +#menu { + flex: 0 0 15rem; + background: #3983CD; } + #menu > * { + display: block; + text-decoration: none; + padding: 0.61805rem 1rem; + white-space: nowrap; + word-wrap: normal; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } + #menu #brand { + color: white; + background: #2b6aa8; + font-size: 120%; + text-align: center; + position: relative; + transition: none; + font-weight: bold; + margin-bottom: 1rem; } + @media screen and (max-width: 544px) { + #menu #brand { + background: #3983CD; + cursor: pointer; + margin-bottom: 0.38198rem; } + #menu #brand::after { + position: absolute; + color: rgba(0, 0, 0, 0.4); + right: 1rem; + content: '▸'; + top: 50%; + font-size: 120%; + font-weight: bold; + transform: translate(0, -50%) rotate(90deg); } } + #menu.expanded #brand { + background: #2b6aa8; } + @media screen and (max-width: 544px) { + #menu.expanded #brand:after { + transform: translate(-25%, -50%) rotate(-90deg); } } + #menu a { + font-size: 130%; + color: white; + transition: background-color 0.2s; + text-shadow: 0 0 5px rgba(0, 0, 0, 0.4); } + #menu a:hover, #menu a.selected { + background: #5badff; + text-shadow: 0 0 5px rgba(0, 0, 0, 0.6); } + #menu a.selected { + position: relative; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); } + #menu a:focus { + outline-color: #ff0099; } + #menu a::before { + vertical-align: -2px; + margin-left: 0; + margin-right: 15px; } + @media screen and (max-width: 544px) { + #menu a { + display: none; } + #menu a::before { + margin-left: 10px; } } + #menu.expanded a { + display: block; } + @media screen and (min-width: 545px) and (max-width: 1000px) { + #menu { + flex-basis: 10rem; } + #menu #brand { + font-size: 95%; + margin-bottom: 0.61805rem; } + #menu a { + font-size: 105%; } + #menu > * { + padding: 0.38198rem 0.61805rem; } } + +#content { + flex-grow: 1; position: relative; padding: 1rem; overflow-y: auto; } @media screen and (max-width: 544px) { - body { + #content { padding: 0.61805rem; } } - body > * { + #content > * { margin-left: auto; margin-right: auto; } - -h1, h2 { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; } - -h1 { - text-align: center; - font-size: 2.02729em; - margin-top: 0; - margin-bottom: 1rem; } + #content h1 { + text-align: center; + font-size: 2.2807em; + margin-top: 0; + margin-bottom: 1rem; } + #content h2 { + font-size: 1.42383em; + margin-bottom: 0.61805rem; } @media screen and (max-width: 544px) { - h1 { - font-size: 1.42383em; + #content h1 { + font-size: 1.80203em; + margin-bottom: 0.61805rem; } + #content h2 { + font-size: 1.26563em; margin-bottom: 0.61805rem; } } - @media screen and (min-width: 545px) and (max-width: 1000px) { - h1 { - font-size: 1.80203em; } } - -h2 { - font-size: 1.26563em; - margin-bottom: 0.61805rem; } - -td, th { - padding: 0.38198rem; - white-space: nowrap; } - @media screen and (max-width: 544px) { - td, th { - padding: 0.23608rem; } } - -tbody th { - text-align: right; - width: 130px; - color: white; } - @media screen and (max-width: 544px) { - tbody th { - width: auto; } } - -tbody td input[type="text"], tbody td input[type="number"] { - width: 10em; } - @media screen and (max-width: 544px) { - tbody td input[type="text"], tbody td input[type="number"] { - width: 8em; } } + #content td, #content th { + padding: 0.38198rem; } + #content tbody th { + text-align: right; + width: 160px; + color: white; } #loader { position: absolute; @@ -496,20 +567,151 @@ tbody td input[type="text"], tbody td input[type="number"] { #loader.show { opacity: 1; } -ul > * { - padding-top: .1em; - padding-bottom: .1em; } +.Box { + display: block; + max-width: 900px; + margin-top: 1rem; + padding: 0.61805rem 1rem; + border-radius: 3px; + background-color: rgba(255, 255, 255, 0.07); + box-shadow: 0 0 4px black; + border: 1px solid #4f4f4f; } + .Box::after { + content: ''; + display: block; + clear: both; } + @media screen and (max-width: 544px) { + .Box { + margin-top: 0.61805rem; } } + .Box h1, .Box h2 { + overflow: hidden; } + h1 + .Box { + margin-top: 0; } + .Box h2 { + margin-top: 0; + margin-bottom: 0 !important; } + .Box p:last-child { + margin-bottom: 0.5em; } + .Box.wide { + width: initial; + max-width: initial; } + .Box.medium { + max-width: 1200px; } + .Box.str { + position: relative; } + .Box.str .Row.buttons { + position: absolute; } + @media screen and (max-width: 544px) { + .Box.str .Row.buttons { + right: 1rem; + top: 1.8em; + margin: 1rem auto; } } + @media screen and (min-width: 545px) { + .Box.str .Row.buttons { + right: 0; + top: 0; + margin-top: 0.61805rem; } } + .Box.str.mobopen .Row.buttons { + top: 0; + margin-top: 0.61805rem; } + .Box .Row.explain { + max-width: 600px; + margin-left: 0; } + @media screen and (max-width: 544px) { + .Box .Row.explain { + margin-top: 60px; } } + .Box.mobopen .Row.explain { + margin-top: 12px; } + @media screen and (max-width: 544px) { + .Box.mobopen .Row.explain { + margin-top: 18px; } } -#botnav { - padding-top: 1.5em; - text-align: center; } - #botnav a { - padding: 0 0.38198rem; - text-decoration: underline; } - #botnav a, #botnav a:visited, #botnav a:link { - color: #2e4d6e; } - #botnav a:hover { - color: #5abfff; } +@media screen and (max-width: 544px) { + .Box.mobcol h2 { + position: relative; + cursor: pointer; + padding-right: 1.3rem; } + .Box.mobcol h2::after { + position: absolute; + right: 0; + content: '▸'; + top: 50%; + font-size: 120%; + font-weight: bold; + transform: translate(0, -50%) rotate(90deg); } + .Box.mobcol.expanded h2::after { + transform: translate(-25%, -50%) rotate(-90deg); + margin-bottom: 1rem; } + .Box.mobcol .Row { + display: none; } + .Box.mobcol #ap-box { + display: none; } + .Box.mobcol.expanded .Row { + display: flex; } + .Box.mobcol.expanded #ap-box { + display: block; } } +.Modal { + position: fixed; + width: 100%; + height: 100%; + left: 0; + top: 0; + right: 0; + bottom: 0; + display: flex; + justify-content: center; + align-items: center; + transition: opacity .5s; + background: rgba(0, 0, 0, 0.65); + opacity: 0; } + .Modal.visible { + opacity: 1; } + .Modal.hidden { + display: none; } + +.Dialog { + margin: 0.61805rem; + padding: 1rem; + overflow: hidden; + max-width: 100%; + max-height: 100%; + flex: 0 1 30rem; + background: #1c1c1e; + border-left: 6px solid #2972ba; + border-right: 6px solid #2972ba; + border-top: 1px solid #2972ba; + border-bottom: 1px solid #2972ba; + box-shadow: 0 0 6px 0 black; + border-radius: 6px; } + .Dialog h1, .Dialog h2 { + margin-top: 0; } + .Dialog p:last-child { + margin-bottom: 0; } + +.NotifyMsg { + position: fixed; + top: 1.618rem; + right: 2.61792rem; + padding: 0.61805rem 1rem; + -webkit-font-smoothing: subpixel-antialiased; + -webkit-transform: translateZ(0) scale(1, 1); + background: #3887d0; + color: white; + text-shadow: 0 0 2px black; + box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.6); + border-radius: 5px; + max-width: 80%; + transition: opacity .5s; + opacity: 0; } + .NotifyMsg.error { + background: #d03e42; } + @media screen and (max-width: 544px) { + .NotifyMsg { + width: calc(100% - 1rem); } } + .NotifyMsg.visible { + opacity: 1; } + .NotifyMsg.hidden { + display: none; } button, input[type=submit], .button { text-align: center; @@ -518,7 +720,6 @@ button, input[type=submit], .button { border-radius: 2px; padding: 0 0.6em; border: 0 none; - outline: 0 none !important; line-height: 1.8em; font-size: 1.1em; margin-bottom: 3px; @@ -527,7 +728,7 @@ button, input[type=submit], .button { -moz-user-select: none; -ms-user-select: none; user-select: none; - text-shadow: 1.5px 1.5px 2px rgba(0, 0, 0, 0.6); + text-shadow: 1.5px 1.5px 2px rgba(0, 0, 0, 0.4); background-color: #3983cd; box-shadow: 0 3px 0 #265f98; text-decoration: none !important; } @@ -536,6 +737,9 @@ button, input[type=submit], .button { top: 2px; } button.narrow, input[type=submit].narrow, .button.narrow { min-width: initial; } + button::before, input[type=submit]::before, .button::before { + vertical-align: -1px; + margin-left: 0; } button, button:link, button:visited, input[type=submit], input[type=submit]:link, input[type=submit]:visited, .button, .button:link, .button:visited { color: #FEFEFE; } button:hover, button:active, button.active, button.selected, input[type=submit]:hover, input[type=submit]:active, input[type=submit].active, input[type=submit].selected, .button:hover, .button:active, .button.active, .button.selected { @@ -545,6 +749,8 @@ button, input[type=submit], .button { box-shadow: 0 3px 0 #154c80; } button:active, input[type=submit]:active, .button:active { box-shadow: 0 1px 0 #154c80; } + button:focus, input[type=submit]:focus, .button:focus { + outline-color: #ff0099; } button, input[type=submit], .button { text-align: center; @@ -553,7 +759,6 @@ button, input[type=submit], .button { border-radius: 2px; padding: 0 0.6em; border: 0 none; - outline: 0 none !important; line-height: 1.8em; font-size: 1.1em; margin-bottom: 3px; @@ -562,7 +767,7 @@ button, input[type=submit], .button { -moz-user-select: none; -ms-user-select: none; user-select: none; - text-shadow: 1.5px 1.5px 2px rgba(0, 0, 0, 0.6); + text-shadow: 1.5px 1.5px 2px rgba(0, 0, 0, 0.4); background-color: #3983cd; box-shadow: 0 3px 0 #265f98; text-decoration: none !important; } @@ -571,6 +776,9 @@ button, input[type=submit], .button { top: 2px; } button.narrow, input[type=submit].narrow, .button.narrow { min-width: initial; } + button::before, input[type=submit]::before, .button::before { + vertical-align: -1px; + margin-left: 0; } button, button:link, button:visited, input[type=submit], input[type=submit]:link, input[type=submit]:visited, .button, .button:link, .button:visited { color: #FEFEFE; } button:hover, button:active, button.active, button.selected, input[type=submit]:hover, input[type=submit]:active, input[type=submit].active, input[type=submit].selected, .button:hover, .button:active, .button.active, .button.selected { @@ -580,25 +788,176 @@ button, input[type=submit], .button { box-shadow: 0 3px 0 #154c80; } button:active, input[type=submit]:active, .button:active { box-shadow: 0 1px 0 #154c80; } + button:focus, input[type=submit]:focus, .button:focus { + outline-color: #ff0099; } input[type="number"], input[type="password"], input[type="text"], textarea, select { border: 0 none; - border-bottom: 2px solid #214e7a; - background-color: #303030; + border-bottom: 2px solid #2972ba; + background-color: #3c3c3c; color: white; padding: 6px; line-height: 1em; - outline: 0 none !important; - -moz-outline: 0 none !important; font-weight: normal; } input[type="number"]:focus, input[type="number"]:hover, input[type="password"]:focus, input[type="password"]:hover, input[type="text"]:focus, input[type="text"]:hover, textarea:focus, textarea:hover, select:focus, select:hover { - border-bottom-color: #2972ba; } + border-bottom-color: #2ea1f9; } + +.Row.checkbox { + line-height: 27px; } + .Row.checkbox .box { + overflow: hidden; + width: 27px; + height: 27px; + border: 1px solid #808080; + border-radius: 3px; + background: #3c3c3c; + display: inline-block; + position: relative; + cursor: pointer; + color: #2ea1f9; } + .Row.checkbox .box::before { + font-weight: bold; + position: absolute; + content: '×'; + left: 0; + top: 0; + right: 0; + bottom: 0; + line-height: 26px; + text-align: center; + font-size: 27px; + vertical-align: middle; + display: none; } + .Row.checkbox .box.checked::before { + display: block; } + +.Row.range .display { + margin-left: 1ex; } +.Row.range label .display { + font-weight: normal; } #psk-modal form > *, #wificonfbox form > * { margin-right: 0.38198rem; } #psk-modal form > *:last-child, #wificonfbox form > *:last-child { margin-right: 0; } +form { + border: 0 none; + margin: 0; + padding: 0; + text-decoration: none; } + +input[type="number"], input[type="password"], input[type="text"], textarea, select, label.select-wrap { + width: 250px; } + +input[type="number"], input.short { + width: 125px; } + +.Box.errors .list { + color: crimson; + font-weight: bold; } +.Box.errors .lead { + color: white; } + +.Row { + vertical-align: middle; + margin: 12px auto; + text-align: left; + display: flex; + flex-direction: row; + align-items: center; } + .Row:first-child { + margin-top: 0; } + .Row:last-child { + margin-bottom: 0; } + .Row .spacer { + width: 160px; } + @media screen and (max-width: 544px) { + .Row .spacer { + display: none; } } + .Row.buttons { + margin: 16px auto; } + .Row.buttons input, .Row.buttons .button { + margin-right: 0.61805rem; } + .Row.centered { + justify-content: center; } + .Row.message { + font-size: 1em; + text-shadow: 1px 1px 3px black; + text-align: center; } + .Row.message.error { + color: crimson; } + .Row.message.ok { + color: #0fe851; } + .Row.separator { + padding-top: 14px; + border-top: 2px solid rgba(255, 255, 255, 0.1); } + .Row textarea { + display: inline-block; + vertical-align: top; + min-height: 10rem; + flex-grow: 1; + resize: vertical; } + .Row label { + font-weight: bold; + color: white; + display: inline-block; + width: 160px; + text-align: right; + text-shadow: 1px 1px 3px black; + padding: 8px; + align-self: flex-start; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + white-space: nowrap; + word-wrap: normal; } + .Row label.error { + color: crimson; } + .Row input[type="range"] { + width: 200px; } + @media screen and (max-width: 544px) { + .Row { + flex-direction: column; + margin: 6px auto; } + .Row.buttons, .Row.centered, .Row.checkbox { + flex-direction: row; } + .Row.buttons { + justify-content: center; } + .Row.buttons :last-child { + margin-right: 0; } + .Row label { + padding-left: 0; + text-align: left; + width: auto; } + .Row .checkbox-wrap { + order: 1; + text-align: left; + padding-bottom: 0; + border-radius: .4px; + width: auto; } + .Row .checkbox-wrap + label { + width: auto; } + .Row input[type="number"], .Row input[type="password"], .Row input[type="text"], .Row textarea, .Row input[type="range"], .Row textarea, .Row select { + width: 100%; } } + +form span.required { + color: red; } + +.RadioGroup { + display: inline-block; + line-height: 1.5em; + vertical-align: middle; } + .RadioGroup label { + width: auto; + text-align: left; + cursor: pointer; + font-weight: normal; } + .RadioGroup input[type="radio"] { + vertical-align: middle; + margin: 0 0 0 5px; } + #ap-list { column-count: 3; column-gap: 0; @@ -610,17 +969,23 @@ input[type="number"], input[type="password"], input[type="text"], textarea, sele #ap-list { column-count: 1; } } -#ap-loader, #ap-noscan { +#ap-loader, #ap-noscan, #ap-scan { background: rgba(255, 255, 255, 0.1); border-radius: 5px; padding: 0.38198rem; - margin-bottom: 0.38198rem; } + margin-bottom: 0.38198rem; + margin-top: 0.38198rem; } #ap-noscan { font-weight: bold; } #ap-box { - padding-bottom: 0.38198rem; } + padding-top: 0.38198rem; } + #ap-box label { + display: block; + color: white; + font-weight: bold; + margin-bottom: 0.23608rem; } #psk-modal form { display: flex; @@ -629,6 +994,40 @@ input[type="number"], input[type="password"], input[type="text"], textarea, sele #psk-modal form input[type=password] { min-width: 5rem; } +.AP .inner, .AP-preview .wrap { + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + position: relative; + border-radius: 3px; + color: #222; + background: #afafaf; + transition: background-color 0.5s; + display: flex; } + .AP .inner:active, .AP-preview .wrap:active { + left: 0; + top: 1px; } + .AP .inner:hover, .AP-preview .wrap:hover { + background: white; } + .AP .inner .rssi, .AP-preview .wrap .rssi { + min-width: 2.5rem; + flex: 0 0 15%; + text-align: right; } + .AP .inner .rssi:after, .AP-preview .wrap .rssi:after { + padding-left: 0.09018rem; + content: '%'; + font-size: 0.88889em; } + .AP .inner .essid, .AP-preview .wrap .essid { + flex: 1 1 70%; + min-width: 0; + text-overflow: ellipsis; + overflow: hidden; + font-weight: bold; } + .AP .inner .auth, .AP-preview .wrap .auth { + flex: 0 0 15%; } + .AP { break-inside: avoid-column; max-width: 500px; @@ -637,50 +1036,62 @@ input[type="number"], input[type="password"], input[type="text"], textarea, sele background: #42a6f9 !important; cursor: default; top: 0 !important; } - .AP .inner { - cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - position: relative; - border-radius: 3px; - color: #222; - background: #afafaf; - transition: background-color 0.5s; - display: flex; } - .AP .inner:active { - left: 0; - top: 1px; } - .AP .inner:hover { - background: white; } - .AP .inner > * { + .AP .inner > * { + padding: 0.61805rem; + white-space: nowrap; + word-wrap: normal; } + +.AP-preview-nil { + padding: 8px; + border-radius: 5px; + border: 1px dashed #ddd; + width: 250px; + height: 94px; } + +.AP-preview .wrap { + flex-direction: row; + background: #ddd !important; + cursor: default; + top: 0 !important; + overflow: hidden; } + .AP-preview .wrap .inner { + display: flex; + flex-direction: column; } + .AP-preview .wrap .inner > * { padding: 0.61805rem; white-space: nowrap; word-wrap: normal; } - .AP .inner .rssi { - min-width: 2.5rem; - flex: 0 0 15%; - text-align: right; } - .AP .inner .rssi:after { - padding-left: 0.09018rem; - content: '%'; - font-size: 0.88889em; } - .AP .inner .essid { - flex: 1 1 70%; - min-width: 0; - text-overflow: ellipsis; - overflow: hidden; - font-weight: bold; } - .AP .inner .auth { - flex: 0 0 15%; } - -.page-term h1 { + .AP-preview .wrap .forget { + align-self: stretch; + line-height: 100%; + padding: 0.61805rem; + border-left: 1px solid #bbb; + display: flex; + align-items: center; + font-size: 28px; } + .AP-preview .wrap .forget, .AP-preview .wrap .forget:hover { + color: black; + text-decoration: none; } + .AP-preview .wrap .forget:hover { + background: #dc4a6a; + color: white; + border-left: 1px solid #666; + border-bottom-right-radius: 3px; + border-top-right-radius: 3px; } + .AP-preview .wrap .forget:active { + position: relative; + padding-top: calc(0.61805rem + 1px); } + .AP-preview .wrap .essid, .AP-preview .wrap .passwd, .AP-preview .wrap .nopasswd { + padding-bottom: 0; } + .AP-preview .wrap .x-passwd { + font-family: monospace; } + +body.term h1 { font-size: 1.80203em; } @media screen and (max-width: 544px) { - .page-term h1 { + body.term h1 { font-size: 1.42383em; } } -.page-term #screen { +body.term #screen { -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; @@ -692,25 +1103,33 @@ input[type="number"], input[type="password"], input[type="text"], textarea, sele padding: 6px; display: inline-block; border: 2px solid #3983CD; } - .page-term #screen span { + body.term #screen span { white-space: pre; cursor: pointer; } - .page-term #screen span:hover { + body.term #screen span:hover { outline: 1px solid rgba(255, 255, 255, 0.4); } @media screen and (max-width: 544px) { - .page-term #screen span:hover { + body.term #screen span:hover { outline: 0 none; } } -.page-term #buttons { +body.term #buttons { margin-top: 10px; white-space: nowrap; } - .page-term #buttons button { + body.term #buttons button { margin: 0 3px; - padding: 10px 0; - width: 18%; - max-width: 65px; - min-width: initial; + padding: 8px 5px; + min-width: 65px; cursor: pointer; font-weight: bold; } +body.term #botnav { + padding-top: 1.5em; + text-align: center; } + body.term #botnav a { + padding: 0 0.38198rem; + text-decoration: underline; } + body.term #botnav a, body.term #botnav a:visited, body.term #botnav a:link { + color: #2e4d6e; } + body.term #botnav a:hover { + color: #5abfff; } #termwrap { text-align: center; } @@ -724,7 +1143,8 @@ input[type="number"], input[type="password"], input[type="text"], textarea, sele float: right; height: 130px; } .page-about #logo2 { - max-width: 100%; } + max-width: 100%; + margin: 1rem; } .page-about td { white-space: normal; } @@ -856,13 +1276,15 @@ input[type="number"], input[type="password"], input[type="text"], textarea, sele @media screen and (min-width: 545px) { .mq-phone { - display: none; } } + display: none !important; } } @media screen and (max-width: 544px) { - .mq-tablet-min { - display: none; } } + .mq-tablet-min, .mq-no-phone { + display: none !important; } } @media screen and (min-width: 1001px) { .mq-tablet-max { - display: none; } } + display: none !important; } } @media screen and (max-width: 1000px) { .mq-normal-min { - display: none; } } + display: none !important; } } + +/*# sourceMappingURL=app.css.map */ diff --git a/html_orig/css/app.css.bak b/html_orig/css/app.css.bak new file mode 100644 index 0000000..5baf098 --- /dev/null +++ b/html_orig/css/app.css.bak @@ -0,0 +1,1284 @@ +@charset "UTF-8"; +/* normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */ +/** + * 1. Set default font family to sans-serif. + * 2. Prevent iOS and IE text size adjust after device orientation change, + * without disabling user zoom. + */ +*, *:before, *:after { + box-sizing: border-box; } + +html { + font-family: sans-serif; + /* 1 */ + -ms-text-size-adjust: 100%; + /* 2 */ + -webkit-text-size-adjust: 100%; + /* 2 */ } + +/** + * Remove default margin. + */ +body { + margin: 0; } + +/* HTML5 display definitions + ========================================================================== */ +/** + * Correct `block` display not defined for any HTML5 element in IE 8/9. + * Correct `block` display not defined for `details` or `summary` in IE 10/11 + * and Firefox. + * Correct `block` display not defined for `main` in IE 11. + */ +figure, +nav + { + display: block; } + +/** + * 1. Correct `inline-block` display not defined in IE 8/9. + * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. + */ +canvas, +progress + { + display: inline-block; + /* 1 */ + vertical-align: baseline; + /* 2 */ } + +/** + * Prevent modern browsers from displaying `audio` without controls. + * Remove excess height in iOS 5 devices. + */ +/** + * Address `[hidden]` styling not present in IE 8/9/10. + * Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22. + */ +[hidden] + { + display: none; } + +/* Links + ========================================================================== */ +/** + * Remove the gray background color from active links in IE 10. + */ +a { + background-color: transparent; } + +/** + * Improve readability of focused elements when they are also in an + * active/hover state. + */ +a:active, +a:hover { + outline: 0; } + +/* Text-level semantics + ========================================================================== */ +/** + * Address styling not present in IE 8/9/10/11, Safari, and Chrome. + */ +/** + * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. + */ +b + { + font-weight: bold; } + +/** + * Address styling not present in Safari and Chrome. + */ +/** + * Address variable `h1` font-size and margin within `section` and `article` + * contexts in Firefox 4+, Safari, and Chrome. + */ +h1 { + font-size: 2em; + margin: 0.67em 0; } + +h2 { + font-size: 2em; + margin: 0.67em 0; } + +/** + * Address styling not present in IE 8/9. + */ +/** + * Address inconsistent and variable font size in all browsers. + */ +small { + font-size: 80%; } + +/** + * Prevent `sub` and `sup` affecting `line-height` in all browsers. + */ +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; } + +sup { + top: -0.5em; } + +sub { + bottom: -0.25em; } + +/* Embedded content + ========================================================================== */ +/** + * Remove border when inside `a` element in IE 8/9/10. + */ +img { + border: 0; } + +/** + * Correct overflow not hidden in IE 9/10/11. + */ +svg:not(:root) { + overflow: hidden; } + +/* Grouping content + ========================================================================== */ +/** + * Address margin not present in IE 8/9 and Safari. + */ +/** + * Address differences between Firefox and other browsers. + */ +hr { + box-sizing: content-box; + height: 0; } + +/** + * Contain overflow in all browsers. + */ +pre { + overflow: auto; } + +/** + * Address odd `em`-unit font size rendering in all browsers. + */ +code, +pre + { + font-family: monospace; + font-size: 1em; } + +/* Forms + ========================================================================== */ +/** + * Known limitation: by default, Chrome and Safari on OS X allow very limited + * styling of `select`, unless a `border` property is set. + */ +/** + * 1. Correct color not being inherited. + * Known issue: affects color of disabled elements. + * 2. Correct font properties not being inherited. + * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. + */ +button, +input, +select, +textarea { + color: inherit; + /* 1 */ + font: inherit; + /* 2 */ + margin: 0; + /* 3 */ } + +/** + * Address `overflow` set to `hidden` in IE 8/9/10/11. + */ +button { + overflow: visible; } + +/** + * Address inconsistent `text-transform` inheritance for `button` and `select`. + * All other form control elements do not inherit `text-transform` values. + * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. + * Correct `select` style inheritance in Firefox. + */ +button, +select { + text-transform: none; } + +/** + * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` + * and `video` controls. + * 2. Correct inability to style clickable `input` types in iOS. + * 3. Improve usability and consistency of cursor style between image-type + * `input` and others. + */ +button, +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; + /* 2 */ + cursor: pointer; + /* 3 */ } + +/** + * Re-set default cursor for disabled elements. + */ +button[disabled], +html input[disabled] { + cursor: default; } + +/** + * Remove inner padding and border in Firefox 4+. + */ +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; } + +/** + * Address Firefox 4+ setting `line-height` on `input` using `!important` in + * the UA stylesheet. + */ +input { + line-height: normal; } + +/** + * It's recommended that you don't attempt to style these elements. + * Firefox's implementation doesn't respect box-sizing, padding, or width. + * + * 1. Address box sizing set to `content-box` in IE 8/9/10. + * 2. Remove excess padding in IE 8/9/10. + */ +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; + /* 1 */ + padding: 0; + /* 2 */ } + +/** + * Fix the cursor style for Chrome's increment/decrement buttons. For certain + * `font-size` values of the `input`, it causes the cursor style of the + * decrement button to change from `default` to `text`. + */ +/** + * 1. Address `appearance` set to `searchfield` in Safari and Chrome. + * 2. Address `box-sizing` set to `border-box` in Safari and Chrome. + */ +/** + * Remove inner padding and search cancel button in Safari and Chrome on OS X. + * Safari (but not Chrome) clips the cancel button when the search input has + * padding (and `textfield` appearance). + */ +/** + * Define consistent border, margin, and padding. + */ +/** + * 1. Correct `color` not being inherited in IE 8/9/10/11. + * 2. Remove padding so people aren't caught out if they zero out fieldsets. + */ +legend { + border: 0; + /* 1 */ + padding: 0; + /* 2 */ } + +/** + * Remove default vertical scrollbar in IE 8/9/10/11. + */ +textarea { + overflow: auto; } + +/** + * Don't inherit the `font-weight` (applied by a rule above). + * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. + */ +/* Tables + ========================================================================== */ +/** + * Remove most spacing between table cells. + */ +table { + border-collapse: collapse; + border-spacing: 0; } + +td, +th { + padding: 0; } + +@font-face { + font-family: 'fontello'; + src: url("data:application/octet-stream;base64,d09GRgABAAAAABa8AA8AAAAAJMgAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABWAAAADsAAABUIIslek9TLzIAAAGUAAAAQwAAAFY+IFOEY21hcAAAAdgAAACkAAACPEafdINjdnQgAAACfAAAABQAAAAgBzn/aGZwZ20AAAKQAAAFkAAAC3CKkZBZZ2FzcAAACCAAAAAIAAAACAAAABBnbHlmAAAIKAAAC34AABCoGAqVRGhlYWQAABOoAAAAMwAAADYOsBL8aGhlYQAAE9wAAAAgAAAAJAfjBBlobXR4AAAT/AAAACkAAAAwK83/+WxvY2EAABQoAAAAGgAAABoYlBJObWF4cAAAFEQAAAAgAAAAIAFSDZ5uYW1lAAAUZAAAAXcAAALNzJ0dH3Bvc3QAABXcAAAAZAAAAIQcABrmcHJlcAAAFkAAAAB6AAAAhuVBK7x4nGNgZGBg4GIwYLBjYHJx8wlh4MtJLMljkGJgYYAAkDwymzEnMz2RgQPGA8qxgGkOIGaDiAIAJjsFSAB4nGNgZF7GOIGBlYGBqYppDwMDQw+EZnzAYMjIBBRlYGVmwAoC0lxTGBxeMHx8zdzyfw5DFHMLQwpQmBEkBwAN0A0rAHic5ZIxDsIwEATXSQgBUyAlBV26VChlnsZLeAEVT1splV8Q9nyuID/grLF0W9xZuwZwAFCLu2iA8EaA1UtqyHqNc9YbPNQPuEppWDFy5MSZS+pSn9ZtA4g99aeCpty+jqmVTdaLWhzR4aS9UXK7M+Hf6pLvZ+miOe1YRizIP7BgmTI6litHx/Lm5MhncHbkOLg48h6poBSQesf+QVodxA+bpTSzeJxjYEADEhDI3PJ/DggDABWMBKV4nK1WaXfTRhQdeUmchCwlCy1qYcTEabBGJmzBgAlBsmMgXZytlaCLFDvpvvGJ3+Bf82Tac+g3flrvGy8kkLTncJqTo3fnzdXM22USWpLYC+uRlJsvxdTWJo3sPAnphk3LUXwoO3shZYrJ3wVREK2W2rcdh0REIlC1rrBEEPseWZpkfOhRRsu2pFdNyi096S5b40G9Vd9+GjrKsTuhpGYzdGg9siVVGFWiSKY9UtKmZaj6K0krvL/CzFfNUMKITiJpvBnG0EjeG2e0ymg1tuMoimyy3ChSJJrhQRR5lNUS5+SKCQzKB82Q8sqnEeXD/Iis2KOcVrBLttP8vi95p3c5P7Ffb1G25EAfyI7s4Ox0JV+EW1th3LST7ShUEXbXd0Js2exU/2aP8ppGA7crMr3QjGCpfIUQKz+hzP4hWS2cT/mSR6NaspETQetlTuxLPoHW44gpcc0YWdDd0QkR1P2SMwz2mD4e/PHeKZYLEwJ4HMt6RyWcCBMpYXM0SdowcmAlZYsqqfWumDjldVrEW8J+7drRl85o41B3YjxbDx1bOVHJ8WhSp5lMndpJzaMpDaKUdCZ4zK8DKD+iSV5tYzWJlUfTOGbGhEQiAi3cS1NBLDuxpCkEzaMZvbkbprl2LVqkyQP13KP39OZWuLnTU9oO9LNGf1anYjrYC9PpaeQv8Wna5SJF6frpGX5M4kHWAjKRLTbDlIMHb/0O0svXlhyF1wbY7u3zK6h91kTwpAH7G9AeT9UpCUyFmFWIVkBirWtZlsnVrBapyNR3Q5pWvqzTBIpyHBfHvoxx/V8zM5aYEr7fidOzIy49c+1LCNMcfJt1PZrXqcVyAXFmeU6nWZbv6zTH8gOd5lme1+kIS1unoyw/1GmB5Uc6HWN5QQuadN/BkIsw5AIOkDCEpQNDWF6CISwVDGG5CENYFmEIyyUYwvJjGMJyGYawvKxl1dRTSePamVgGbEJgYo4eucxF5WoquVRCu2hUakOeEm6VVBTPqn9loF488oY5sBZIl8iaXzHOlY9G5fjWFS1vGjtXwLHqbx+O9jnxUtaLhT8F/9XWVCW9Ys3Dk6vwG4aebCeqNql4dE2Xz1U9uv5fVFRYC/QbSIVYKMqybHBnIoSPOp2GaqCVQ8xszDy063XLmp/D/TcxQhZQ/fg3FBoL3INOWUlZ7eCs1dfbstw7g3I4EyxJMTfz+lb4IiOz0n6RWcqej3wecAWMSmXYagOtFbzZJzEPmd4kzwRxW1E2SNrYzgSJDRzzgHnznQQmYeqqDeRO4YYN+AVhbsF5J1yieqMsh+5F7PMopPxbp+JE9qhojMCz2Rthr+9Cym9xDCQ0+aV+DFQVoakYNRXQNFJuqAZfxtm6bULGDvQjKnbDsqziw8cW95WSbRmEfKSI1aOjn9Zeok6q3H5mFJfvnb4FwSA1MX9733RxkMq7WskyR20DU7calVPXmkPjVYfq5lH1vePsEzlrmm66Jx56X9Oq28HFXCyw9m0O0lImF9T1YYUNosvFpVDqZTRJ77gHGBYY0O9Qio3/q/rYfJ4rVYXRcSTfTtS30edgDPwP2H9H9QPQ92Pocg0uz/eaE59u9OFsma6iF+un6Dcwa625WboG3NB0A+IhR62OuMoNfKcGcXqkuRzpIeBj3RXiAcAmgMXgE921jOZTAKP5jDk+wOfMYdBkDoMt5jDYZs4awA5zGOwyh8Eecxh8wZx1gC+ZwyBkDoOIOQyeMCcAeMocBl8xh8HXzGHwDXPuA3zLHAYxcxgkzGGwr+nWMMwtXtBdoLZBVaADU09Y3MPiUFNlyP6OF4b9vUHM/sEgpv6o6faQ+hMvDPVng5j6i0FM/VXTnSH1N14Y6u8GMfUPg5j6TL8Yy2UGv4x8lwoHlF1sPufvifcP28VAuQABAAH//wAPeJzFV11sXMd1njO/94/37s/deymKWi13yV2KpJbW/tyVSWu9tiyRltaqTNMKadgOEduJY1VWoSoOmkJO4OjBSAyn6ENiBLYSA/FjAieIkfQnQR/itEhe4haQhbYB2uZFzUMfCgUFnGiVb5Z02rz2od2fuXdmzpw55ztnzjnDJGN3fiG2xTEm2Aw7wk6whwdnFhdqVelIWpsgqcnhJ5lnSDqe3GEOE9oRO0wzYpqeVJwz12Ub9sncLeYydzg4drSb1tv54mo+n/PVgcXeTHdGtfOd+jI1yZRiXZup1hvdWr7d7WTtmVZiRLdTX6SqTktx0mu3MpXfI8rvEhykMl2nb44eoVvrgXpDTTvldPTDpEzr65USvZdU6LrnnDcB7bZPl9PbhbRMlYSrNItezyWL16/TLWfKvK4DuplUKsnN29n4SeXvOJ7nfMeu9W5/YIf4fySVyfCNKGP4kG3EW/wnrMQODKYiYkRrjBO/iCm6iMmPx2ksVLpIsSFdbVC9cy9lrYM0bhLxVm5UyS3lRr+MoiGe36Dn0Q5zfD3BRBRRMu7m3qKLudwwsvvBHjdFn02zmUE5P96Pk91LYO7jxNLShM+maVqqeFFhv0YT6GW9VpmnfZG2EisG3Qr90Tcib6E69eqV6nq/WYyXBidrV1793Ogt74BHZyMvy87O/ekXaHKhWopn56folV99bvRtb7z/f/LXxAssZoffofH2p9+ePbs18C0Yf4R+QA/uH7i/U3/7u2nKVbLo8iSO9hDo7Sof81dH79Mhz3vCn/ZHH/N9uuaVvSc8/s+jG6P3x68efQ1Puub7T3jlXbzv/Ea8KS6xg6w78AqRJ4UC3hBiGkIEkIhDCs4DDik8dPhFssBsv3MgTnftEFGxFIe0TEZX691Or9iw7VzPyqQS8Wb0k7uCUvDrD4IkoLt+Gh6kyRf9SnCFJit8fxC9O/r3wM+RuXrVFDzpUPpuFJTU/ChNR/OKiTu3YZ+P4rxErMceYOuDE4dICZc4BFtjkgsuxQUmFBfqPDOMS8N3mIJiiu0w0po2AaHewuHRw1J5sZjU6zVHTS/O7R2BAxQnq9TKip0m1apGA8Z2q9enVoKDY0KyR8TOd7J7CfaGntUmx3SZ6Ba83zozmi/1Hz/y8po7cUpqV5Xnji4k+2v30HhqsjDtlePg/Us/vvl3z+vP/PWtv3zx6ofLPPrskc3mpyf8njT1/eVCaSqI7p+LMVGo+jk9NT2/8akfXb78o1/aZg8LGzsC+gH9F//06bfds1v33cN+wP6KfZ99jX2ZvYxAIWDWLzFrN8H+kf0Du8C22cPsftZnbVZh+5hnYaJr9Bp9mV6hL9Jn6FP0DD1Fgv0r+xcWgIOhR+ghmsd6B7jdon+i9+in9Df0QzpKbYyRHWdr+0+/7WH/43u7v8ykBR7SEFaq/wMZDFuDzoS9iJ3c//8HxPb22BKDLuPCCG4uMKOF0ecZQrd2zjOHhEPn4YfPu4BGsE08mNhS8F4mhrswDlYkwYuVeIZxo7iBM2u1y0Pt8lD/zUOpXR7qHHRXp/b/L3fe3r5vn40AdAPR/i/oe/QROsf+lr3L3mHfZd9m32J/wl4ARho4BqALgJhi8aJ1/g/PAYVkWn3q9qmXpXWcEfx0vRubTl13m3IZwcnGhXiB4qqumqxRr2X1RrvJG007jNOmy3hBIEvSJNZVvNQb+Br7b9VNn2qWaSNBg4CStJNOozUm0KklxgYNsAXXRt32y9RKE4OtdGKa1EgaNbw36r1O2tCmZVmlvRSLTWIgAZZqU+ZxLzFYhoWNuk7als9BCNTTBwWivLb8uqBKelmjybtthDVd5m3I3SrLgyJpgSsW96o2bZbKlGZdcEFjta9naSuDulAr1qVaZrMVxk3VhKIOEWy/YeVCkOlAjyQDJwic9Moc6GS9BIm1T/Vuo9sEBJ0xGi1QVCFNn9qJbXtJVu9TqZfVrIwW4FYXgIisV68uU4ZCwP4igmYl4NWE1SKqZ3WLe6ZLIZWa1IPgCeDQaawT+ublH1/6MGJRkTuCEGfzpaJHAXe0gMmk9JSW5CASCyHxQYTljqukBiU5AalpKTgHQUjcuCAheB0Zj0s1IUQcFqWjsZgrl1PR1ZIr7QlHwvmFdsFNuVIJwZWk0PiRzAlwRX5w7AOMheSyoEQQYHse7NsvtFJFJXw54WMjjXrKlQ+3pOJaCZr0IIOSVk5sScQ9YwrSuBIb8hB9HnLJeeQIsBaKJEIzOKjAcOEI1yRaK8fJyRh8wFyEQpKnnLzH8SHF0eMiEBxoQD4cRB/7cCcWDhZYvRVQwo/kpHAFBBATPLRwINtyDRmAk5TGUSaQ6HBoPxYkkLyA5VyFLueeA6i0NsoNvE/+8VkUBqgXRcmGDQu0CnDm8SEruQcLcUANIggi/Yi465E4tpdL0Ix+jkoT1NIRygcZWKAmM2NcietAaeAqyRoXD7xzx8JK0By2NsIxnpFKq8C6BlQLXICioILIcxE6dly4MKvQFEoPLFGzak8aY8hVjnEAkrBYwh08IUI7rZC8kTQjLmwwCwGA1PhCiMN/IK3VpY48yKAk8Ih9TnqKUwqPEyoWIgeMpaNQR/uTEyqA1jJwQhmS58cG4ROQwxYF4UnpKs2FNwaY55yC9V/I4SHdW1MC75yKbCzmPpRGV06GbqhcW50DaoCOY6J4BB9BHz9HpZI7ADLknqcwIH1XWdeADaCzxIEABJqgHhZau6MZTZQetTprHpE9B4Cae0JjCOiGmlsa60+WDyrwvBu6AZc5s1ez2jrgCOsPVhfgAHMHcOhsPSSsfS6wsVjnEa1hEtwecFngmyji2BZe2PBQN62ls1pNoXgLuUHdPy6ibaRpt/qiyavapIlGeYRrQyc7iPjTRr0rth+49PVzO9+6R6qhzstDLxxfefbsAm8O//Di0/MP5YvpB1FMS/n1e7668ZFrl++nP7Mt33hQh/IhRbq70hw+d/m5YXN+7qHc4ST8YLJYWL+7/8Dla8gv/M6dO7+Qy+Iq6rxldtfg8LjCq1ivXGMwFArhXax3QKy2kAED9mAXIawej/XAbacxA7HzcYILTFbEhcbMxAhpexebDnIUnr29C4646Tm/+bxBNNPiiglOduZG7xVyKGVH/wYdCoXRz2Yz6syJK3MdeozeGxOOlg2iwo3bb2IsbiJ+J0noVJdK2Sz/6Fynw9ju3e5N/itkyn2oMR5jpwfr2xtn1mDHCVuF8DWXjGMuMoc7F606uNUgD9uCWrGLTCHIbEIzW60qPXx0s3bgdK9aWJwuerjVzSHlNkWfl0n97i3t1G3NigSHNGfisuhZ+/ViE0pTbcpxHW7/rSTN2q3UJopSbBJ6Jqw2Z+H6lVa5QE/9j87WmfvSc/M7jUcn7x/SbLZxqvlk89TG0eqL1eTU5nPnWovrj58ZrhRrw2j62GPHNh575NTK46sHouHPk+Y8n23Nd6pyYWny9zrHnwy0Dp48fmR9KUHumJof/vnlE4v9aoyD5k1Wj86euHx1+3BrcPfSclxoLtDdgyOHty2WANTW/R5qtRW2wb44eHmNfGcZh7CEYI7bGcm1/AT3He34+kLO5Uba6v9CZMOh9ZULtmJxAs/eopEP9A642vC2M6557L2AbYVgw4arqzMzuC2x1Y3VjeHpE8cH986szKx02ocXG3N+xa9M7SsWolAr5pFXwD1wbtejEhPrHrJpFXl0fPPpc9QBJTtoy5tqKGq7w2Rz6944R+yo2fW9bLYix8N81T9LJ5fWaeMVWlhfP5kk3qZaeumlq4tq81Wthy89urxz8miFu5v61M9u/P2DGqPmqRuj608b7W6SfpYqtETVT6jN1oZfmOTTOX/jK9PT02G46Rm9eBfvHNLG23xNrRylyersJEbVqQ1+Zqgw+hW1tcUfP6cs6bOXLj1rKdlvATmUMHkAAHicY2BkYGAA4pPf3s2M57f5ysDN/AIownB1VnoEjP7/9/9jlnjmFiCXg4EJJAoArJoPDAB4nGNgZGBgbvk/h4GBpez/3/+/WOIZgCIogAcArKgHA3icY37BwMAcCcQvIJjpFJBeABL7/xeCGRhY9P//B4mxlDEwAAAlswznAAAAAAAAAACQAMgBCAFIAZgCHgYiBowG7geUCFQAAAABAAAADAH4AAQAAAAAAAIAJAA0AHMAAACqC3AAAAAAeJx1kN1qwjAYht/Mn20K29hgp8vRUMbqDwxBEASHnmwnMjwdtda2UhtJo+Bt7B52MbuJXcte2ziGspY0z/fky5evAXCNbwjkzxNHzgJnjHI+wSl6lgv0z5aL5BfLJVTxZrlM/265ggcElqu4wQcriOI5owU+LQtciUvLJ7gQd5YL9I+Wi+Se5RJuxavlMr1nuYKJSC1XcS++Bmq11VEQGlkb1GW72erI6VYqqihxY+muTah0KvtyrhLjx7FyPLXc89gP1rGr9+F+nvg6jVQiW05zr0Z+4mvX+LNd9XQTtI2Zy7lWSzm0GXKl1cL3jBMas+o2Gn/PwwAKK2yhEfGqQhhI1GjrnNtoooUOacoMycw8K0ICFzGNizV3hNlKyrjPMWeU0PrMiMkOPH6XR35MCrg/ZhV9tHoYT0i7M6LMS/blsLvDrBEpyTLdzM5+e0+x4WltWsNduy511pXE8KCG5H3s1hY0Hr2T3Yqh7aLB95//+wHmboRRAHicbcHREoIgEAXQvYhgZh8JtSaDsc6yjr/fQ6+dQ45+ZvpvgcMAjxEBERNumHHHggfFg7WXbk5qeElLxj6nZx1TltP8xvsRG9slWqNyN1GejPVTWtrDu9h2Zn+VtRB9AeSVGgt4nGPw3sFwIihiIyNjX+QGxp0cDBwMyQUbGVidNjEwMmiBGJu5mBg5ICw+BjCLzWkX0wGgNCeQze60i8EBwmZmcNmowtgRGLHBoSNiI3OKy0Y1EG8XRwMDI4tDR3JIBEhJJBBs5mFi5NHawfi/dQNL70YmBhcADHYj9AAA") format("woff"), url("data:application/octet-stream;base64,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I7AEQ2CwASuwBSVhsAUlsAJiILAAUFiwQGBZZrABY7AEJmEgsAQlYGQjsAMlYGRQWCEbIyFZIyAgsAQmI0ZhOFktsDcssAAWICAgsAUmIC5HI0cjYSM8OC2wOCywABYgsAgjQiAgIEYjR7ABKyNhOC2wOSywABawAyWwAiVHI0cjYbAAVFguIDwjIRuwAiWwAiVHI0cjYSCwBSWwBCVHI0cjYbAGJbAFJUmwAiVhuQgACABjYyMgWGIbIVljuAQAYiCwAFBYsEBgWWawAWNgIy4jICA8ijgjIVktsDossAAWILAIQyAuRyNHI2EgYLAgYGawAmIgsABQWLBAYFlmsAFjIyAgPIo4LbA7LCMgLkawAiVGUlggPFkusSsBFCstsDwsIyAuRrACJUZQWCA8WS6xKwEUKy2wPSwjIC5GsAIlRlJYIDxZIyAuRrACJUZQWCA8WS6xKwEUKy2wPiywNSsjIC5GsAIlRlJYIDxZLrErARQrLbA/LLA2K4ogIDywBCNCijgjIC5GsAIlRlJYIDxZLrErARQrsARDLrArKy2wQCywABawBCWwBCYgLkcjRyNhsAlDKyMgPCAuIzixKwEUKy2wQSyxCAQlQrAAFrAEJbAEJSAuRyNHI2EgsAQjQrAJQysgsGBQWCCwQFFYswIgAyAbswImAxpZQkIjIEewBEOwAmIgsABQWLBAYFlmsAFjYCCwASsgiophILACQ2BkI7ADQ2FkUFiwAkNhG7ADQ2BZsAMlsAJiILAAUFiwQGBZZrABY2GwAiVGYTgjIDwjOBshICBGI0ewASsjYTghWbErARQrLbBCLLA1Ky6xKwEUKy2wQyywNishIyAgPLAEI0IjOLErARQrsARDLrArKy2wRCywABUgR7AAI0KyAAEBFRQTLrAxKi2wRSywABUgR7AAI0KyAAEBFRQTLrAxKi2wRiyxAAEUE7AyKi2wRyywNCotsEgssAAWRSMgLiBGiiNhOLErARQrLbBJLLAII0KwSCstsEossgAAQSstsEsssgABQSstsEwssgEAQSstsE0ssgEBQSstsE4ssgAAQistsE8ssgABQistsFAssgEAQistsFEssgEBQistsFIssgAAPistsFMssgABPistsFQssgEAPistsFUssgEBPistsFYssgAAQCstsFcssgABQCstsFgssgEAQCstsFkssgEBQCstsFossgAAQystsFsssgABQystsFwssgEAQystsF0ssgEBQystsF4ssgAAPystsF8ssgABPystsGAssgEAPystsGEssgEBPystsGIssDcrLrErARQrLbBjLLA3K7A7Ky2wZCywNyuwPCstsGUssAAWsDcrsD0rLbBmLLA4Ky6xKwEUKy2wZyywOCuwOystsGgssDgrsDwrLbBpLLA4K7A9Ky2waiywOSsusSsBFCstsGsssDkrsDsrLbBsLLA5K7A8Ky2wbSywOSuwPSstsG4ssDorLrErARQrLbBvLLA6K7A7Ky2wcCywOiuwPCstsHEssDorsD0rLbByLLMJBAIDRVghGyMhWUIrsAhlsAMkUHiwARUwLQBLuADIUlixAQGOWbABuQgACABjcLEABUKyAAEAKrEABUKzCgIBCCqxAAVCsw4AAQgqsQAGQroCwAABAAkqsQAHQroAQAABAAkqsQMARLEkAYhRWLBAiFixA2REsSYBiFFYugiAAAEEQIhjVFixAwBEWVlZWbMMAgEMKrgB/4WwBI2xAgBEAAA=") format("truetype"); } +/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ +/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ +/* +@media screen and (-webkit-min-device-pixel-ratio:0) { + @font-face { + font-family: 'fontello'; + src: url('../font/fontello.svg?60007293#fontello') format('svg'); + } +} +*/ +[class^="icn-"]:before, [class*=" icn-"]:before { + font-family: "fontello"; + font-style: normal; + font-weight: normal; + speak: none; + display: inline-block; + text-decoration: inherit; + width: 1em; + margin-right: .2em; + text-align: center; + /* opacity: .8; */ + /* For safety - reset parent styles, that can break glyph codes*/ + font-variant: normal; + text-transform: none; + /* fix buttons height, for twitter bootstrap */ + line-height: 1em; + /* Animation center compensation - margins should be symmetric */ + /* remove if not needed */ + margin-left: .2em; + /* you can be more comfortable with increased icons size */ + /* font-size: 120%; */ + /* Uncomment for 3D effect */ + /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ } + +.icn-persist:before { + content: '\e800'; } + +/* '' */ +.icn-ok:before { + content: '\e801'; } + +/* '' */ +.icn-donate:before { + content: '\e802'; } + +/* '' */ +.icn-back:before { + content: '\e80b'; } + +/* '' */ +.icn-about:before { + content: '\e81f'; } + +/* '' */ +.icn-help:before { + content: '\e826'; } + +/* '' */ +.icn-network:before { + content: '\e82e'; } + +/* '' */ +.icn-restore:before { + content: '\e836'; } + +/* '' */ +.icn-terminal:before { + content: '\f108'; } + +/* '' */ +.icn-github:before { + content: '\f113'; } + +/* '' */ +.icn-wifi:before { + content: '\f1eb'; } + +/* '' */ +html { + box-sizing: border-box; } + +*, *::after, *::before { + box-sizing: inherit; } + +html { + font-family: Arial, sans-serif; + color: #D0D0D0; + background: #131315; } + +html, body { + border: 0 none; + margin: 0; + padding: 0; + text-decoration: none; + 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; } + +/* 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; } + +@media screen and (max-width: 544px) { + #outer { + display: block; + overflow-y: scroll; } } +#menu { + flex: 0 0 15rem; + background: #3983CD; } + #menu > * { + display: block; + text-decoration: none; + padding: 0.61805rem 1rem; + white-space: nowrap; + word-wrap: normal; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } + #menu #brand { + color: white; + background: #2b6aa8; + font-size: 120%; + text-align: center; + position: relative; + transition: none; + font-weight: bold; + margin-bottom: 1rem; } + @media screen and (max-width: 544px) { + #menu #brand { + background: #3983CD; + cursor: pointer; + margin-bottom: 0.38198rem; } + #menu #brand::after { + position: absolute; + color: rgba(0, 0, 0, 0.4); + right: 1rem; + content: '▸'; + top: 50%; + font-size: 120%; + font-weight: bold; + transform: translate(0, -50%) rotate(90deg); } } + #menu.expanded #brand { + background: #2b6aa8; } + @media screen and (max-width: 544px) { + #menu.expanded #brand:after { + transform: translate(-25%, -50%) rotate(-90deg); } } + #menu a { + font-size: 130%; + color: white; + transition: background-color 0.2s; + text-shadow: 0 0 5px rgba(0, 0, 0, 0.4); } + #menu a:hover, #menu a.selected { + background: #5badff; + text-shadow: 0 0 5px rgba(0, 0, 0, 0.6); } + #menu a.selected { + position: relative; + box-shadow: 0 0 5px rgba(0, 0, 0, 0.5); } + #menu a:focus { + outline-color: #ff0099; } + #menu a::before { + vertical-align: -2px; + margin-left: 0; + margin-right: 15px; } + @media screen and (max-width: 544px) { + #menu a { + display: none; } + #menu a::before { + margin-left: 10px; } } + #menu.expanded a { + display: block; } + @media screen and (min-width: 545px) and (max-width: 1000px) { + #menu { + flex-basis: 10rem; } + #menu #brand { + font-size: 95%; + margin-bottom: 0.61805rem; } + #menu a { + font-size: 105%; } + #menu > * { + padding: 0.38198rem 0.61805rem; } } + +#content { + flex-grow: 1; + position: relative; + padding: 1rem; + overflow-y: auto; } + @media screen and (max-width: 544px) { + #content { + padding: 0.61805rem; } } + #content > * { + margin-left: auto; + margin-right: auto; } + #content h1 { + text-align: center; + font-size: 2.2807em; + margin-top: 0; + margin-bottom: 1rem; } + #content h2 { + font-size: 1.42383em; + margin-bottom: 0.61805rem; } + @media screen and (max-width: 544px) { + #content h1 { + font-size: 1.80203em; + margin-bottom: 0.61805rem; } + #content h2 { + font-size: 1.26563em; + margin-bottom: 0.61805rem; } } + #content td, #content th { + padding: 0.38198rem; } + #content tbody th { + text-align: right; + width: 160px; + color: white; } + +#loader { + position: absolute; + right: 1.618rem; + top: 1.618rem; + transition: opacity .2s; + opacity: 0; } + @media screen and (max-width: 544px) { + #loader { + top: 1rem; + right: 1rem; } } + #loader.show { + opacity: 1; } + +.Box { + display: block; + max-width: 900px; + margin-top: 1rem; + padding: 0.61805rem 1rem; + border-radius: 3px; + background-color: rgba(255, 255, 255, 0.07); + box-shadow: 0 0 4px black; + border: 1px solid #4f4f4f; } + @media screen and (max-width: 544px) { + .Box { + margin-top: 0.61805rem; } } + .Box h1, .Box h2 { + overflow: hidden; } + h1 + .Box { + margin-top: 0; } + .Box h2 { + margin-top: 0; + margin-bottom: 0 !important; } + .Box.wide { + width: initial; + max-width: initial; } + .Box.medium { + max-width: 1200px; } + .Box.str { + position: relative; } + .Box.str .Row.buttons { + position: absolute; } + @media screen and (max-width: 544px) { + .Box.str .Row.buttons { + right: 1rem; + top: 1.8em; + margin: 1rem auto; } } + @media screen and (min-width: 545px) { + .Box.str .Row.buttons { + right: 0; + top: 0; + margin-top: 0.61805rem; } } + .Box.str.mobopen .Row.buttons { + top: 0; + margin-top: 0.61805rem; } + .Box .Row.explain { + max-width: 600px; + margin-left: 0; } + @media screen and (max-width: 544px) { + .Box .Row.explain { + margin-top: 60px; } } + .Box.mobopen .Row.explain { + margin-top: 12px; } + @media screen and (max-width: 544px) { + .Box.mobopen .Row.explain { + margin-top: 18px; } } + +@media screen and (max-width: 544px) { + .Box.mobcol h2 { + position: relative; + cursor: pointer; + padding-right: 1.3rem; } + .Box.mobcol h2::after { + position: absolute; + right: 0; + content: '▸'; + top: 50%; + font-size: 120%; + font-weight: bold; + transform: translate(0, -50%) rotate(90deg); } + .Box.mobcol.expanded h2::after { + transform: translate(-25%, -50%) rotate(-90deg); + margin-bottom: 1rem; } + .Box.mobcol .Row { + display: none; } + .Box.mobcol #ap-box { + display: none; } + .Box.mobcol.expanded .Row { + display: flex; } + .Box.mobcol.expanded #ap-box { + display: block; } } +.Modal { + position: fixed; + width: 100%; + height: 100%; + left: 0; + top: 0; + right: 0; + bottom: 0; + display: flex; + justify-content: center; + align-items: center; + transition: opacity .5s; + background: rgba(0, 0, 0, 0.65); + opacity: 0; } + .Modal.visible { + opacity: 1; } + .Modal.hidden { + display: none; } + +.Dialog { + margin: 0.61805rem; + padding: 1rem; + overflow: hidden; + max-width: 100%; + max-height: 100%; + flex: 0 1 30rem; + background: #1c1c1e; + border-left: 6px solid #2972ba; + border-right: 6px solid #2972ba; + border-top: 1px solid #2972ba; + border-bottom: 1px solid #2972ba; + box-shadow: 0 0 6px 0 black; + border-radius: 6px; } + .Dialog h1, .Dialog h2 { + margin-top: 0; } + .Dialog p:last-child { + margin-bottom: 0; } + +.NotifyMsg { + position: fixed; + top: 1.618rem; + right: 2.61792rem; + padding: 0.61805rem 1rem; + -webkit-font-smoothing: subpixel-antialiased; + -webkit-transform: translateZ(0) scale(1, 1); + background: #3887d0; + color: white; + text-shadow: 0 0 2px black; + box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.6); + border-radius: 5px; + max-width: 80%; + transition: opacity .5s; + opacity: 0; } + .NotifyMsg.error { + background: #d03e42; } + @media screen and (max-width: 544px) { + .NotifyMsg { + width: calc(100% - 1rem); } } + .NotifyMsg.visible { + opacity: 1; } + .NotifyMsg.hidden { + display: none; } + +button, input[type=submit], .button { + text-align: center; + cursor: pointer; + display: inline-block; + border-radius: 2px; + padding: 0 0.6em; + border: 0 none; + line-height: 1.8em; + font-size: 1.1em; + margin-bottom: 3px; + min-width: 5em; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + text-shadow: 1.5px 1.5px 2px rgba(0, 0, 0, 0.4); + background-color: #3983cd; + box-shadow: 0 3px 0 #265f98; + text-decoration: none !important; } + button:active, input[type=submit]:active, .button:active { + position: relative; + top: 2px; } + button.narrow, input[type=submit].narrow, .button.narrow { + min-width: initial; } + button::before, input[type=submit]::before, .button::before { + vertical-align: -1px; + margin-left: 0; } + button, button:link, button:visited, input[type=submit], input[type=submit]:link, input[type=submit]:visited, .button, .button:link, .button:visited { + color: #FEFEFE; } + button:hover, button:active, button.active, button.selected, input[type=submit]:hover, input[type=submit]:active, input[type=submit].active, input[type=submit].selected, .button:hover, .button:active, .button.active, .button.selected { + background-color: #2076C6; + color: #FEFEFE; } + button:hover, button.selected, button.active, input[type=submit]:hover, input[type=submit].selected, input[type=submit].active, .button:hover, .button.selected, .button.active { + box-shadow: 0 3px 0 #154c80; } + button:active, input[type=submit]:active, .button:active { + box-shadow: 0 1px 0 #154c80; } + button:focus, input[type=submit]:focus, .button:focus { + outline-color: #ff0099; } + +button, input[type=submit], .button { + text-align: center; + cursor: pointer; + display: inline-block; + border-radius: 2px; + padding: 0 0.6em; + border: 0 none; + line-height: 1.8em; + font-size: 1.1em; + margin-bottom: 3px; + min-width: 5em; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + text-shadow: 1.5px 1.5px 2px rgba(0, 0, 0, 0.4); + background-color: #3983cd; + box-shadow: 0 3px 0 #265f98; + text-decoration: none !important; } + button:active, input[type=submit]:active, .button:active { + position: relative; + top: 2px; } + button.narrow, input[type=submit].narrow, .button.narrow { + min-width: initial; } + button::before, input[type=submit]::before, .button::before { + vertical-align: -1px; + margin-left: 0; } + button, button:link, button:visited, input[type=submit], input[type=submit]:link, input[type=submit]:visited, .button, .button:link, .button:visited { + color: #FEFEFE; } + button:hover, button:active, button.active, button.selected, input[type=submit]:hover, input[type=submit]:active, input[type=submit].active, input[type=submit].selected, .button:hover, .button:active, .button.active, .button.selected { + background-color: #2076C6; + color: #FEFEFE; } + button:hover, button.selected, button.active, input[type=submit]:hover, input[type=submit].selected, input[type=submit].active, .button:hover, .button.selected, .button.active { + box-shadow: 0 3px 0 #154c80; } + button:active, input[type=submit]:active, .button:active { + box-shadow: 0 1px 0 #154c80; } + button:focus, input[type=submit]:focus, .button:focus { + outline-color: #ff0099; } + +input[type="number"], input[type="password"], input[type="text"], textarea, select { + border: 0 none; + border-bottom: 2px solid #2972ba; + background-color: #3c3c3c; + color: white; + padding: 6px; + line-height: 1em; + font-weight: normal; } + input[type="number"]:focus, input[type="number"]:hover, input[type="password"]:focus, input[type="password"]:hover, input[type="text"]:focus, input[type="text"]:hover, textarea:focus, textarea:hover, select:focus, select:hover { + border-bottom-color: #2ea1f9; } + +.Row.checkbox { + line-height: 27px; } + .Row.checkbox .box { + overflow: hidden; + width: 27px; + height: 27px; + border: 1px solid #808080; + border-radius: 3px; + background: #3c3c3c; + display: inline-block; + position: relative; + cursor: pointer; + color: #2ea1f9; } + .Row.checkbox .box::before { + font-weight: bold; + position: absolute; + content: '×'; + left: 0; + top: 0; + right: 0; + bottom: 0; + line-height: 26px; + text-align: center; + font-size: 27px; + vertical-align: middle; + display: none; } + .Row.checkbox .box.checked::before { + display: block; } + +.Row.range .display { + margin-left: 1ex; } +.Row.range label .display { + font-weight: normal; } + +#psk-modal form > *, #wificonfbox form > * { + margin-right: 0.38198rem; } + #psk-modal form > *:last-child, #wificonfbox form > *:last-child { + margin-right: 0; } + +form { + border: 0 none; + margin: 0; + padding: 0; + text-decoration: none; } + +input[type="number"], input[type="password"], input[type="text"], textarea, select, label.select-wrap { + width: 250px; } + +input[type="number"], input.short { + width: 125px; } + +.Box.errors .list { + color: crimson; + font-weight: bold; } +.Box.errors .lead { + color: white; } + +.Row { + vertical-align: middle; + margin: 12px auto; + text-align: left; + display: flex; + flex-direction: row; + align-items: center; } + .Row:first-child { + margin-top: 0; } + .Row:last-child { + margin-bottom: 0; } + .Row .spacer { + width: 160px; } + @media screen and (max-width: 544px) { + .Row .spacer { + display: none; } } + .Row.buttons { + margin: 16px auto; } + .Row.buttons input, .Row.buttons .button { + margin-right: 0.61805rem; } + .Row.centered { + justify-content: center; } + .Row.message { + font-size: 1em; + text-shadow: 1px 1px 3px black; + text-align: center; } + .Row.message.error { + color: crimson; } + .Row.message.ok { + color: #0fe851; } + .Row.separator { + padding-top: 14px; + border-top: 2px solid rgba(255, 255, 255, 0.1); } + .Row textarea { + display: inline-block; + vertical-align: top; + min-height: 10rem; + flex-grow: 1; + resize: vertical; } + .Row label { + font-weight: bold; + color: white; + display: inline-block; + width: 160px; + text-align: right; + text-shadow: 1px 1px 3px black; + padding: 8px; + align-self: flex-start; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + white-space: nowrap; + word-wrap: normal; } + .Row label.error { + color: crimson; } + .Row input[type="range"] { + width: 200px; } + @media screen and (max-width: 544px) { + .Row { + flex-direction: column; + margin: 6px auto; } + .Row.buttons, .Row.centered, .Row.checkbox { + flex-direction: row; } + .Row.buttons { + justify-content: center; } + .Row.buttons :last-child { + margin-right: 0; } + .Row label { + padding-left: 0; + text-align: left; + width: auto; } + .Row .checkbox-wrap { + order: 1; + text-align: left; + padding-bottom: 0; + border-radius: .4px; + width: auto; } + .Row .checkbox-wrap + label { + width: auto; } + .Row input[type="number"], .Row input[type="password"], .Row input[type="text"], .Row textarea, .Row input[type="range"], .Row textarea, .Row select { + width: 100%; } } + +form span.required { + color: red; } + +.RadioGroup { + display: inline-block; + line-height: 1.5em; + vertical-align: middle; } + .RadioGroup label { + width: auto; + text-align: left; + cursor: pointer; + font-weight: normal; } + .RadioGroup input[type="radio"] { + vertical-align: middle; + margin: 0 0 0 5px; } + +#ap-list { + column-count: 3; + column-gap: 0; + margin: 0 -0.23608rem; } + @media screen and (min-width: 545px) and (max-width: 1000px) { + #ap-list { + column-count: 2; } } + @media screen and (max-width: 544px) { + #ap-list { + column-count: 1; } } + +#ap-loader, #ap-noscan, #ap-scan { + background: rgba(255, 255, 255, 0.1); + border-radius: 5px; + padding: 0.38198rem; + margin-bottom: 0.38198rem; } + +#ap-noscan { + font-weight: bold; } + +#ap-box { + padding-top: 0.38198rem; } + #ap-box label { + display: block; + color: white; + font-weight: bold; + margin-bottom: 0.23608rem; } + +#psk-modal form { + display: flex; + align-items: center; + margin: 0.38198rem; } + #psk-modal form input[type=password] { + min-width: 5rem; } + +.AP .inner, .AP-preview .wrap { + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + position: relative; + border-radius: 3px; + color: #222; + background: #afafaf; + transition: background-color 0.5s; + display: flex; } + .AP .inner:active, .AP-preview .wrap:active { + left: 0; + top: 1px; } + .AP .inner:hover, .AP-preview .wrap:hover { + background: white; } + .AP .inner .rssi, .AP-preview .wrap .rssi { + min-width: 2.5rem; + flex: 0 0 15%; + text-align: right; } + .AP .inner .rssi:after, .AP-preview .wrap .rssi:after { + padding-left: 0.09018rem; + content: '%'; + font-size: 0.88889em; } + .AP .inner .essid, .AP-preview .wrap .essid { + flex: 1 1 70%; + min-width: 0; + text-overflow: ellipsis; + overflow: hidden; + font-weight: bold; } + .AP .inner .auth, .AP-preview .wrap .auth { + flex: 0 0 15%; } + +.AP { + break-inside: avoid-column; + max-width: 500px; + padding: 0.23608rem; } + .AP.selected .inner { + background: #42a6f9 !important; + cursor: default; + top: 0 !important; } + .AP .inner > * { + padding: 0.61805rem; + white-space: nowrap; + word-wrap: normal; } + +.AP-preview-nil { + padding: 8px; + border-radius: 5px; + border: 1px dashed #ddd; + width: 250px; + height: 94px; } + +.AP-preview .wrap { + flex-direction: row; + background: #ddd !important; + cursor: default; + top: 0 !important; + overflow: hidden; } + .AP-preview .wrap .inner { + display: flex; + flex-direction: column; } + .AP-preview .wrap .inner > * { + padding: 0.61805rem; + white-space: nowrap; + word-wrap: normal; } + .AP-preview .wrap .forget { + align-self: stretch; + line-height: 100%; + padding: 0.61805rem; + border-left: 1px solid #bbb; + display: flex; + align-items: center; + font-size: 28px; } + .AP-preview .wrap .forget, .AP-preview .wrap .forget:hover { + color: black; + text-decoration: none; } + .AP-preview .wrap .forget:hover { + background: #dc4a6a; + color: white; + border-left: 1px solid #666; + border-bottom-right-radius: 3px; + border-top-right-radius: 3px; } + .AP-preview .wrap .forget:active { + position: relative; + padding-top: calc(0.61805rem + 1px); } + .AP-preview .wrap .essid, .AP-preview .wrap .passwd, .AP-preview .wrap .nopasswd { + padding-bottom: 0; } + .AP-preview .wrap .x-passwd { + font-family: monospace; } + +body.term h1 { + font-size: 1.80203em; } + @media screen and (max-width: 544px) { + body.term h1 { + font-size: 1.42383em; } } +body.term #screen { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + font-family: monospace; + font-size: 16pt; + white-space: nowrap; + background: #111213; + padding: 6px; + display: inline-block; + border: 2px solid #3983CD; } + body.term #screen span { + white-space: pre; + cursor: pointer; } + body.term #screen span:hover { + outline: 1px solid rgba(255, 255, 255, 0.4); } + @media screen and (max-width: 544px) { + body.term #screen span:hover { + outline: 0 none; } } +body.term #buttons { + margin-top: 10px; + white-space: nowrap; } + body.term #buttons button { + margin: 0 3px; + padding: 10px 0; + width: 18%; + max-width: 65px; + min-width: initial; + cursor: pointer; + font-weight: bold; } +body.term #botnav { + padding-top: 1.5em; + text-align: center; } + body.term #botnav a { + padding: 0 0.38198rem; + text-decoration: underline; } + body.term #botnav a, body.term #botnav a:visited, body.term #botnav a:link { + color: #2e4d6e; } + body.term #botnav a:hover { + color: #5abfff; } + +#termwrap { + text-align: center; } + +.page-about .Box { + padding-left: 1rem; + padding-right: 1rem; } + .page-about .Box a { + font-weight: bold; } +.page-about #logo { + float: right; + height: 130px; } +.page-about #logo2 { + max-width: 100%; } +.page-about td { + white-space: normal; } + +.colorprev { + margin-top: 0.38198rem; + margin-bottom: 0.38198rem; } + .colorprev span { + display: inline-block; + width: 2em; + padding: 0.38198rem 0; + text-align: center; } + +.ansiref, .ansiref td, .ansiref th { + border: 1px solid #666; } +.ansiref th, .ansiref td { + white-space: normal; } +.ansiref th { + background-color: rgba(255, 255, 255, 0.1); } +.ansiref td:nth-child(1) { + font-family: monospace; } +.ansiref.w100 { + width: 100%; } + .ansiref.w100 td:nth-child(1) { + width: 9em; } + .ansiref.w100 td:nth-child(2) { + width: 8em; } + +.fg0 { + color: #111213; } + +.bg0 { + background-color: #111213; } + +.fg1 { + color: #CC0000; } + +.bg1 { + background-color: #CC0000; } + +.fg2 { + color: #4E9A06; } + +.bg2 { + background-color: #4E9A06; } + +.fg3 { + color: #C4A000; } + +.bg3 { + background-color: #C4A000; } + +.fg4 { + color: #3465A4; } + +.bg4 { + background-color: #3465A4; } + +.fg5 { + color: #75507B; } + +.bg5 { + background-color: #75507B; } + +.fg6 { + color: #06989A; } + +.bg6 { + background-color: #06989A; } + +.fg7 { + color: #D3D7CF; } + +.bg7 { + background-color: #D3D7CF; } + +.fg8 { + color: #555753; } + +.bg8 { + background-color: #555753; } + +.fg9 { + color: #EF2929; } + +.bg9 { + background-color: #EF2929; } + +.fg10 { + color: #8AE234; } + +.bg10 { + background-color: #8AE234; } + +.fg11 { + color: #FCE94F; } + +.bg11 { + background-color: #FCE94F; } + +.fg12 { + color: #729FCF; } + +.bg12 { + background-color: #729FCF; } + +.fg13 { + color: #AD7FA8; } + +.bg13 { + background-color: #AD7FA8; } + +.fg14 { + color: #34E2E2; } + +.bg14 { + background-color: #34E2E2; } + +.fg15 { + color: #EEEEEC; } + +.bg15 { + background-color: #EEEEEC; } + +.fg8, .fg9, .fg10, .fg11, .fg12, .fg13, .fg14, .fg15 { + font-weight: bold; } + +.nb { + font-weight: normal !important; } + +@media screen and (min-width: 545px) { + .mq-phone { + display: none !important; } } +@media screen and (max-width: 544px) { + .mq-tablet-min, .mq-no-phone { + display: none !important; } } +@media screen and (min-width: 1001px) { + .mq-tablet-max { + display: none !important; } } +@media screen and (max-width: 1000px) { + .mq-normal-min { + display: none !important; } } + +/*# sourceMappingURL=app.css.map */ diff --git a/html_orig/dump_js_lang.php b/html_orig/dump_js_lang.php new file mode 100755 index 0000000..41cf168 --- /dev/null +++ b/html_orig/dump_js_lang.php @@ -0,0 +1,21 @@ +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'; diff --git a/html_orig/js/app.js b/html_orig/js/app.js index 7aed10d..7ed55a3 100644 --- a/html_orig/js/app.js +++ b/html_orig/js/app.js @@ -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('
{0}
'.format(ap.rssi_perc)) + .htmlAppend('
{0}
'.format($.htmlEscape(ap.essid))) + .htmlAppend('
{0}
'.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('
{0}
'.format(ap.rssi_perc)) - .htmlAppend('
{0}
'.format($.htmlEscape(ap.essid))) - .htmlAppend('
{0}
'.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([ - 'Client+AP AP only', - 'Client+AP', - 'Client only AP only' - ][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(); }; })(); diff --git a/html_orig/jssrc/appcommon.js b/html_orig/jssrc/appcommon.js index 09fd0b1..0e5616b 100644 --- a/html_orig/jssrc/appcommon.js +++ b/html_orig/jssrc/appcommon.js @@ -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); }; diff --git a/html_orig/jssrc/chibi.js b/html_orig/jssrc/chibi.js index 63dfa77..db51e2c 100755 --- a/html_orig/jssrc/chibi.js +++ b/html_orig/jssrc/chibi.js @@ -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 diff --git a/html_orig/jssrc/lang.js b/html_orig/jssrc/lang.js new file mode 100644 index 0000000..6ef4755 --- /dev/null +++ b/html_orig/jssrc/lang.js @@ -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+'?'; } diff --git a/html_orig/jssrc/notif.js b/html_orig/jssrc/notif.js new file mode 100644 index 0000000..ad08e51 --- /dev/null +++ b/html_orig/jssrc/notif.js @@ -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 = {}); diff --git a/html_orig/jssrc/term.js b/html_orig/jssrc/term.js index b9253d6..6206c85 100644 --- a/html_orig/jssrc/term.js +++ b/html_orig/jssrc/term.js @@ -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(); - } + }; })(); diff --git a/html_orig/jssrc/utils.js b/html_orig/jssrc/utils.js index 4a7fd0b..6fa4b7d 100755 --- a/html_orig/jssrc/utils.js +++ b/html_orig/jssrc/utils.js @@ -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; + } +} diff --git a/html_orig/jssrc/wifi.js b/html_orig/jssrc/wifi.js index dd7f370..ece92c7 100644 --- a/html_orig/jssrc/wifi.js +++ b/html_orig/jssrc/wifi.js @@ -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('
{0}
'.format(ap.rssi_perc)) + .htmlAppend('
{0}
'.format($.htmlEscape(ap.essid))) + .htmlAppend('
{0}
'.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('
{0}
'.format(ap.rssi_perc)) - .htmlAppend('
{0}
'.format($.htmlEscape(ap.essid))) - .htmlAppend('
{0}
'.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([ - 'Client+AP AP only', - 'Client+AP', - 'Client only AP only' - ][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 = {}); diff --git a/html_orig/lang/en.php b/html_orig/lang/en.php new file mode 100644 index 0000000..c3d6b69 --- /dev/null +++ b/html_orig/lang/en.php @@ -0,0 +1,140 @@ + '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:', +]; diff --git a/html_orig/packjs.sh b/html_orig/packjs.sh index 37c976c..6df5917 100755 --- a/html_orig/packjs.sh +++ b/html_orig/packjs.sh @@ -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 diff --git a/html_orig/pages/_cfg_menu.php b/html_orig/pages/_cfg_menu.php new file mode 100644 index 0000000..570c623 --- /dev/null +++ b/html_orig/pages/_cfg_menu.php @@ -0,0 +1,20 @@ + + + diff --git a/html_orig/pages/_head.php b/html_orig/pages/_head.php new file mode 100644 index 0000000..a6d428d --- /dev/null +++ b/html_orig/pages/_head.php @@ -0,0 +1,31 @@ + + + + + + + <?= $_GET['PAGE_TITLE'] ?> + + + + + +
+ + +
+Loading… + +

+ + + + diff --git a/html_orig/pages/_tail.php b/html_orig/pages/_tail.php new file mode 100644 index 0000000..dfdca17 --- /dev/null +++ b/html_orig/pages/_tail.php @@ -0,0 +1,9 @@ + + + +
+ +
+ + + diff --git a/html_orig/pages/about.php b/html_orig/pages/about.php new file mode 100644 index 0000000..2708855 --- /dev/null +++ b/html_orig/pages/about.php @@ -0,0 +1,69 @@ +
+ +

ESP8266 Remote Terminal

+ + + +

+ © Ondřej Hruška, 2016-2017 + <ondra@ondrovo.com> +

+ +

+ Katedra měření, FEL ČVUT
+ Department of Measurement, FEE CTU +

+
+ +
+

Version

+ + + + + + + + + + + + + +
ESPTermv%vers_fw%, build %date% at %time%
libesphttpdv%vers_httpd%
ESP IoT SDKv%vers_sdk%
+
+ +
+

Issues

+

+ Please report any issues to the bugtracker or send them by e-mail (see above). +

+

+ Firmware updates can be downloaded from the releases page and flashed + with esptool.py. +

+
+ +
+

Contributing

+

+ You're welcome to submit your improvements and ideas to our GitHub repository! +

+ +

+ If you'd like to donate, please try PayPal or + LiberaPay. +

+
+ +
+

Thanks

+

+ The webserver is based on a fork of the + esphttpd library by Jeroen Domburg (Sprite_tm). +

+

+ Using (modified) JS library chibi.js by + Kyle Barrow as a lightweight jQuery alternative. +

+
diff --git a/html_orig/pages/cfg_admin.php b/html_orig/pages/cfg_admin.php new file mode 100644 index 0000000..986f83e --- /dev/null +++ b/html_orig/pages/cfg_admin.php @@ -0,0 +1,34 @@ +
+
+ +
+ +
+ + + +
+ +
+ +
+ +
+ + + +
+
+ + diff --git a/html_orig/pages/cfg_app.php b/html_orig/pages/cfg_app.php new file mode 100644 index 0000000..d967641 --- /dev/null +++ b/html_orig/pages/cfg_app.php @@ -0,0 +1,71 @@ +
+

+ +
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
diff --git a/html_orig/pages/cfg_network.php b/html_orig/pages/cfg_network.php new file mode 100644 index 0000000..00cf133 --- /dev/null +++ b/html_orig/pages/cfg_network.php @@ -0,0 +1,95 @@ + + +
+

+ +
+ +
+ +
+ + +
+ +
+ + required> +
+ +
+ + required> +
+ +
+ + required> +
+ +
+ +
+
+ +
+

+ +
+ +
+ +
+ + +  (min) +
+ +
+ + required> +
+ +
+ + required> +
+ +
+ + required> +
+ +
+ + required> +
+ +
+ +
+
+ +
+

+ +
+ +
+
+ +
+
+ + diff --git a/html_orig/pages/cfg_wifi.php b/html_orig/pages/cfg_wifi.php new file mode 100644 index 0000000..cd8fe1c --- /dev/null +++ b/html_orig/pages/cfg_wifi.php @@ -0,0 +1,108 @@ +
+

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + + +
+ +
+ + +
+ +
+ +
+
+ +
+

+ +
+ + +
+ + + + +
+ + +
+ +
+
+ +
+ +
+ + + +
+ +
+ +
+
+ + + + diff --git a/html_orig/pages/cfg_wifi_conn.php b/html_orig/pages/cfg_wifi_conn.php new file mode 100755 index 0000000..33c8b64 --- /dev/null +++ b/html_orig/pages/cfg_wifi_conn.php @@ -0,0 +1,53 @@ +

+ +
+

.

+ +
+ + diff --git a/html_orig/help.html b/html_orig/pages/help.php similarity index 86% rename from html_orig/help.html rename to html_orig/pages/help.php index 9cece09..6f6314b 100644 --- a/html_orig/help.html +++ b/html_orig/pages/help.php @@ -1,29 +1,15 @@ - - - - - Help - ESP8266 Remote Terminal - - - - - - - -

Quick Reference

-

Wiring

@@ -328,12 +314,3 @@ - - - - - diff --git a/html_orig/pages/term.php b/html_orig/pages/term.php new file mode 100644 index 0000000..5f96a80 --- /dev/null +++ b/html_orig/pages/term.php @@ -0,0 +1,37 @@ + + +

%term_title%

+ +
+
+ +
+ +
+
+ + + + diff --git a/html_orig/sass/_fontello-embedded.scss b/html_orig/sass/_fontello-embedded.scss new file mode 100644 index 0000000..506e911 --- /dev/null +++ b/html_orig/sass/_fontello-embedded.scss @@ -0,0 +1,57 @@ +@font-face { + font-family: 'fontello'; + src: url('data:application/octet-stream;base64,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') format('woff'), + url('data:application/octet-stream;base64,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I7AEQ2CwASuwBSVhsAUlsAJiILAAUFiwQGBZZrABY7AEJmEgsAQlYGQjsAMlYGRQWCEbIyFZIyAgsAQmI0ZhOFktsDcssAAWICAgsAUmIC5HI0cjYSM8OC2wOCywABYgsAgjQiAgIEYjR7ABKyNhOC2wOSywABawAyWwAiVHI0cjYbAAVFguIDwjIRuwAiWwAiVHI0cjYSCwBSWwBCVHI0cjYbAGJbAFJUmwAiVhuQgACABjYyMgWGIbIVljuAQAYiCwAFBYsEBgWWawAWNgIy4jICA8ijgjIVktsDossAAWILAIQyAuRyNHI2EgYLAgYGawAmIgsABQWLBAYFlmsAFjIyAgPIo4LbA7LCMgLkawAiVGUlggPFkusSsBFCstsDwsIyAuRrACJUZQWCA8WS6xKwEUKy2wPSwjIC5GsAIlRlJYIDxZIyAuRrACJUZQWCA8WS6xKwEUKy2wPiywNSsjIC5GsAIlRlJYIDxZLrErARQrLbA/LLA2K4ogIDywBCNCijgjIC5GsAIlRlJYIDxZLrErARQrsARDLrArKy2wQCywABawBCWwBCYgLkcjRyNhsAlDKyMgPCAuIzixKwEUKy2wQSyxCAQlQrAAFrAEJbAEJSAuRyNHI2EgsAQjQrAJQysgsGBQWCCwQFFYswIgAyAbswImAxpZQkIjIEewBEOwAmIgsABQWLBAYFlmsAFjYCCwASsgiophILACQ2BkI7ADQ2FkUFiwAkNhG7ADQ2BZsAMlsAJiILAAUFiwQGBZZrABY2GwAiVGYTgjIDwjOBshICBGI0ewASsjYTghWbErARQrLbBCLLA1Ky6xKwEUKy2wQyywNishIyAgPLAEI0IjOLErARQrsARDLrArKy2wRCywABUgR7AAI0KyAAEBFRQTLrAxKi2wRSywABUgR7AAI0KyAAEBFRQTLrAxKi2wRiyxAAEUE7AyKi2wRyywNCotsEgssAAWRSMgLiBGiiNhOLErARQrLbBJLLAII0KwSCstsEossgAAQSstsEsssgABQSstsEwssgEAQSstsE0ssgEBQSstsE4ssgAAQistsE8ssgABQistsFAssgEAQistsFEssgEBQistsFIssgAAPistsFMssgABPistsFQssgEAPistsFUssgEBPistsFYssgAAQCstsFcssgABQCstsFgssgEAQCstsFkssgEBQCstsFossgAAQystsFsssgABQystsFwssgEAQystsF0ssgEBQystsF4ssgAAPystsF8ssgABPystsGAssgEAPystsGEssgEBPystsGIssDcrLrErARQrLbBjLLA3K7A7Ky2wZCywNyuwPCstsGUssAAWsDcrsD0rLbBmLLA4Ky6xKwEUKy2wZyywOCuwOystsGgssDgrsDwrLbBpLLA4K7A9Ky2waiywOSsusSsBFCstsGsssDkrsDsrLbBsLLA5K7A8Ky2wbSywOSuwPSstsG4ssDorLrErARQrLbBvLLA6K7A7Ky2wcCywOiuwPCstsHEssDorsD0rLbByLLMJBAIDRVghGyMhWUIrsAhlsAMkUHiwARUwLQBLuADIUlixAQGOWbABuQgACABjcLEABUKyAAEAKrEABUKzCgIBCCqxAAVCsw4AAQgqsQAGQroCwAABAAkqsQAHQroAQAABAAkqsQMARLEkAYhRWLBAiFixA2REsSYBiFFYugiAAAEEQIhjVFixAwBEWVlZWbMMAgEMKrgB/4WwBI2xAgBEAAA=') format('truetype'); +} +/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ +/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ +/* +@media screen and (-webkit-min-device-pixel-ratio:0) { + @font-face { + font-family: 'fontello'; + src: url('../font/fontello.svg?60007293#fontello') format('svg'); + } +} +*/ + + [class^="icn-"]:before, [class*=" icn-"]:before { + font-family: "fontello"; + font-style: normal; + font-weight: normal; + speak: none; + + display: inline-block; + text-decoration: inherit; + width: 1em; + margin-right: .2em; + text-align: center; + /* opacity: .8; */ + + /* For safety - reset parent styles, that can break glyph codes*/ + font-variant: normal; + text-transform: none; + + /* fix buttons height, for twitter bootstrap */ + line-height: 1em; + + /* Animation center compensation - margins should be symmetric */ + /* remove if not needed */ + margin-left: .2em; + + /* you can be more comfortable with increased icons size */ + /* font-size: 120%; */ + + /* Uncomment for 3D effect */ + /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ +} +.icn-persist:before { content: '\e800'; } /* '' */ +.icn-ok:before { content: '\e801'; } /* '' */ +.icn-donate:before { content: '\e802'; } /* '' */ +.icn-back:before { content: '\e80b'; } /* '' */ +.icn-about:before { content: '\e81f'; } /* '' */ +.icn-help:before { content: '\e826'; } /* '' */ +.icn-network:before { content: '\e82e'; } /* '' */ +.icn-restore:before { content: '\e836'; } /* '' */ +.icn-terminal:before { content: '\f108'; } /* '' */ +.icn-github:before { content: '\f113'; } /* '' */ +.icn-wifi:before { content: '\f1eb'; } /* '' */ diff --git a/html_orig/sass/_layout.scss b/html_orig/sass/_layout.scss deleted file mode 100644 index ffc2126..0000000 --- a/html_orig/sass/_layout.scss +++ /dev/null @@ -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; - } - } -} diff --git a/html_orig/sass/app.scss b/html_orig/sass/app.scss index c80c715..ecd627c 100755 --- a/html_orig/sass/app.scss +++ b/html_orig/sass/app.scss @@ -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; } } diff --git a/html_orig/sass/form/_buttons.scss b/html_orig/sass/form/_buttons.scss index 7c0b5b3..5f36222 100755 --- a/html_orig/sass/form/_buttons.scss +++ b/html_orig/sass/form/_buttons.scss @@ -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 { diff --git a/html_orig/sass/form/_fancy_button_mixins.scss b/html_orig/sass/form/_fancy_button_mixins.scss index 0ddb476..c7d2a4f 100755 --- a/html_orig/sass/form/_fancy_button_mixins.scss +++ b/html_orig/sass/form/_fancy_button_mixins.scss @@ -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; diff --git a/html_orig/sass/form/_form_elements.scss b/html_orig/sass/form/_form_elements.scss index cbc8ef2..83c2aea 100755 --- a/html_orig/sass/form/_form_elements.scss +++ b/html_orig/sass/form/_form_elements.scss @@ -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(); //} diff --git a/html_orig/sass/form/_form_layout.scss b/html_orig/sass/form/_form_layout.scss index 73320ab..ec373a7 100755 --- a/html_orig/sass/form/_form_layout.scss +++ b/html_orig/sass/form/_form_layout.scss @@ -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%; } } diff --git a/html_orig/sass/form/_index.scss b/html_orig/sass/form/_index.scss index 7736400..f2e9a7a 100755 --- a/html_orig/sass/form/_index.scss +++ b/html_orig/sass/form/_index.scss @@ -9,5 +9,5 @@ } } -//@import 'form_layout'; +@import 'form_layout'; //@import 'select'; diff --git a/html_orig/sass/layout/_base.scss b/html_orig/sass/layout/_base.scss new file mode 100755 index 0000000..eda4c7e --- /dev/null +++ b/html_orig/sass/layout/_base.scss @@ -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; +} diff --git a/html_orig/sass/layout/_box.scss b/html_orig/sass/layout/_box.scss new file mode 100755 index 0000000..a61c850 --- /dev/null +++ b/html_orig/sass/layout/_box.scss @@ -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; + } + } + } +} diff --git a/html_orig/sass/layout/_content.scss b/html_orig/sass/layout/_content.scss new file mode 100755 index 0000000..b1d6b96 --- /dev/null +++ b/html_orig/sass/layout/_content.scss @@ -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; + } +} diff --git a/html_orig/sass/layout/_index.scss b/html_orig/sass/layout/_index.scss new file mode 100755 index 0000000..f37ef8d --- /dev/null +++ b/html_orig/sass/layout/_index.scss @@ -0,0 +1,9 @@ +@import "base"; + +@import "outer-wrap"; +@import "menu"; +@import "content"; +@import "loader"; + +@import "box"; +@import "modal"; diff --git a/html_orig/sass/layout/_loader.scss b/html_orig/sass/layout/_loader.scss new file mode 100644 index 0000000..62af4cc --- /dev/null +++ b/html_orig/sass/layout/_loader.scss @@ -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; + } +} diff --git a/html_orig/sass/layout/_menu.scss b/html_orig/sass/layout/_menu.scss new file mode 100755 index 0000000..e23fffe --- /dev/null +++ b/html_orig/sass/layout/_menu.scss @@ -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); } + } +} diff --git a/html_orig/sass/_modal.scss b/html_orig/sass/layout/_modal.scss similarity index 78% rename from html_orig/sass/_modal.scss rename to html_orig/sass/layout/_modal.scss index c0c72fb..2b33c9a 100755 --- a/html_orig/sass/_modal.scss +++ b/html_orig/sass/layout/_modal.scss @@ -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 } } -*/ diff --git a/html_orig/sass/layout/_outer-wrap.scss b/html_orig/sass/layout/_outer-wrap.scss new file mode 100755 index 0000000..c32d3a8 --- /dev/null +++ b/html_orig/sass/layout/_outer-wrap.scss @@ -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; + } +} diff --git a/html_orig/sass/pages/_about.scss b/html_orig/sass/pages/_about.scss index 9eba5e8..d87129f 100644 --- a/html_orig/sass/pages/_about.scss +++ b/html_orig/sass/pages/_about.scss @@ -14,6 +14,7 @@ // mobile friendly #logo2 { max-width: 100%; + margin: 1rem; } td { diff --git a/html_orig/sass/pages/_term.scss b/html_orig/sass/pages/_term.scss index d642e6d..17634ba 100755 --- a/html_orig/sass/pages/_term.scss +++ b/html_orig/sass/pages/_term.scss @@ -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 { diff --git a/html_orig/sass/pages/_wifi.scss b/html_orig/sass/pages/_wifi.scss index 78a52f1..90a758d 100755 --- a/html_orig/sass/pages/_wifi.scss +++ b/html_orig/sass/pages/_wifi.scss @@ -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; } } } diff --git a/html_orig/sass/utils/_background-tiling.scss b/html_orig/sass/utils/_background-tiling.scss new file mode 100755 index 0000000..c1c5916 --- /dev/null +++ b/html_orig/sass/utils/_background-tiling.scss @@ -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; +} diff --git a/html_orig/sass/utils/_index.scss b/html_orig/sass/utils/_index.scss new file mode 100755 index 0000000..4e98733 --- /dev/null +++ b/html_orig/sass/utils/_index.scss @@ -0,0 +1,3 @@ +@import "background-tiling"; +@import "pointer"; +@import "misc"; diff --git a/html_orig/sass/utils/_misc.scss b/html_orig/sass/utils/_misc.scss new file mode 100755 index 0000000..91c0d77 --- /dev/null +++ b/html_orig/sass/utils/_misc.scss @@ -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; +} diff --git a/html_orig/sass/utils/_pointer.scss b/html_orig/sass/utils/_pointer.scss new file mode 100755 index 0000000..c6a9179 --- /dev/null +++ b/html_orig/sass/utils/_pointer.scss @@ -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; +} diff --git a/html_orig/server.sh b/html_orig/server.sh index 4b0b817..467ae0b 100755 --- a/html_orig/server.sh +++ b/html_orig/server.sh @@ -1,3 +1,3 @@ #!/bin/bash -xterm -e "php -S localhost:2000" +xterm -e "php -S 0.0.0.0:2000" diff --git a/html_orig/term.html b/html_orig/term.html deleted file mode 100644 index c0d5106..0000000 --- a/html_orig/term.html +++ /dev/null @@ -1,53 +0,0 @@ - - - - - ESP8266 Remote Terminal - - - - - - - - - -

ESP8266 Remote Terminal

- -
-
- -
- -
-
- - - - - - - diff --git a/html_orig/term_test.php b/html_orig/term_test.php deleted file mode 100644 index efa507a..0000000 --- a/html_orig/term_test.php +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - WiFi Settings - ESP8266 Remote Terminal - - - - - - -Loading… - -

WiFi settings

- -
- - - - - - - - - - - - - - - - - - - - - - -
WiFi mode%WiFiMode%
IP%StaIP%
Switch to
-
- -
-
-
- -
-
-

Some changes require a reboot, dropping connection. It can take a while to re-connect.

-

- If you lose access, 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. -

-

-
- -
-

Select AP to join

-
Scanning.
-
Can't scan in AP-only mode.
- -
- - - - - - - - diff --git a/html_orig/wifi_conn.html b/html_orig/wifi_conn.html deleted file mode 100755 index a9448dc..0000000 --- a/html_orig/wifi_conn.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - Connecting… - - - - - - -

Connecting to network

- -
-

Status:
.

- -
- - - - diff --git a/html_orig/wifi_test.php b/html_orig/wifi_test.php deleted file mode 100644 index b1863e7..0000000 --- a/html_orig/wifi_test.php +++ /dev/null @@ -1,15 +0,0 @@ ->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 diff --git a/libesphttpd b/libesphttpd index 03003ea..f3dd1a2 160000 --- a/libesphttpd +++ b/libesphttpd @@ -1 +1 @@ -Subproject commit 03003ea591a272df50159ba52f84ca84c5cad78e +Subproject commit f3dd1a25993775bec062a1906ced7b07a7fc9db1 diff --git a/user/cgi_appcfg.c b/user/cgi_appcfg.c new file mode 100644 index 0000000..e766139 --- /dev/null +++ b/user/cgi_appcfg.c @@ -0,0 +1,178 @@ +/* +Cgi/template routines for configuring non-wifi settings +*/ + +#include +#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; +} diff --git a/user/cgi_appcfg.h b/user/cgi_appcfg.h new file mode 100644 index 0000000..823a0f1 --- /dev/null +++ b/user/cgi_appcfg.h @@ -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 diff --git a/user/cgi_main.c b/user/cgi_main.c index 997ebef..70a5ec7 100644 --- a/user/cgi_main.c +++ b/user/cgi_main.c @@ -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; diff --git a/user/cgi_network.c b/user/cgi_network.c new file mode 100644 index 0000000..ad4ac21 --- /dev/null +++ b/user/cgi_network.c @@ -0,0 +1,251 @@ +/* +configuring the network settings +*/ + +#include +#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; +} diff --git a/user/cgi_network.h b/user/cgi_network.h new file mode 100644 index 0000000..818c953 --- /dev/null +++ b/user/cgi_network.h @@ -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 diff --git a/user/cgi_persist.c b/user/cgi_persist.c new file mode 100644 index 0000000..7f2faac --- /dev/null +++ b/user/cgi_persist.c @@ -0,0 +1,74 @@ +/* +Cgi/template routines for configuring non-wifi settings +*/ + +#include +#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; +} diff --git a/user/cgi_persist.h b/user/cgi_persist.h new file mode 100644 index 0000000..4875407 --- /dev/null +++ b/user/cgi_persist.h @@ -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 diff --git a/user/cgi_wifi.c b/user/cgi_wifi.c new file mode 100644 index 0000000..653f928 --- /dev/null +++ b/user/cgi_wifi.c @@ -0,0 +1,585 @@ +/* +Cgi/template routines for the /wifi url. +*/ + +/* + * ---------------------------------------------------------------------------- + * "THE BEER-WARE LICENSE" (Revision 42): + * Jeroen Domburg 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 + */ + +#include +#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; +} diff --git a/user/cgi_wifi.h b/user/cgi_wifi.h new file mode 100644 index 0000000..53999dc --- /dev/null +++ b/user/cgi_wifi.h @@ -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 diff --git a/user/io.c b/user/io.c index e3ba069..4969e20 100644 --- a/user/io.c +++ b/user/io.c @@ -11,6 +11,8 @@ #include #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; } diff --git a/user/persist.c b/user/persist.c new file mode 100644 index 0000000..c1b7541 --- /dev/null +++ b/user/persist.c @@ -0,0 +1,171 @@ +// +// Created by MightyPork on 2017/07/09. +// + +#include "persist.h" +#include +#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."); +} diff --git a/user/persist.h b/user/persist.h new file mode 100644 index 0000000..be334e4 --- /dev/null +++ b/user/persist.h @@ -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 diff --git a/user/routes.c b/user/routes.c index 7c77313..ee2fcd7 100644 --- a/user/routes.c +++ b/user/routes.c @@ -2,15 +2,17 @@ #include #include #include -#include #include #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(), diff --git a/user/screen.c b/user/screen.c index 54fd004..ce9075d 100644 --- a/user/screen.c +++ b/user/screen.c @@ -1,9 +1,42 @@ #include #include #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; } } diff --git a/user/screen.h b/user/screen.h index 701e759..5ab2bc6 100644 --- a/user/screen.h +++ b/user/screen.h @@ -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); diff --git a/user/user_main.c b/user/user_main.c index 95364d4..8730ff9 100644 --- a/user/user_main.c +++ b/user/user_main.c @@ -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(); diff --git a/user/wifimgr.c b/user/wifimgr.c new file mode 100644 index 0000000..5a4b132 --- /dev/null +++ b/user/wifimgr.c @@ -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."); +} diff --git a/user/wifimgr.h b/user/wifimgr.h new file mode 100644 index 0000000..79a4246 --- /dev/null +++ b/user/wifimgr.h @@ -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 +#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