From e617b4f2832f8366f9dbd966c3c1b6165673e0bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sun, 9 Jul 2017 00:41:08 +0200 Subject: [PATCH 01/19] implemented "wifi manager" and many advanced wifi config options. todo unification and persistence --- CMakeLists.txt | 6 +- html_orig/_start.php | 144 +++++++++ html_orig/jssrc/wifi.js | 6 +- html_orig/wifi.html | 8 +- libesphttpd | 2 +- user/cgi_wifi.c | 654 ++++++++++++++++++++++++++++++++++++++++ user/cgi_wifi.h | 33 ++ user/routes.c | 7 +- user/user_main.c | 67 ++-- user/wifi_manager.c | 194 ++++++++++++ user/wifi_manager.h | 47 +++ 11 files changed, 1129 insertions(+), 39 deletions(-) create mode 100644 html_orig/_start.php create mode 100644 user/cgi_wifi.c create mode 100644 user/cgi_wifi.h create mode 100644 user/wifi_manager.c create mode 100644 user/wifi_manager.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ee1d441..3d9277e 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,8 @@ set(SOURCE_FILES include/ets_sys_extra.h user/io.c user/io.h + user/cgi_wifi.c + user/cgi_wifi.h user/cgi_ping.c user/cgi_reset.c user/uart_driver.c @@ -113,7 +113,7 @@ 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/wifi_manager.c user/wifi_manager.h) include_directories(include) include_directories(user) diff --git a/html_orig/_start.php b/html_orig/_start.php new file mode 100644 index 0000000..31bab8a --- /dev/null +++ b/html_orig/_start.php @@ -0,0 +1,144 @@ + [ $prod ? '/status' : '/page_status.php', 'Home' ], + 'wifi' => [ $prod ? '/wifi' : '/page_wifi.php', 'WiFi config' ], + 'about' => [ $prod ? '/about' : '/page_about.php', 'About' ], +]; + +$appname = 'Current Analyser'; + +function e($s) { + return htmlspecialchars($s, ENT_HTML5|ENT_QUOTES); +} + +?> + + + + + + + <?= e($menu[$page][1]) ?> - <?= e($appname) ?> + + + + + + +
+ +
+ Loading… + + + + + + + + + + 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/jssrc/wifi.js b/html_orig/jssrc/wifi.js index dd7f370..9fcf4c0 100644 --- a/html_orig/jssrc/wifi.js +++ b/html_orig/jssrc/wifi.js @@ -118,9 +118,9 @@ } $('#modeswitch').html([ - 'Client+AP AP only', - 'Client+AP', - 'Client only AP only' + 'Client+AP AP only', + 'Client+AP', + 'Client only AP only' ][obj.mode-1]); }; diff --git a/html_orig/wifi.html b/html_orig/wifi.html index 32ee1a8..e59f965 100644 --- a/html_orig/wifi.html +++ b/html_orig/wifi.html @@ -32,8 +32,8 @@ -
-
@@ -41,8 +41,8 @@ -
-
diff --git a/libesphttpd b/libesphttpd index 03003ea..38c6c91 160000 --- a/libesphttpd +++ b/libesphttpd @@ -1 +1 @@ -Subproject commit 03003ea591a272df50159ba52f84ca84c5cad78e +Subproject commit 38c6c91f50e5a5cfba8df8309a95e814695accba diff --git a/user/cgi_wifi.c b/user/cgi_wifi.c new file mode 100644 index 0000000..11a9291 --- /dev/null +++ b/user/cgi_wifi.c @@ -0,0 +1,654 @@ +/* +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 + */ + +// TODO convert to work with WiFi Manager +// TODO make changes write to wificonf and apply when a different CGI is run (/wifi/apply or something) +// TODO (connection will trigger this immediately, with some delayto show the connecting page. Then polling cna proceed as usual) + +#include +#include "cgi_wifi.h" + +/** 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; + +/** Progress of connection to AP enum */ +typedef enum { + CONNTRY_IDLE = 0, + CONNTRY_WORKING = 1, + CONNTRY_SUCCESS = 2, + CONNTRY_FAIL = 3, +} ConnTry; + +/** Connection result var */ +static ConnTry connTryStatus = CONNTRY_IDLE; + +/** Connection to AP periodic check timer */ +static os_timer_t staCheckTimer; + +/** reset_later() timer */ +static ETSTimer resetTmr; + +/** + * Callback for reset_later() + */ +static void ICACHE_FLASH_ATTR resetTmrCb(void *arg) +{ + system_restart(); +} + +/** + * Schedule a reset + * @param ms reset delay (milliseconds) + */ +static void ICACHE_FLASH_ATTR reset_later(int ms) +{ + os_timer_disarm(&resetTmr); + os_timer_setfn(&resetTmr, resetTmrCb, NULL); + os_timer_arm(&resetTmr, ms, false); +} + +/** + * 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; + } +} + +/** Temp store for new ap info. */ +static struct station_config stconf; + +/** + * This routine is ran some time after a connection attempt to an access point. If + * the connect succeeds, this gets the module in STA-only mode. + */ +static void ICACHE_FLASH_ATTR staCheckConnStatus(void *arg) +{ + int x = wifi_station_get_connect_status(); + if (x == STATION_GOT_IP) { + info("Connected to AP."); + connTryStatus = CONNTRY_SUCCESS; + + // This would enter STA only mode, but that kills the browser page if using STA+AP. + // Instead we stay in the current mode and let the user switch manually. + + //wifi_set_opmode(STATION_MODE); + //system_restart(); + } + else { + connTryStatus = CONNTRY_FAIL; + error("Connection failed."); + } +} + +/** + * Actually connect to a station. This routine is timed because I had problems + * with immediate connections earlier. It probably was something else that caused it, + * but I can't be arsed to put the code back :P + */ +static void ICACHE_FLASH_ATTR cgiWiFiConnect_do(void *arg) +{ + int x; + dbg("Try to connect to AP..."); + + wifi_station_disconnect(); + wifi_station_set_config(&stconf); + wifi_station_connect(); + + x = wifi_get_opmode(); + connTryStatus = CONNTRY_WORKING; + if (x != STATION_MODE) { + //Schedule check + os_timer_disarm(&staCheckTimer); + os_timer_setfn(&staCheckTimer, staCheckConnStatus, NULL); + os_timer_arm(&staCheckTimer, 15000, 0); //time out after 15 secs of trying to connect + } +} + +/** + * This cgi uses the routines above to connect to a specific access point with the + * given ESSID using the given password. + * + * Args: + * - essid = SSID to connect to + * - passwd = password to connect with + */ +httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiConnect(HttpdConnData *connData) +{ + char essid[128]; + char passwd[128]; + static os_timer_t reassTimer; + + if (connData->conn == NULL) { + //Connection aborted. Clean up. + return HTTPD_CGI_DONE; + } + + int ssilen = httpdFindArg(connData->post->buff, "essid", essid, sizeof(essid)); + int passlen = httpdFindArg(connData->post->buff, "passwd", passwd, sizeof(passwd)); + + if (ssilen == -1 || passlen == -1) { + error("Not rx needed args!"); + httpdRedirect(connData, "/wifi"); + } + else { + strncpy((char *) stconf.ssid, essid, 32); + strncpy((char *) stconf.password, passwd, 64); + info("Try to connect to AP %s pw %s", essid, passwd); + + //Schedule disconnect/connect + os_timer_disarm(&reassTimer); + os_timer_setfn(&reassTimer, cgiWiFiConnect_do, NULL); + // redirect & start connecting a little bit later + os_timer_arm(&reassTimer, 2000, 0); // was 500, increased so the connecting page has time to load + + connTryStatus = CONNTRY_IDLE; + httpdRedirect(connData, "/wifi/connecting"); + } + return HTTPD_CGI_DONE; +} + +/** + * 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]; + int len; + struct ip_info info; + int st = wifi_station_get_connect_status(); + + httpdStartResponse(connData, 200); + httpdHeader(connData, "Content-Type", "application/json"); + httpdEndHeaders(connData); + + if (connTryStatus == CONNTRY_IDLE) { + len = sprintf(buff, "{\"status\": \"idle\"}"); + } + else if (connTryStatus == CONNTRY_WORKING || connTryStatus == CONNTRY_SUCCESS) { + if (st == STATION_GOT_IP) { + wifi_get_ip_info(STATION_IF, &info); + len = sprintf(buff, "{\"status\": \"success\", \"ip\": \"" + IPSTR + "\"}", GOOD_IP2STR(info.ip.addr)); + os_timer_disarm(&staCheckTimer); + os_timer_setfn(&staCheckTimer, staCheckConnStatus, NULL); + os_timer_arm(&staCheckTimer, 1000, 0); + } else { + len = sprintf(buff, "{\"status\": \"working\"}"); + } + } + else { + len = sprintf(buff, "{\"status\": \"fail\"}"); + } + + httpdSend(connData, buff, len); + return HTTPD_CGI_DONE; +} + +/** + * Universal CGI endpoint to set WiFi params. + * Note that some may cause a (delayed) restart. + * + * Args: + * - ap_ch = channel 1-14 + * - ap_ssid = SSID name for AP mode + * - opmode = WiFi mode (resets device) + * - hostname = set client hostname + * - tpw = set transmit power + * - sta_dhcp_lt = DHCP server lease time + * - sta_ip = station mode static IP + * - sta_mask = station mode static IP mask (apply only if 'ip' is also sent) + * - sta_gw = station mode default gateway (apply only if 'ip' is also sent) + * (can be left out, then 0.0.0.0 is used and outbound connections won't work - + * but we're normally not making any) + * - dhcp = enable or disable DHCP on the station interface + */ +httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiSetParams(HttpdConnData *connData) +{ + int len, len2, len3; + char buff[50]; + char buff2[50]; + char buff3[50]; + + // TODO change so that settings are not applied immediately, but persisted first + // TODO apply temporary changes like static IP in wifi event CBs + + if (connData->conn == NULL) { + //Connection aborted. Clean up. + return HTTPD_CGI_DONE; + } + + // AP channel (applies in AP-only mode) + len = httpdFindArg(connData->getArgs, "ap_ch", buff, sizeof(buff)); + if (len > 0) { + info("Setting WiFi channel for AP-only mode to: %s", buff); + int channel = atoi(buff); + if (channel > 0 && channel < 15) { + dbg("Setting channel=%d", channel); + + struct softap_config wificfg; + wifi_softap_get_config(&wificfg); + wificfg.channel = (uint8) channel; + wifi_softap_set_config(&wificfg); + } else { + warn("Bad channel value %s, allowed 1-14", buff); + } + } + + // SSID name in AP mode + len = httpdFindArg(connData->getArgs, "ap_ssid", buff, sizeof(buff)); + if (len > 0) { + 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; + + info("Setting SSID to %s", buff); + + struct softap_config wificfg; + wifi_softap_get_config(&wificfg); + sprintf((char *) wificfg.ssid, buff); + wificfg.ssid_len = strlen((char *) wificfg.ssid); + wifi_softap_set_config(&wificfg); + } + + // WiFi mode + len = httpdFindArg(connData->getArgs, "opmode", buff, sizeof(buff)); + if (len > 0) { + dbg("Setting WiFi opmode to: %s", buff); + int mode = atoi(buff); + if (mode > NULL_MODE && mode < MAX_MODE) { + wifi_set_opmode(mode); + reset_later(200); + } else { + warn("Bad opmode value %s", buff); + } + } + + // Hostname in station mode (for DHCP) + len = httpdFindArg(connData->getArgs, "hostname", buff, sizeof(buff)); + if (len > 0) { + dbg("Setting station sta_hostname to: %s", buff); + wifi_station_set_hostname(buff); + // TODO persistency, re-apply on boot + } + + // Hostname in station mode (for DHCP) + len = httpdFindArg(connData->getArgs, "tpw", buff, sizeof(buff)); + if (len > 0) { + dbg("Setting AP power to: %s", buff); + int tpw = atoi(buff); + // min tpw to avoid user locking themselves out TODO verify + if (tpw >= 0 && tpw <= 82) { + // TODO persistency, re-apply on boot + system_phy_set_max_tpw(tpw); + } else { + warn("tpw %s out of allowed range 0-82.", buff); + } + } + + // DHCP server lease time + len = httpdFindArg(connData->getArgs, "ap_dhcp_lt", buff, sizeof(buff)); + if (len > 0) { + dbg("Setting DHCP lease time to: %s min.", buff); + int min = atoi(buff); + if (min >= 1 && min <= 2880) { + // TODO persistency, re-apply on boot + // TODO set only if we're in the right opmode + wifi_softap_set_dhcps_lease_time(min); + } else { + warn("Lease time %s out of allowed range 1-2880.", buff); + } + } + + // DHCP enable / disable (disable means static IP is enabled) + len = httpdFindArg(connData->getArgs, "sta_dhcp", buff, sizeof(buff)); + if (len > 0) { + dbg("DHCP enable = %s", buff); + int enable = atoi(buff); + if (enable != 0) { + wifi_station_dhcpc_stop(); + } else { + wifi_station_dhcpc_start(); + } + // TODO persistency + } + + // Static IP + len = httpdFindArg(connData->getArgs, "sta_ip", buff, sizeof(buff)); + len2 = httpdFindArg(connData->getArgs, "sta_mask", buff2, sizeof(buff2)); + len3 = httpdFindArg(connData->getArgs, "sta_gw", buff3, sizeof(buff3)); + if (len > 0) { + // TODO set only if we're in the right opmode + // TODO persistency + dbg("Setting static IP = %s", buff); + struct ip_info ipinfo; + ipinfo.ip.addr = ipaddr_addr(buff); + ipinfo.netmask.addr = IPADDR_NONE; + ipinfo.gw.addr = IPADDR_NONE; + if (len2 > 0) { + dbg("Netmask = %s", buff2); + ipinfo.netmask.addr = ipaddr_addr(buff2); + } + if (len3 > 0) { + dbg("Gateway = %s", buff3); + ipinfo.gw.addr = ipaddr_addr(buff3); + } + // TODO ... + wifi_station_dhcpc_stop(); + wifi_set_ip_info(STATION_IF, &ipinfo); + } + + httpdRedirect(connData, "/wifi"); + 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[500]; + int x; + int connectStatus; + static struct station_config stconf; + static struct softap_config apconf; + + if (token == NULL) { + // We're done + return HTTPD_CGI_DONE; + } + + wifi_station_get_config(&stconf); + wifi_softap_get_config(&apconf); + + + strcpy(buff, "Unknown"); + if (streq(token, "WiFiMode")) { + x = wifi_get_opmode(); + strcpy(buff, opmode2str(x)); + } + else if (streq(token, "WiFiModeNum")) { + x = wifi_get_opmode(); + sprintf(buff, "%d", x); + } + else if (streq(token, "WiFiChannel")) { + sprintf(buff, "%d", apconf.channel); + } + else if (streq(token, "APName")) { + sprintf(buff, "%s", apconf.ssid); + } + else if (streq(token, "StaIP")) { + 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)); + } + } + else if (streq(token, "StaSSID")) { + connectStatus = wifi_station_get_connect_status(); + x = wifi_get_opmode(); + if (x == SOFTAP_MODE || connectStatus != STATION_GOT_IP) { + strcpy(buff, ""); + } + else { + strcpy(buff, (char *) stconf.ssid); + } + } + else if (streq(token, "WiFiPasswd")) { + strcpy(buff, (char *) stconf.password); + } + else if (streq(token, "WiFiapwarn")) { + // TODO get rid of this + x = wifi_get_opmode(); + if (x == SOFTAP_MODE) { // 2 + strcpy(buff, "Enable client for scanning."); + } + else if (x == STATIONAP_MODE) { // 3 + strcpy(buff, + "Switch: Client only, AP only"); + } + else { // 1 + strcpy(buff, + "Switch: Client+AP, AP only"); + } + } + 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..a300d2a --- /dev/null +++ b/user/cgi_wifi.h @@ -0,0 +1,33 @@ +#ifndef CGIWIFI_H +#define CGIWIFI_H + +#include "httpd.h" + +/** + * Convert IP hex to arguments for printf. + * Library IP2STR(ip) does not work correctly due to unaligned memory access. + */ +#define GOOD_IP2STR(ip) ((ip)>>0)&0xff, ((ip)>>8)&0xff, ((ip)>>16)&0xff, ((ip)>>24)&0xff + +httpd_cgi_state cgiWiFiScan(HttpdConnData *connData); +httpd_cgi_state cgiWiFiConnect(HttpdConnData *connData); +httpd_cgi_state cgiWiFiConnStatus(HttpdConnData *connData); +httpd_cgi_state cgiWiFiSetParams(HttpdConnData *connData); +httpd_cgi_state tplWlan(HttpdConnData *connData, char *token, void **arg); + +// WiFi config options: +// - Persistent +// - channel +// - AP ssid +// - opmode +// - AP to connect to +// - Temporary +// - sta_hostname (sta) +// - tpw (ap, sta+ap?) +// - dhcp_lt (ap, sta+ap) +// - static IP +// - static mask +// - static gw +// - dhcp enable or disable + +#endif diff --git a/user/routes.c b/user/routes.c index 7c77313..2f3dc68 100644 --- a/user/routes.c +++ b/user/routes.c @@ -2,11 +2,10 @@ #include #include #include -#include #include #include "routes.h" - +#include "cgi_wifi.h" #include "cgi_reset.h" #include "cgi_ping.h" #include "cgi_main.h" @@ -48,9 +47,7 @@ HttpdBuiltInUrl routes[] = { 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_CGI("/wifi/set", cgiWiFiSetParams), ROUTE_FILESYSTEM(), ROUTE_END(), diff --git a/user/user_main.c b/user/user_main.c index 95364d4..0990b41 100644 --- a/user/user_main.c +++ b/user/user_main.c @@ -26,6 +26,7 @@ #include "user_main.h" #include "uart_driver.h" #include "ansi_parser_callbacks.h" +#include "wifi_manager.h" #ifdef ESPFS_POS CgiUploadFlashDef uploadParams={ @@ -48,6 +49,7 @@ CgiUploadFlashDef uploadParams={ #endif static ETSTimer prHeapTimer; +static ETSTimer userStartTimer; /** Periodically show heap usage */ static void ICACHE_FLASH_ATTR prHeapTimerCb(void *arg) @@ -77,11 +79,49 @@ static void ICACHE_FLASH_ATTR prHeapTimerCb(void *arg) cnt++; } +static void user_start(void *unused) +{ + // TODO load persistent data, init wificonf + + // 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; + } + + // Set up WiFi & connect + wifimgr_restore_defaults(); + wifimgr_apply_settings(); + + // Captive portal + captdnsInit(); + + // Server + httpdInit(routes, 80); + + // The terminal screen + screen_init(); + + // Print the CANCEL character to indicate the module has restarted + // Critically important for client application if any kind of screen persistence / content re-use is needed + UART_WriteChar(UART0, 24, UART_TIMEOUT_US); // 0x18 - 24 - CAN + + info("Listening on UART0, 115200-8-N-1!"); +} + //Main routine. Initialize stdout, the I/O, filesystem and the webserver and we're done. void ICACHE_FLASH_ATTR user_init(void) { serialInit(); + // Prevent WiFi starting and connecting by default + // let wifi manager handle it + wifi_station_set_auto_connect(false); + wifi_set_opmode(NULL_MODE); + printf("\r\n"); banner("====== ESP8266 Remote Terminal ======"); banner_info("Firmware (c) Ondrej Hruska, 2017"); @@ -94,15 +134,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,25 +142,15 @@ 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); - // The terminal screen - screen_init(); - - // Print the CANCEL character to indicate the module has restarted - // Critically important for client application if any kind of screen persistence / content re-use is needed - UART_WriteChar(UART0, 24, UART_TIMEOUT_US); // 0x18 - 24 - CAN - - info("Listening on UART0, 115200-8-N-1!"); + // do later (some functions do not yet work if called from user_init) + os_timer_disarm(&userStartTimer); + os_timer_setfn(&userStartTimer, user_start, NULL); + os_timer_arm(&userStartTimer, 10, 0); } // ---- unused funcs removed from sdk to save space --- diff --git a/user/wifi_manager.c b/user/wifi_manager.c new file mode 100644 index 0000000..d19a6dc --- /dev/null +++ b/user/wifi_manager.c @@ -0,0 +1,194 @@ +// +// Created by MightyPork on 2017/07/08. +// + +#include "wifi_manager.h" + +WiFiSettingsBlock wificonf; + +/** + * 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 wifimgr_restore_defaults(void) +{ + u8 mac[6]; + wifi_get_macaddr(SOFTAP_IF, mac); + + wificonf.opmode = STATIONAP_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_dhcp_lease_time = 120; + wificonf.ap_hidden = false; + + IP4_ADDR(&wificonf.ap_ip.ip, 192, 168, mac[5], 1); + IP4_ADDR(&wificonf.ap_ip.netmask, 255, 255, 255, 0); + IP4_ADDR(&wificonf.ap_ip.gw, 192, 168, mac[5], 1); + + // --- Client config --- + wificonf.sta_ssid[0] = 0; + wificonf.sta_password[0] = 0; + //sprintf((char *) wificonf.sta_ssid, "Chlivek"); + //sprintf((char *) wificonf.sta_password, "prase chrochta"); + strcpy((char *) wificonf.sta_hostname, (char *) wificonf.ap_ssid); // use the same value for sta_hostname as AP name + wificonf.sta_dhcp_enable = true; + + IP4_ADDR(&wificonf.sta_ip.ip, 192, 168, 0, (mac[5]==1?2:mac[5]));// avoid being the same as "default gw" + IP4_ADDR(&wificonf.sta_ip.netmask, 255, 255, 255, 0); + IP4_ADDR(&wificonf.sta_ip.gw, 192, 168, 0, 1); +} + +/** + * Event handler + */ +void wifimgr_event_cb(System_Event_t *event) +{ + switch (event->event) { +// case EVENT_STAMODE_CONNECTED: +// EVENT_STAMODE_DISCONNECTED, +// EVENT_STAMODE_AUTHMODE_CHANGE, +// EVENT_STAMODE_GOT_IP, +// EVENT_STAMODE_DHCP_TIMEOUT, +// EVENT_SOFTAPMODE_STACONNECTED, +// EVENT_SOFTAPMODE_STADISCONNECTED, +// EVENT_SOFTAPMODE_PROBEREQRECVED, + } +} + +static void 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); + dbg("[WiFi] Hostname = %s", wificonf.sta_hostname); + wifi_station_set_hostname((char*)wificonf.sta_hostname); + + 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_ip.ip.addr)); + dbg("[WiFi] Client.mask = "IPSTR, GOOD_IP2STR(wificonf.sta_ip.netmask.addr)); + dbg("[WiFi] Client.gw = "IPSTR, GOOD_IP2STR(wificonf.sta_ip.gw.addr)); + + wifi_station_dhcpc_stop(); + // Load static IP config + if (!wifi_set_ip_info(STATION_IF, &wificonf.sta_ip)) { + error("[WiFi] Error setting static IP!"); + return; + } + } + + info("[WiFi] Trying to connect to AP..."); + wifi_station_connect(); +} + +static void 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 = 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_ip.ip.addr)); + dbg("[WiFi] SoftAP.mask = "IPSTR, GOOD_IP2STR(wificonf.ap_ip.netmask.addr)); + dbg("[WiFi] SoftAP.gw = "IPSTR, GOOD_IP2STR(wificonf.ap_ip.gw.addr)); + + wifi_softap_dhcps_stop(); + + // Configure DHCP + if (!wifi_set_ip_info(SOFTAP_IF, &wificonf.ap_ip)) { + error("[WiFi] IP set fail!"); + return; + } + + info("[WiFi] Configuring SoftAP DHCP server..."); + struct dhcps_lease dhcp_lease; + struct ip_addr ip; + ip.addr = wificonf.ap_ip.ip.addr; + ip.addr = (ip.addr & 0x00FFFFFFUL) | ((((ip.addr >> 24) & 0xFF) + 99UL) << 24); + dhcp_lease.start_ip.addr = ip.addr; + ip.addr = (ip.addr & 0x00FFFFFFUL) | ((((ip.addr >> 24) & 0xFF) + 100UL) << 24); + dhcp_lease.end_ip.addr = ip.addr; + + dbg("[WiFi] DHCP.start = "IPSTR, GOOD_IP2STR(dhcp_lease.start_ip.addr)); + dbg("[WiFi] DHCP.end = "IPSTR, GOOD_IP2STR(dhcp_lease.end_ip.addr)); + dbg("[WiFi] DHCP.lease = %d minutes", wificonf.ap_dhcp_lease_time); + + if (!wifi_softap_set_dhcps_lease(&dhcp_lease)) { + error("[WiFi] DHCP address range set fail!"); + return; + } + + if (!wifi_softap_set_dhcps_lease_time(wificonf.ap_dhcp_lease_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 wifimgr_apply_settings(void) +{ + info("[WiFi] Initializing WiFi manager..."); +// wifi_set_event_handler_cb(wifimgr_event_cb); + + // Force wifi cycle + dbg("[WiFi] WiFi reset to apply new settings"); + wifi_set_opmode(NULL_MODE); + wifi_set_opmode(wificonf.opmode); + + // Configure the client + if (wificonf.opmode == STATIONAP_MODE || wificonf.opmode == STATION_MODE) { + configure_station(); + } + + // Configure the AP + if (wificonf.opmode == STATIONAP_MODE || wificonf.opmode == SOFTAP_MODE) { + configure_ap(); + } +} diff --git a/user/wifi_manager.h b/user/wifi_manager.h new file mode 100644 index 0000000..daf09e3 --- /dev/null +++ b/user/wifi_manager.h @@ -0,0 +1,47 @@ +// +// 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" + +/** + * 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 : 32; + u8 sta_hostname[32]; + u32 tpw; + + // --- AP config --- + u32 ap_channel; // 32 for alignment, needs 8 + u8 ap_ssid[32]; + u8 ap_password[32]; + u32 ap_hidden; + u32 ap_dhcp_lease_time; // in minutes + + struct ip_info ap_ip; + + // --- Client config --- + u8 sta_ssid[32]; + u8 sta_password[64]; + u32 sta_dhcp_enable; + + struct ip_info sta_ip; +} WiFiSettingsBlock; + +extern WiFiSettingsBlock wificonf; + +void wifimgr_restore_defaults(void); + +void wifimgr_apply_settings(void); + +#endif //ESP_VT100_FIRMWARE_WIFI_MANAGER_H From 509b19a9bcf92e21d51450fc0884790b384a84e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sun, 9 Jul 2017 16:44:41 +0200 Subject: [PATCH 02/19] implemented a persistence subsystem --- CMakeLists.txt | 2 +- user/persist.c | 192 +++++++++++++++++++++++++++++ user/persist.h | 39 ++++++ user/screen.c | 48 +++++--- user/screen.h | 28 +++-- user/user_main.c | 73 ++++++----- user/{wifi_manager.c => wifimgr.c} | 69 ++++------- user/{wifi_manager.h => wifimgr.h} | 19 +-- 8 files changed, 357 insertions(+), 113 deletions(-) create mode 100644 user/persist.c create mode 100644 user/persist.h rename user/{wifi_manager.c => wifimgr.c} (73%) rename user/{wifi_manager.h => wifimgr.h} (70%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3d9277e..7fa43d7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -113,7 +113,7 @@ set(SOURCE_FILES user/cgi_sockets.h user/ansi_parser_callbacks.c user/ansi_parser_callbacks.h - user/user_main.h user/wifi_manager.c user/wifi_manager.h) + user/user_main.h user/wifimgr.c user/wifimgr.h user/persist.c user/persist.h) include_directories(include) include_directories(user) diff --git a/user/persist.c b/user/persist.c new file mode 100644 index 0000000..381b266 --- /dev/null +++ b/user/persist.c @@ -0,0 +1,192 @@ +// +// Created by MightyPork on 2017/07/09. +// + +#include "persist.h" +#include +#include "wifimgr.h" +#include "screen.h" + +FullPersistBlock persist; + +#define PERSIST_SECTOR_ID 0x3D + +//region Persist and restore individual modules + +/** + * Load persistent settings to live config structs + */ +static void ICACHE_FLASH_ATTR +load_settings_to_live(void) +{ + dbg("[Persist] Loading current settings to modules..."); + memcpy(&wificonf, &persist.current.wificonf, sizeof(WiFiConfigBlock)); + memcpy(&termconf, &persist.current.termconf, sizeof(TerminalConfigBlock)); + // ... +} + +static void ICACHE_FLASH_ATTR +store_settings_from_live(void) +{ + dbg("[Persist] Collecting live settings to persist block..."); + memcpy(&persist.current.wificonf, &wificonf, sizeof(wificonf)); + memcpy(&persist.current.termconf, &termconf, sizeof(termconf)); + // ... +} + +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(PersistBundle *bundle) +{ + return calculateCRC32((uint8_t *) bundle, sizeof(PersistBundle) - 4); +} + +/** + * Load, verify and apply persistent config + */ +void ICACHE_FLASH_ATTR +persist_load(void) +{ + info("[Persist] Loading stored settings from FLASH..."); + + bool hard_reset = false; + + // Try to load + hard_reset |= !system_param_load(PERSIST_SECTOR_ID, 0, &persist, sizeof(persist)); + + // Verify checksums + if (hard_reset || + (compute_checksum(&persist.defaults) != persist.defaults.checksum) || + (compute_checksum(&persist.current) != persist.current.checksum)) { + error("[Persist] Config block failed to load, restoring to hard defaults."); + hard_reset = true; + } + + if (hard_reset) { + persist_restore_hard_default(); + // this also stores them to flash and applies to modues + } else { + load_settings_to_live(); + apply_live_settings(); + } + + info("[Persist] All settings loaded and applied."); +} + +void ICACHE_FLASH_ATTR +persist_store(void) +{ + info("[Persist] Storing all settings to FLASH..."); + store_settings_from_live(); + + // 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(persist))) { + 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 live -> current + store_settings_from_live(); + + // Store current -> default + memcpy(&persist.defaults, &persist.current, sizeof(persist.current)); + 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(persist.defaults)); + load_settings_to_live(); + 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.."); + + store_settings_from_live(); + memcpy(&persist.defaults, &persist.current, sizeof(persist.current)); + + persist_store(); + + info("[Persist] Default settings updated."); +} diff --git a/user/persist.h b/user/persist.h new file mode 100644 index 0000000..197eaa6 --- /dev/null +++ b/user/persist.h @@ -0,0 +1,39 @@ +// +// 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" + +typedef struct { + WiFiConfigBlock wificonf; + TerminalConfigBlock termconf; + // ... + // other settings here + // ... + uint32_t checksum; // computed before write and tested on load. If it doesn't match, values are reset to hard defaults. +} PersistBundle; + +typedef struct { + PersistBundle defaults; // defaults are stored here + PersistBundle current; // settings persisted by user +} FullPersistBlock; + +// Persist holds the data currently loaded from the flash +extern FullPersistBlock 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/screen.c b/user/screen.c index 54fd004..90f3fbc 100644 --- a/user/screen.c +++ b/user/screen.c @@ -4,6 +4,36 @@ //region Data structures +TerminalConfigBlock termconf; + +/** + * 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, "ESP8266 Wireless Terminal"); + 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) +{ + screen_init(); +} + +#define W termconf.width +#define H termconf.height + /** * Highest permissible value of the color attribute */ @@ -50,16 +80,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 +119,8 @@ cursor_reset(void) { cursor.x = 0; cursor.y = 0; - cursor.fg = SCREEN_DEF_FG; - cursor.bg = SCREEN_DEF_BG; + cursor.fg = termconf.default_fg; + cursor.bg = termconf.default_bg; cursor.visible = 1; cursor.inverse = 0; cursor.autowrap = 1; @@ -363,8 +383,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.default_fg; + cursor_sav.bg = termconf.default_bg; cursor_sav.inverse = 0; } } diff --git a/user/screen.h b/user/screen.h index 701e759..36ab637 100644 --- a/user/screen.h +++ b/user/screen.h @@ -34,6 +34,25 @@ * */ +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]; +} TerminalConfigBlock; + +// Live config +extern TerminalConfigBlock termconf; + +void terminal_restore_defaults(void); +void terminal_apply_settings(void); + /** * Maximum screen size (determines size of the static data array) * @@ -42,20 +61,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 0990b41..67c7762 100644 --- a/user/user_main.c +++ b/user/user_main.c @@ -26,7 +26,8 @@ #include "user_main.h" #include "uart_driver.h" #include "ansi_parser_callbacks.h" -#include "wifi_manager.h" +#include "wifimgr.h" +#include "persist.h" #ifdef ESPFS_POS CgiUploadFlashDef uploadParams={ @@ -48,9 +49,6 @@ CgiUploadFlashDef uploadParams={ #define INCLUDE_FLASH_FNS #endif -static ETSTimer prHeapTimer; -static ETSTimer userStartTimer; - /** Periodically show heap usage */ static void ICACHE_FLASH_ATTR prHeapTimerCb(void *arg) { @@ -79,42 +77,15 @@ static void ICACHE_FLASH_ATTR prHeapTimerCb(void *arg) cnt++; } -static void user_start(void *unused) -{ - // TODO load persistent data, init wificonf - - // 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; - } - - // Set up WiFi & connect - wifimgr_restore_defaults(); - wifimgr_apply_settings(); - - // Captive portal - captdnsInit(); - - // Server - httpdInit(routes, 80); - - // The terminal screen - screen_init(); - - // Print the CANCEL character to indicate the module has restarted - // Critically important for client application if any kind of screen persistence / content re-use is needed - UART_WriteChar(UART0, 24, UART_TIMEOUT_US); // 0x18 - 24 - CAN - - info("Listening on UART0, 115200-8-N-1!"); -} +// 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 @@ -147,12 +118,40 @@ void ICACHE_FLASH_ATTR user_init(void) os_timer_setfn(&prHeapTimer, prHeapTimerCb, NULL); os_timer_arm(&prHeapTimer, 1000, 1); - // do later (some functions do not yet work if called from user_init) + // 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(); + + // Print the CANCEL character to indicate the module has restarted + // Critically important for client application if any kind of screen persistence / content re-use is needed + UART_WriteChar(UART0, 24, UART_TIMEOUT_US); // 0x18 - 24 - CAN + + info("Listening on UART0, 115200-8-N-1!"); +} + // ---- unused funcs removed from sdk to save space --- // вызывается из phy_chip_v6.o diff --git a/user/wifi_manager.c b/user/wifimgr.c similarity index 73% rename from user/wifi_manager.c rename to user/wifimgr.c index d19a6dc..04ec363 100644 --- a/user/wifi_manager.c +++ b/user/wifimgr.c @@ -2,37 +2,40 @@ // Created by MightyPork on 2017/07/08. // -#include "wifi_manager.h" +#include "wifimgr.h" -WiFiSettingsBlock wificonf; +WiFiConfigBlock wificonf; /** * 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 wifimgr_restore_defaults(void) +void ICACHE_FLASH_ATTR +wifimgr_restore_defaults(void) { u8 mac[6]; wifi_get_macaddr(SOFTAP_IF, mac); - wificonf.opmode = STATIONAP_MODE; + 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_dhcp_lease_time = 120; wificonf.ap_hidden = false; - IP4_ADDR(&wificonf.ap_ip.ip, 192, 168, mac[5], 1); + IP4_ADDR(&wificonf.ap_ip.ip, 192, 168, 4, 60); IP4_ADDR(&wificonf.ap_ip.netmask, 255, 255, 255, 0); - IP4_ADDR(&wificonf.ap_ip.gw, 192, 168, mac[5], 1); + wificonf.ap_ip.gw.addr = wificonf.ap_ip.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; + wificonf.ap_dhcp_lease_time = 120; // --- Client config --- wificonf.sta_ssid[0] = 0; wificonf.sta_password[0] = 0; - //sprintf((char *) wificonf.sta_ssid, "Chlivek"); - //sprintf((char *) wificonf.sta_password, "prase chrochta"); strcpy((char *) wificonf.sta_hostname, (char *) wificonf.ap_ssid); // use the same value for sta_hostname as AP name wificonf.sta_dhcp_enable = true; @@ -41,24 +44,8 @@ void wifimgr_restore_defaults(void) IP4_ADDR(&wificonf.sta_ip.gw, 192, 168, 0, 1); } -/** - * Event handler - */ -void wifimgr_event_cb(System_Event_t *event) -{ - switch (event->event) { -// case EVENT_STAMODE_CONNECTED: -// EVENT_STAMODE_DISCONNECTED, -// EVENT_STAMODE_AUTHMODE_CHANGE, -// EVENT_STAMODE_GOT_IP, -// EVENT_STAMODE_DHCP_TIMEOUT, -// EVENT_SOFTAPMODE_STACONNECTED, -// EVENT_SOFTAPMODE_STADISCONNECTED, -// EVENT_SOFTAPMODE_PROBEREQRECVED, - } -} - -static void configure_station(void) +static void ICACHE_FLASH_ATTR +configure_station(void) { info("[WiFi] Configuring Station mode..."); struct station_config conf; @@ -97,7 +84,8 @@ static void configure_station(void) wifi_station_connect(); } -static void configure_ap(void) +static void ICACHE_FLASH_ATTR +configure_ap(void) { bool suc; @@ -108,7 +96,7 @@ static void configure_ap(void) 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 = strlen((char *) conf.ssid); + 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 @@ -137,19 +125,11 @@ static void configure_ap(void) } info("[WiFi] Configuring SoftAP DHCP server..."); - struct dhcps_lease dhcp_lease; - struct ip_addr ip; - ip.addr = wificonf.ap_ip.ip.addr; - ip.addr = (ip.addr & 0x00FFFFFFUL) | ((((ip.addr >> 24) & 0xFF) + 99UL) << 24); - dhcp_lease.start_ip.addr = ip.addr; - ip.addr = (ip.addr & 0x00FFFFFFUL) | ((((ip.addr >> 24) & 0xFF) + 100UL) << 24); - dhcp_lease.end_ip.addr = ip.addr; - - dbg("[WiFi] DHCP.start = "IPSTR, GOOD_IP2STR(dhcp_lease.start_ip.addr)); - dbg("[WiFi] DHCP.end = "IPSTR, GOOD_IP2STR(dhcp_lease.end_ip.addr)); + 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_lease_time); - if (!wifi_softap_set_dhcps_lease(&dhcp_lease)) { + if (!wifi_softap_set_dhcps_lease(&wificonf.ap_dhcp_range)) { error("[WiFi] DHCP address range set fail!"); return; } @@ -172,13 +152,12 @@ static void configure_ap(void) /** * Register the WiFi event listener, cycle WiFi, apply settings */ -void wifimgr_apply_settings(void) +void ICACHE_FLASH_ATTR +wifimgr_apply_settings(void) { - info("[WiFi] Initializing WiFi manager..."); -// wifi_set_event_handler_cb(wifimgr_event_cb); + info("[WiFi] Initializing..."); // Force wifi cycle - dbg("[WiFi] WiFi reset to apply new settings"); wifi_set_opmode(NULL_MODE); wifi_set_opmode(wificonf.opmode); @@ -191,4 +170,6 @@ void wifimgr_apply_settings(void) if (wificonf.opmode == STATIONAP_MODE || wificonf.opmode == SOFTAP_MODE) { configure_ap(); } + + info("[WiFi] WiFi settings applied."); } diff --git a/user/wifi_manager.h b/user/wifimgr.h similarity index 70% rename from user/wifi_manager.h rename to user/wifimgr.h index daf09e3..2bb7f11 100644 --- a/user/wifi_manager.h +++ b/user/wifimgr.h @@ -17,28 +17,29 @@ * This block can be used eg. for WiFi config backup. */ typedef struct { - WIFI_MODE opmode : 32; - u8 sta_hostname[32]; - u32 tpw; + WIFI_MODE opmode : 8; + u8 tpw; // --- AP config --- - u32 ap_channel; // 32 for alignment, needs 8 + u8 ap_channel; u8 ap_ssid[32]; u8 ap_password[32]; - u32 ap_hidden; - u32 ap_dhcp_lease_time; // in minutes + bool ap_hidden; + u16 ap_dhcp_lease_time; // in minutes + struct dhcps_lease ap_dhcp_range; struct ip_info ap_ip; // --- Client config --- u8 sta_ssid[32]; u8 sta_password[64]; - u32 sta_dhcp_enable; + u8 sta_hostname[32]; // hostname set via the API. This does not seem to have much effect. + bool sta_dhcp_enable; struct ip_info sta_ip; -} WiFiSettingsBlock; +} WiFiConfigBlock; -extern WiFiSettingsBlock wificonf; +extern WiFiConfigBlock wificonf; void wifimgr_restore_defaults(void); From 5c8eab387eb723e3ce19344aaef3eb72629ad848 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sun, 9 Jul 2017 17:22:47 +0200 Subject: [PATCH 03/19] some renaming --- user/persist.c | 32 ++++++++--------- user/persist.h | 14 ++++---- user/screen.c | 37 ++++++++++--------- user/screen.h | 10 ++++-- user/wifimgr.c | 97 +++++++++++++++++++++++++------------------------- user/wifimgr.h | 4 +-- 6 files changed, 102 insertions(+), 92 deletions(-) diff --git a/user/persist.c b/user/persist.c index 381b266..7fc2c9e 100644 --- a/user/persist.c +++ b/user/persist.c @@ -7,7 +7,7 @@ #include "wifimgr.h" #include "screen.h" -FullPersistBlock persist; +PersistBlock persist; #define PERSIST_SECTOR_ID 0x3D @@ -20,17 +20,17 @@ static void ICACHE_FLASH_ATTR load_settings_to_live(void) { dbg("[Persist] Loading current settings to modules..."); - memcpy(&wificonf, &persist.current.wificonf, sizeof(WiFiConfigBlock)); - memcpy(&termconf, &persist.current.termconf, sizeof(TerminalConfigBlock)); + memcpy(wificonf, &persist.current.wificonf, sizeof(WiFiConfigBundle)); + memcpy(termconf, &persist.current.termconf, sizeof(TerminalConfigBundle)); // ... } static void ICACHE_FLASH_ATTR -store_settings_from_live(void) +store_all_settings_from_live(void) { dbg("[Persist] Collecting live settings to persist block..."); - memcpy(&persist.current.wificonf, &wificonf, sizeof(wificonf)); - memcpy(&persist.current.termconf, &termconf, sizeof(termconf)); + memcpy(&persist.current.wificonf, wificonf, sizeof(WiFiConfigBundle)); + memcpy(&persist.current.termconf, termconf, sizeof(TerminalConfigBundle)); // ... } @@ -86,9 +86,9 @@ calculateCRC32(const uint8_t *data, size_t length) * @return */ static uint32_t ICACHE_FLASH_ATTR -compute_checksum(PersistBundle *bundle) +compute_checksum(AppConfigBundle *bundle) { - return calculateCRC32((uint8_t *) bundle, sizeof(PersistBundle) - 4); + return calculateCRC32((uint8_t *) bundle, sizeof(AppConfigBundle) - 4); } /** @@ -102,7 +102,7 @@ persist_load(void) bool hard_reset = false; // Try to load - hard_reset |= !system_param_load(PERSIST_SECTOR_ID, 0, &persist, sizeof(persist)); + hard_reset |= !system_param_load(PERSIST_SECTOR_ID, 0, &persist, sizeof(PersistBlock)); // Verify checksums if (hard_reset || @@ -127,13 +127,13 @@ void ICACHE_FLASH_ATTR persist_store(void) { info("[Persist] Storing all settings to FLASH..."); - store_settings_from_live(); + store_all_settings_from_live(); // 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(persist))) { + if (!system_param_save_with_protect(PERSIST_SECTOR_ID, &persist, sizeof(PersistBlock))) { error("[Persist] Store to flash failed!"); } info("[Persist] All settings persisted."); @@ -151,10 +151,10 @@ persist_restore_hard_default(void) restore_live_settings_to_hard_defaults(); // Store live -> current - store_settings_from_live(); + store_all_settings_from_live(); // Store current -> default - memcpy(&persist.defaults, &persist.current, sizeof(persist.current)); + memcpy(&persist.defaults, &persist.current, sizeof(AppConfigBundle)); persist_store(); info("[Persist] All settings restored to hard defaults."); @@ -169,7 +169,7 @@ void ICACHE_FLASH_ATTR persist_restore_default(void) { info("[Persist] Restoring live settings to stored defaults..."); - memcpy(&persist.current, &persist.defaults, sizeof(persist.defaults)); + memcpy(&persist.current, &persist.defaults, sizeof(AppConfigBundle)); load_settings_to_live(); apply_live_settings(); info("[Persist] Settings restored to stored defaults."); @@ -183,8 +183,8 @@ persist_set_as_default(void) { info("[Persist] Storing live settings as defaults.."); - store_settings_from_live(); - memcpy(&persist.defaults, &persist.current, sizeof(persist.current)); + store_all_settings_from_live(); + memcpy(&persist.defaults, &persist.current, sizeof(AppConfigBundle)); persist_store(); diff --git a/user/persist.h b/user/persist.h index 197eaa6..676c1e6 100644 --- a/user/persist.h +++ b/user/persist.h @@ -14,21 +14,21 @@ #include "screen.h" typedef struct { - WiFiConfigBlock wificonf; - TerminalConfigBlock termconf; + WiFiConfigBundle wificonf; + TerminalConfigBundle termconf; // ... // other settings here // ... uint32_t checksum; // computed before write and tested on load. If it doesn't match, values are reset to hard defaults. -} PersistBundle; +} AppConfigBundle; typedef struct { - PersistBundle defaults; // defaults are stored here - PersistBundle current; // settings persisted by user -} FullPersistBlock; + AppConfigBundle defaults; // defaults are stored here + AppConfigBundle current; // settings persisted by user +} PersistBlock; // Persist holds the data currently loaded from the flash -extern FullPersistBlock persist; +extern PersistBlock persist; void persist_load(void); void persist_restore_hard_default(void); diff --git a/user/screen.c b/user/screen.c index 90f3fbc..e2e07f1 100644 --- a/user/screen.c +++ b/user/screen.c @@ -1,26 +1,28 @@ #include #include #include "screen.h" +#include "persist.h" //region Data structures -TerminalConfigBlock termconf; +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, "ESP8266 Wireless Terminal"); - sprintf(termconf.btn1, "1"); - sprintf(termconf.btn2, "2"); - sprintf(termconf.btn3, "3"); - sprintf(termconf.btn4, "4"); - sprintf(termconf.btn5, "5"); + termconf->default_bg = 0; + termconf->default_fg = 7; + termconf->width = 26; + termconf->height = 10; + sprintf(termconf->title, "ESP8266 Wireless Terminal"); + sprintf(termconf->btn1, "1"); + sprintf(termconf->btn2, "2"); + sprintf(termconf->btn3, "3"); + sprintf(termconf->btn4, "4"); + sprintf(termconf->btn5, "5"); } /** @@ -28,11 +30,12 @@ void terminal_restore_defaults(void) */ void terminal_apply_settings(void) { + memcpy(&termconf_scratch, termconf, sizeof(TerminalConfigBundle)); screen_init(); } -#define W termconf.width -#define H termconf.height +#define W termconf_scratch.width +#define H termconf_scratch.height /** * Highest permissible value of the color attribute @@ -119,8 +122,8 @@ cursor_reset(void) { cursor.x = 0; cursor.y = 0; - cursor.fg = termconf.default_fg; - cursor.bg = termconf.default_bg; + cursor.fg = termconf_scratch.default_fg; + cursor.bg = termconf_scratch.default_bg; cursor.visible = 1; cursor.inverse = 0; cursor.autowrap = 1; @@ -383,8 +386,8 @@ screen_cursor_save(bool withAttrs) cursor_sav.bg = cursor.bg; cursor_sav.inverse = cursor.inverse; } else { - cursor_sav.fg = termconf.default_fg; - cursor_sav.bg = termconf.default_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 36ab637..5ab2bc6 100644 --- a/user/screen.h +++ b/user/screen.h @@ -45,10 +45,16 @@ typedef struct { char btn3[10]; char btn4[10]; char btn5[10]; -} TerminalConfigBlock; +} TerminalConfigBundle; // Live config -extern TerminalConfigBlock termconf; +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); diff --git a/user/wifimgr.c b/user/wifimgr.c index 04ec363..5d120e4 100644 --- a/user/wifimgr.c +++ b/user/wifimgr.c @@ -3,8 +3,9 @@ // #include "wifimgr.h" +#include "persist.h" -WiFiConfigBlock wificonf; +WiFiConfigBundle * const wificonf = &persist.current.wificonf; /** * Restore defaults in the WiFi config block. @@ -17,31 +18,31 @@ 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; + 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_ip.ip, 192, 168, 4, 60); - IP4_ADDR(&wificonf.ap_ip.netmask, 255, 255, 255, 0); - wificonf.ap_ip.gw.addr = wificonf.ap_ip.gw.addr; + IP4_ADDR(&wificonf->ap_ip.ip, 192, 168, 4, 60); + IP4_ADDR(&wificonf->ap_ip.netmask, 255, 255, 255, 0); + wificonf->ap_ip.gw.addr = wificonf->ap_ip.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; - wificonf.ap_dhcp_lease_time = 120; + 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; + wificonf->ap_dhcp_lease_time = 120; // --- Client config --- - wificonf.sta_ssid[0] = 0; - wificonf.sta_password[0] = 0; - strcpy((char *) wificonf.sta_hostname, (char *) wificonf.ap_ssid); // use the same value for sta_hostname as AP name - wificonf.sta_dhcp_enable = true; - - IP4_ADDR(&wificonf.sta_ip.ip, 192, 168, 0, (mac[5]==1?2:mac[5]));// avoid being the same as "default gw" - IP4_ADDR(&wificonf.sta_ip.netmask, 255, 255, 255, 0); - IP4_ADDR(&wificonf.sta_ip.gw, 192, 168, 0, 1); + wificonf->sta_ssid[0] = 0; + wificonf->sta_password[0] = 0; + strcpy((char *) wificonf->sta_hostname, (char *) wificonf->ap_ssid); // use the same value for sta_hostname as AP name + wificonf->sta_dhcp_enable = true; + + IP4_ADDR(&wificonf->sta_ip.ip, 192, 168, 0, (mac[5]==1?2:mac[5]));// avoid being the same as "default gw" + IP4_ADDR(&wificonf->sta_ip.netmask, 255, 255, 255, 0); + IP4_ADDR(&wificonf->sta_ip.gw, 192, 168, 0, 1); } static void ICACHE_FLASH_ATTR @@ -49,17 +50,17 @@ 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); + 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); - dbg("[WiFi] Hostname = %s", wificonf.sta_hostname); - wifi_station_set_hostname((char*)wificonf.sta_hostname); + dbg("[WiFi] Hostname = %s", wificonf->sta_hostname); + wifi_station_set_hostname((char*)wificonf->sta_hostname); - if (wificonf.sta_dhcp_enable) { + if (wificonf->sta_dhcp_enable) { dbg("[WiFi] Starting DHCP..."); if (!wifi_station_dhcpc_start()) { error("[WiFi] DHCp failed to start!"); @@ -68,13 +69,13 @@ configure_station(void) } else { info("[WiFi] Setting up static IP..."); - dbg("[WiFi] Client.ip = "IPSTR, GOOD_IP2STR(wificonf.sta_ip.ip.addr)); - dbg("[WiFi] Client.mask = "IPSTR, GOOD_IP2STR(wificonf.sta_ip.netmask.addr)); - dbg("[WiFi] Client.gw = "IPSTR, GOOD_IP2STR(wificonf.sta_ip.gw.addr)); + dbg("[WiFi] Client.ip = "IPSTR, GOOD_IP2STR(wificonf->sta_ip.ip.addr)); + dbg("[WiFi] Client.mask = "IPSTR, GOOD_IP2STR(wificonf->sta_ip.netmask.addr)); + dbg("[WiFi] Client.gw = "IPSTR, GOOD_IP2STR(wificonf->sta_ip.gw.addr)); wifi_station_dhcpc_stop(); // Load static IP config - if (!wifi_set_ip_info(STATION_IF, &wificonf.sta_ip)) { + if (!wifi_set_ip_info(STATION_IF, &wificonf->sta_ip)) { error("[WiFi] Error setting static IP!"); return; } @@ -92,12 +93,12 @@ configure_ap(void) 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.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.ssid_hidden = wificonf->ap_hidden; conf.max_connection = 4; // default 4 (max possible) conf.beacon_interval = 100; // default 100 ms @@ -112,29 +113,29 @@ configure_ap(void) // Set IP info("[WiFi] Configuring SoftAP local IP..."); - dbg("[WiFi] SoftAP.ip = "IPSTR, GOOD_IP2STR(wificonf.ap_ip.ip.addr)); - dbg("[WiFi] SoftAP.mask = "IPSTR, GOOD_IP2STR(wificonf.ap_ip.netmask.addr)); - dbg("[WiFi] SoftAP.gw = "IPSTR, GOOD_IP2STR(wificonf.ap_ip.gw.addr)); + dbg("[WiFi] SoftAP.ip = "IPSTR, GOOD_IP2STR(wificonf->ap_ip.ip.addr)); + dbg("[WiFi] SoftAP.mask = "IPSTR, GOOD_IP2STR(wificonf->ap_ip.netmask.addr)); + dbg("[WiFi] SoftAP.gw = "IPSTR, GOOD_IP2STR(wificonf->ap_ip.gw.addr)); wifi_softap_dhcps_stop(); // Configure DHCP - if (!wifi_set_ip_info(SOFTAP_IF, &wificonf.ap_ip)) { + if (!wifi_set_ip_info(SOFTAP_IF, &wificonf->ap_ip)) { 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_lease_time); + 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_lease_time); - if (!wifi_softap_set_dhcps_lease(&wificonf.ap_dhcp_range)) { + 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_lease_time)) { + if (!wifi_softap_set_dhcps_lease_time(wificonf->ap_dhcp_lease_time)) { error("[WiFi] DHCP lease time set fail!"); return; } @@ -159,15 +160,15 @@ wifimgr_apply_settings(void) // Force wifi cycle wifi_set_opmode(NULL_MODE); - wifi_set_opmode(wificonf.opmode); + wifi_set_opmode(wificonf->opmode); // Configure the client - if (wificonf.opmode == STATIONAP_MODE || wificonf.opmode == STATION_MODE) { + if (wificonf->opmode == STATIONAP_MODE || wificonf->opmode == STATION_MODE) { configure_station(); } // Configure the AP - if (wificonf.opmode == STATIONAP_MODE || wificonf.opmode == SOFTAP_MODE) { + if (wificonf->opmode == STATIONAP_MODE || wificonf->opmode == SOFTAP_MODE) { configure_ap(); } diff --git a/user/wifimgr.h b/user/wifimgr.h index 2bb7f11..fa616d7 100644 --- a/user/wifimgr.h +++ b/user/wifimgr.h @@ -37,9 +37,9 @@ typedef struct { bool sta_dhcp_enable; struct ip_info sta_ip; -} WiFiConfigBlock; +} WiFiConfigBundle; -extern WiFiConfigBlock wificonf; +extern WiFiConfigBundle * const wificonf; void wifimgr_restore_defaults(void); From d5bd53b78addf3ce44d1ccdc60e0ca6fc1b87b52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sun, 9 Jul 2017 18:10:48 +0200 Subject: [PATCH 04/19] cleaning & linked boot button to new factory reset impl --- user/io.c | 14 ++++++++------ user/user_main.c | 2 +- user/wifimgr.c | 13 ++++++++++--- 3 files changed, 19 insertions(+), 10 deletions(-) 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/user_main.c b/user/user_main.c index 67c7762..8730ff9 100644 --- a/user/user_main.c +++ b/user/user_main.c @@ -91,7 +91,7 @@ void ICACHE_FLASH_ATTR user_init(void) // Prevent WiFi starting and connecting by default // let wifi manager handle it wifi_station_set_auto_connect(false); - wifi_set_opmode(NULL_MODE); + 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 ======"); diff --git a/user/wifimgr.c b/user/wifimgr.c index 5d120e4..02f170f 100644 --- a/user/wifimgr.c +++ b/user/wifimgr.c @@ -40,7 +40,7 @@ wifimgr_restore_defaults(void) strcpy((char *) wificonf->sta_hostname, (char *) wificonf->ap_ssid); // use the same value for sta_hostname as AP name wificonf->sta_dhcp_enable = true; - IP4_ADDR(&wificonf->sta_ip.ip, 192, 168, 0, (mac[5]==1?2:mac[5]));// avoid being the same as "default gw" + IP4_ADDR(&wificonf->sta_ip.ip, 192, 168, 0, (mac[5]==1?2:mac[5])); // avoid being the same as "default gw" IP4_ADDR(&wificonf->sta_ip.netmask, 255, 255, 255, 0); IP4_ADDR(&wificonf->sta_ip.gw, 192, 168, 0, 1); } @@ -159,8 +159,15 @@ wifimgr_apply_settings(void) info("[WiFi] Initializing..."); // Force wifi cycle - wifi_set_opmode(NULL_MODE); - wifi_set_opmode(wificonf->opmode); + // Disconnect - may not be needed? + WIFI_MODE opmode = wifi_get_opmode(); + if (opmode == STATIONAP_MODE || opmode == STATION_MODE) { + wifi_station_disconnect(); + } + + // This should hopefully deinit everything + wifi_set_opmode_current(NULL_MODE); + wifi_set_opmode_current(wificonf->opmode); // Configure the client if (wificonf->opmode == STATIONAP_MODE || wificonf->opmode == STATION_MODE) { From a85582b94ed16d081d5ada3fa4655c50dd86a159 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Mon, 10 Jul 2017 00:26:29 +0200 Subject: [PATCH 05/19] wifi settings remake, now correctly applied only as needed via change flags --- user/cgi_wifi.c | 510 ++++++++++++++++++++++++++++++------------------ user/persist.c | 37 +--- user/persist.h | 2 +- user/wifimgr.c | 67 ++++--- user/wifimgr.h | 25 ++- 5 files changed, 384 insertions(+), 257 deletions(-) diff --git a/user/cgi_wifi.c b/user/cgi_wifi.c index 11a9291..6347325 100644 --- a/user/cgi_wifi.c +++ b/user/cgi_wifi.c @@ -19,6 +19,11 @@ Cgi/template routines for the /wifi url. #include #include "cgi_wifi.h" +#include "wifimgr.h" +#include "persist.h" + +// strcpy that adds 0 at the end of the buffer. Returns void. +#define strncpy_safe(dst, src, n) do { strncpy((char *)(dst), (char *)(src), (n)); dst[(n)-1]=0; } while (0) /** WiFi access point data */ typedef struct { @@ -53,28 +58,6 @@ static ConnTry connTryStatus = CONNTRY_IDLE; /** Connection to AP periodic check timer */ static os_timer_t staCheckTimer; -/** reset_later() timer */ -static ETSTimer resetTmr; - -/** - * Callback for reset_later() - */ -static void ICACHE_FLASH_ATTR resetTmrCb(void *arg) -{ - system_restart(); -} - -/** - * Schedule a reset - * @param ms reset delay (milliseconds) - */ -static void ICACHE_FLASH_ATTR reset_later(int ms) -{ - os_timer_disarm(&resetTmr); - os_timer_setfn(&resetTmr, resetTmrCb, NULL); - os_timer_arm(&resetTmr, ms, false); -} - /** * Calculate approximate signal strength % from RSSI */ @@ -278,9 +261,6 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiScan(HttpdConnData *connData) } } -/** Temp store for new ap info. */ -static struct station_config stconf; - /** * This routine is ran some time after a connection attempt to an access point. If * the connect succeeds, this gets the module in STA-only mode. @@ -305,23 +285,29 @@ static void ICACHE_FLASH_ATTR staCheckConnStatus(void *arg) } /** - * Actually connect to a station. This routine is timed because I had problems - * with immediate connections earlier. It probably was something else that caused it, - * but I can't be arsed to put the code back :P + * Delayed connect callback */ static void ICACHE_FLASH_ATTR cgiWiFiConnect_do(void *arg) { int x; + struct station_config cfg; + dbg("Try to connect to AP..."); + strncpy_safe(cfg.password, wificonf->sta_password, PASSWORD_LEN); + strncpy_safe(cfg.ssid, wificonf->sta_ssid, SSID_LEN); + cfg.bssid_set = 0; + wifi_station_disconnect(); - wifi_station_set_config(&stconf); + wifi_station_set_config(&cfg); wifi_station_connect(); x = wifi_get_opmode(); connTryStatus = CONNTRY_WORKING; + // Assumption: + // if we're in station mode, no need to check: the browser will be disconnected + // and the user finds out whether it succeeded or not by checking if they can connect if (x != STATION_MODE) { - //Schedule check os_timer_disarm(&staCheckTimer); os_timer_setfn(&staCheckTimer, staCheckConnStatus, NULL); os_timer_arm(&staCheckTimer, 15000, 0); //time out after 15 secs of trying to connect @@ -338,8 +324,8 @@ static void ICACHE_FLASH_ATTR cgiWiFiConnect_do(void *arg) */ httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiConnect(HttpdConnData *connData) { - char essid[128]; - char passwd[128]; + char ssid[100]; + char password[100]; static os_timer_t reassTimer; if (connData->conn == NULL) { @@ -347,26 +333,26 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiConnect(HttpdConnData *connData) return HTTPD_CGI_DONE; } - int ssilen = httpdFindArg(connData->post->buff, "essid", essid, sizeof(essid)); - int passlen = httpdFindArg(connData->post->buff, "passwd", passwd, sizeof(passwd)); + int ssilen = httpdFindArg(connData->post->buff, "sta_ssid", ssid, sizeof(ssid)); + int passlen = httpdFindArg(connData->post->buff, "sta_password", password, sizeof(password)); if (ssilen == -1 || passlen == -1) { - error("Not rx needed args!"); + error("Did not receive the required arguments!"); httpdRedirect(connData, "/wifi"); } else { - strncpy((char *) stconf.ssid, essid, 32); - strncpy((char *) stconf.password, passwd, 64); - info("Try to connect to AP %s pw %s", essid, passwd); + strncpy_safe(wificonf->sta_ssid, ssid, SSID_LEN); + strncpy_safe(wificonf->sta_password, password, PASSWORD_LEN); + info("Try to connect to AP \"%s\" pw \"%s\".", ssid, password); //Schedule disconnect/connect os_timer_disarm(&reassTimer); os_timer_setfn(&reassTimer, cgiWiFiConnect_do, NULL); // redirect & start connecting a little bit later - os_timer_arm(&reassTimer, 2000, 0); // was 500, increased so the connecting page has time to load + os_timer_arm(&reassTimer, 1000, 0); // was 500, increased so the connecting page has time to load connTryStatus = CONNTRY_IDLE; - httpdRedirect(connData, "/wifi/connecting"); + httpdRedirect(connData, "/wifi/connecting"); // this page is meant to show progress } return HTTPD_CGI_DONE; } @@ -413,59 +399,83 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiConnStatus(HttpdConnData *connData) return HTTPD_CGI_DONE; } +/** reset_later() timer */ + +/** + * Callback for async timer + */ +static void ICACHE_FLASH_ATTR applyWifiSettingsLaterCb(void *arg) +{ + wifimgr_apply_settings(); +} + /** * Universal CGI endpoint to set WiFi params. * Note that some may cause a (delayed) restart. - * - * Args: - * - ap_ch = channel 1-14 - * - ap_ssid = SSID name for AP mode - * - opmode = WiFi mode (resets device) - * - hostname = set client hostname - * - tpw = set transmit power - * - sta_dhcp_lt = DHCP server lease time - * - sta_ip = station mode static IP - * - sta_mask = station mode static IP mask (apply only if 'ip' is also sent) - * - sta_gw = station mode default gateway (apply only if 'ip' is also sent) - * (can be left out, then 0.0.0.0 is used and outbound connections won't work - - * but we're normally not making any) - * - dhcp = enable or disable DHCP on the station interface */ httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiSetParams(HttpdConnData *connData) { - int len, len2, len3; + static ETSTimer timer; + char buff[50]; - char buff2[50]; - char buff3[50]; - // TODO change so that settings are not applied immediately, but persisted first - // TODO apply temporary changes like static IP in wifi event CBs + char redir_url_buf[300]; + char *redir_url = redir_url_buf; + redir_url += sprintf(redir_url, "/wifi?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 channel (applies in AP-only mode) - len = httpdFindArg(connData->getArgs, "ap_ch", buff, sizeof(buff)); - if (len > 0) { - info("Setting WiFi channel for AP-only mode to: %s", buff); +#define GET_ARG(key) (httpdFindArg(connData->getArgs, key, buff, sizeof(buff)) > 0) + + // ---- 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,"); + } + } + + // ---- 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 + 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) { - dbg("Setting channel=%d", channel); - - struct softap_config wificfg; - wifi_softap_get_config(&wificfg); - wificfg.channel = (uint8) channel; - wifi_softap_set_config(&wificfg); + wificonf->ap_channel = (u8) channel; + wifi_change_flags.ap = true; } else { - warn("Bad channel value %s, allowed 1-14", buff); + warn("Bad channel value \"%s\", allowed 1-14", buff); + redir_url += sprintf(redir_url, "ap_channel,"); } } - // SSID name in AP mode - len = httpdFindArg(connData->getArgs, "ap_ssid", buff, sizeof(buff)); - if (len > 0) { + // ---- 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]; @@ -474,103 +484,198 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiSetParams(HttpdConnData *connData) } buff[i] = 0; - info("Setting SSID to %s", buff); - - struct softap_config wificfg; - wifi_softap_get_config(&wificfg); - sprintf((char *) wificfg.ssid, buff); - wificfg.ssid_len = strlen((char *) wificfg.ssid); - wifi_softap_set_config(&wificfg); - } - - // WiFi mode - len = httpdFindArg(connData->getArgs, "opmode", buff, sizeof(buff)); - if (len > 0) { - dbg("Setting WiFi opmode to: %s", buff); - int mode = atoi(buff); - if (mode > NULL_MODE && mode < MAX_MODE) { - wifi_set_opmode(mode); - reset_later(200); + if (strlen(buff) > 0) { + info("Setting SSID to \"%s\"", buff); + strncpy_safe(wificonf->ap_ssid, buff, SSID_LEN); + wifi_change_flags.ap = true; } else { - warn("Bad opmode value %s", buff); + warn("Bad SSID len."); + redir_url += sprintf(redir_url, "ap_ssid,"); } } - // Hostname in station mode (for DHCP) - len = httpdFindArg(connData->getArgs, "hostname", buff, sizeof(buff)); - if (len > 0) { - dbg("Setting station sta_hostname to: %s", buff); - wifi_station_set_hostname(buff); - // TODO persistency, re-apply on boot - } + // ---- AP password ---- - // Hostname in station mode (for DHCP) - len = httpdFindArg(connData->getArgs, "tpw", buff, sizeof(buff)); - if (len > 0) { - dbg("Setting AP power to: %s", buff); - int tpw = atoi(buff); - // min tpw to avoid user locking themselves out TODO verify - if (tpw >= 0 && tpw <= 82) { - // TODO persistency, re-apply on boot - system_phy_set_max_tpw(tpw); + 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)) { + info("Setting AP password to \"%s\"", buff); + strncpy_safe(wificonf->ap_password, buff, PASSWORD_LEN); + wifi_change_flags.ap = true; } else { - warn("tpw %s out of allowed range 0-82.", buff); + warn("Bad password len."); + redir_url += sprintf(redir_url, "ap_password,"); } } - // DHCP server lease time - len = httpdFindArg(connData->getArgs, "ap_dhcp_lt", buff, sizeof(buff)); - if (len > 0) { + // ---- Hide AP network (do not announce) ---- + + if (GET_ARG("ap_hidden")) { + dbg("AP hidden = %s", buff); + int hidden = atoi(buff); + wificonf->ap_hidden = (hidden != 0); + wifi_change_flags.ap = true; + } + + // ---- 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) { - // TODO persistency, re-apply on boot - // TODO set only if we're in the right opmode - wifi_softap_set_dhcps_lease_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_range_start")) { + dbg("Setting DHCP range start IP to: \"%s\"", buff); + u32 ip = ipaddr_addr(buff); + if (ip != 0) { + 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_range_start,"); + } + } + + if (GET_ARG("ap_dhcp_range_end")) { + dbg("Setting DHCP range end IP to: \"%s\"", buff); + u32 ip = ipaddr_addr(buff); + if (ip != 0) { + 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_range_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) { + 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) { + // 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 SSID (to connect to) ---- + + if (GET_ARG("sta_ssid")) { + // 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")) { + // 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; + } + + // ---- Station enable/disable DHCP ---- + // DHCP enable / disable (disable means static IP is enabled) - len = httpdFindArg(connData->getArgs, "sta_dhcp", buff, sizeof(buff)); - if (len > 0) { - dbg("DHCP enable = %s", buff); + if (GET_ARG("sta_dhcp_enable")) { + dbg("DHCP enable = %s", buff); int enable = atoi(buff); - if (enable != 0) { - wifi_station_dhcpc_stop(); + wificonf->sta_dhcp_enable = (enable != 0); + 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) { + wificonf->sta_addr.ip.addr = ip; + wifi_change_flags.sta = true; } else { - wifi_station_dhcpc_start(); + warn("Bad IP: %s", buff); + redir_url += sprintf(redir_url, "sta_addr_ip,"); } - // TODO persistency - } - - // Static IP - len = httpdFindArg(connData->getArgs, "sta_ip", buff, sizeof(buff)); - len2 = httpdFindArg(connData->getArgs, "sta_mask", buff2, sizeof(buff2)); - len3 = httpdFindArg(connData->getArgs, "sta_gw", buff3, sizeof(buff3)); - if (len > 0) { - // TODO set only if we're in the right opmode - // TODO persistency - dbg("Setting static IP = %s", buff); - struct ip_info ipinfo; - ipinfo.ip.addr = ipaddr_addr(buff); - ipinfo.netmask.addr = IPADDR_NONE; - ipinfo.gw.addr = IPADDR_NONE; - if (len2 > 0) { - dbg("Netmask = %s", buff2); - ipinfo.netmask.addr = ipaddr_addr(buff2); + } + + 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) { + 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 (len3 > 0) { - dbg("Gateway = %s", buff3); - ipinfo.gw.addr = ipaddr_addr(buff3); + } + + 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) { + 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,"); } - // TODO ... - wifi_station_dhcpc_stop(); - wifi_set_ip_info(STATION_IF, &ipinfo); } - httpdRedirect(connData, "/wifi"); + if (redir_url_buf[10] == 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, "/wifi"); + } 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; } @@ -578,77 +683,104 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiSetParams(HttpdConnData *connData) //Template code for the WLAN page. httpd_cgi_state ICACHE_FLASH_ATTR tplWlan(HttpdConnData *connData, char *token, void **arg) { - char buff[500]; + char buff[100]; int x; int connectStatus; - static struct station_config stconf; - static struct softap_config apconf; if (token == NULL) { // We're done return HTTPD_CGI_DONE; } - wifi_station_get_config(&stconf); - wifi_softap_get_config(&apconf); + strcpy(buff, ""); // fallback - - strcpy(buff, "Unknown"); - if (streq(token, "WiFiMode")) { - x = wifi_get_opmode(); - strcpy(buff, opmode2str(x)); + if (streq(token, "opmode_name")) { + strcpy(buff, opmode2str(wificonf->opmode)); } - else if (streq(token, "WiFiModeNum")) { - x = wifi_get_opmode(); - sprintf(buff, "%d", x); + else if (streq(token, "opmode")) { + sprintf(buff, "%d", wificonf->opmode); } - else if (streq(token, "WiFiChannel")) { - sprintf(buff, "%d", apconf.channel); + else if (streq(token, "tpw")) { + sprintf(buff, "%d", wificonf->tpw); } - else if (streq(token, "APName")) { - sprintf(buff, "%s", apconf.ssid); + else if (streq(token, "ap_channel")) { + sprintf(buff, "%d", wificonf->ap_channel); } - else if (streq(token, "StaIP")) { - x = wifi_get_opmode(); + 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, "ap_dhcp_time")) { + sprintf(buff, "%d", wificonf->ap_dhcp_time); + } + else if (streq(token, "ap_dhcp_range_start")) { + sprintf(buff, IPSTR, GOOD_IP2STR(wificonf->ap_dhcp_range.start_ip.addr)); + } + else if (streq(token, "ap_dhcp_range_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_ssid")) { + sprintf(buff, "%s", wificonf->sta_ssid); + } + else if (streq(token, "sta_password")) { + sprintf(buff, "%s", wificonf->sta_password); + } + 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, "ap_addr_mask")) { + sprintf(buff, IPSTR, GOOD_IP2STR(wificonf->sta_addr.netmask.addr)); + } + else if (streq(token, "ap_addr_gw")) { + sprintf(buff, IPSTR, GOOD_IP2STR(wificonf->sta_addr.gw.addr)); + } + 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 ip_info info; - wifi_get_ip_info(STATION_IF, &info); - sprintf(buff, IPSTR, GOOD_IP2STR(info.ip.addr)); + struct station_config staconf; + wifi_station_get_config(&staconf); + strcpy(buff, (char *) staconf.ssid); } } - else if (streq(token, "StaSSID")) { - connectStatus = wifi_station_get_connect_status(); + 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 { - strcpy(buff, (char *) stconf.ssid); - } - } - else if (streq(token, "WiFiPasswd")) { - strcpy(buff, (char *) stconf.password); - } - else if (streq(token, "WiFiapwarn")) { - // TODO get rid of this - x = wifi_get_opmode(); - if (x == SOFTAP_MODE) { // 2 - strcpy(buff, "Enable client for scanning."); - } - else if (x == STATIONAP_MODE) { // 3 - strcpy(buff, - "Switch: Client only, AP only"); - } - else { // 1 - strcpy(buff, - "Switch: Client+AP, AP only"); + struct ip_info info; + wifi_get_ip_info(STATION_IF, &info); + 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/persist.c b/user/persist.c index 7fc2c9e..c7ec05c 100644 --- a/user/persist.c +++ b/user/persist.c @@ -11,28 +11,10 @@ PersistBlock persist; #define PERSIST_SECTOR_ID 0x3D -//region Persist and restore individual modules - -/** - * Load persistent settings to live config structs - */ -static void ICACHE_FLASH_ATTR -load_settings_to_live(void) -{ - dbg("[Persist] Loading current settings to modules..."); - memcpy(wificonf, &persist.current.wificonf, sizeof(WiFiConfigBundle)); - memcpy(termconf, &persist.current.termconf, sizeof(TerminalConfigBundle)); - // ... -} +// This is used to force-erase the config area (when it's changed) +#define CHECKSUM_SALT 0x02 -static void ICACHE_FLASH_ATTR -store_all_settings_from_live(void) -{ - dbg("[Persist] Collecting live settings to persist block..."); - memcpy(&persist.current.wificonf, wificonf, sizeof(WiFiConfigBundle)); - memcpy(&persist.current.termconf, termconf, sizeof(TerminalConfigBundle)); - // ... -} +//region Persist and restore individual modules static void ICACHE_FLASH_ATTR apply_live_settings(void) @@ -88,7 +70,7 @@ calculateCRC32(const uint8_t *data, size_t length) static uint32_t ICACHE_FLASH_ATTR compute_checksum(AppConfigBundle *bundle) { - return calculateCRC32((uint8_t *) bundle, sizeof(AppConfigBundle) - 4); + return calculateCRC32((uint8_t *) bundle, sizeof(AppConfigBundle) - 4) ^ CHECKSUM_SALT; } /** @@ -108,15 +90,16 @@ persist_load(void) if (hard_reset || (compute_checksum(&persist.defaults) != persist.defaults.checksum) || (compute_checksum(&persist.current) != persist.current.checksum)) { - error("[Persist] Config block failed to load, restoring to hard defaults."); + dbg("[Persist] Checksum verification: FAILED"); hard_reset = true; + } else { + dbg("[Persist] Checksum verification: PASSED"); } if (hard_reset) { persist_restore_hard_default(); // this also stores them to flash and applies to modues } else { - load_settings_to_live(); apply_live_settings(); } @@ -127,7 +110,6 @@ void ICACHE_FLASH_ATTR persist_store(void) { info("[Persist] Storing all settings to FLASH..."); - store_all_settings_from_live(); // Update checksums before write persist.current.checksum = compute_checksum(&persist.current); @@ -150,9 +132,6 @@ persist_restore_hard_default(void) // Set live config to default values restore_live_settings_to_hard_defaults(); - // Store live -> current - store_all_settings_from_live(); - // Store current -> default memcpy(&persist.defaults, &persist.current, sizeof(AppConfigBundle)); persist_store(); @@ -170,7 +149,6 @@ persist_restore_default(void) { info("[Persist] Restoring live settings to stored defaults..."); memcpy(&persist.current, &persist.defaults, sizeof(AppConfigBundle)); - load_settings_to_live(); apply_live_settings(); info("[Persist] Settings restored to stored defaults."); } @@ -183,7 +161,6 @@ persist_set_as_default(void) { info("[Persist] Storing live settings as defaults.."); - store_all_settings_from_live(); memcpy(&persist.defaults, &persist.current, sizeof(AppConfigBundle)); persist_store(); diff --git a/user/persist.h b/user/persist.h index 676c1e6..d9bc487 100644 --- a/user/persist.h +++ b/user/persist.h @@ -24,7 +24,7 @@ typedef struct { typedef struct { AppConfigBundle defaults; // defaults are stored here - AppConfigBundle current; // settings persisted by user + AppConfigBundle current; // active settings adjusted by the user } PersistBlock; // Persist holds the data currently loaded from the flash diff --git a/user/wifimgr.c b/user/wifimgr.c index 02f170f..3e9bc82 100644 --- a/user/wifimgr.c +++ b/user/wifimgr.c @@ -6,6 +6,7 @@ #include "persist.h" WiFiConfigBundle * const wificonf = &persist.current.wificonf; +WiFiConfChangeFlags wifi_change_flags; /** * Restore defaults in the WiFi config block. @@ -25,24 +26,23 @@ wifimgr_restore_defaults(void) wificonf->ap_password[0] = 0; // PSK2 always if password is not null. wificonf->ap_hidden = false; - IP4_ADDR(&wificonf->ap_ip.ip, 192, 168, 4, 60); - IP4_ADDR(&wificonf->ap_ip.netmask, 255, 255, 255, 0); - wificonf->ap_ip.gw.addr = wificonf->ap_ip.gw.addr; + 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; - wificonf->ap_dhcp_lease_time = 120; + 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; - strcpy((char *) wificonf->sta_hostname, (char *) wificonf->ap_ssid); // use the same value for sta_hostname as AP name wificonf->sta_dhcp_enable = true; - IP4_ADDR(&wificonf->sta_ip.ip, 192, 168, 0, (mac[5]==1?2:mac[5])); // avoid being the same as "default gw" - IP4_ADDR(&wificonf->sta_ip.netmask, 255, 255, 255, 0); - IP4_ADDR(&wificonf->sta_ip.gw, 192, 168, 0, 1); + 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); } static void ICACHE_FLASH_ATTR @@ -57,25 +57,23 @@ configure_station(void) conf.bssid[0] = 0; wifi_station_disconnect(); wifi_station_set_config_current(&conf); - dbg("[WiFi] Hostname = %s", wificonf->sta_hostname); - wifi_station_set_hostname((char*)wificonf->sta_hostname); if (wificonf->sta_dhcp_enable) { dbg("[WiFi] Starting DHCP..."); if (!wifi_station_dhcpc_start()) { - error("[WiFi] DHCp failed to start!"); + error("[WiFi] DHCP failed to start!"); return; } } else { info("[WiFi] Setting up static IP..."); - dbg("[WiFi] Client.ip = "IPSTR, GOOD_IP2STR(wificonf->sta_ip.ip.addr)); - dbg("[WiFi] Client.mask = "IPSTR, GOOD_IP2STR(wificonf->sta_ip.netmask.addr)); - dbg("[WiFi] Client.gw = "IPSTR, GOOD_IP2STR(wificonf->sta_ip.gw.addr)); + 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_ip)) { + if (!wifi_set_ip_info(STATION_IF, &wificonf->sta_addr)) { error("[WiFi] Error setting static IP!"); return; } @@ -113,14 +111,14 @@ configure_ap(void) // Set IP info("[WiFi] Configuring SoftAP local IP..."); - dbg("[WiFi] SoftAP.ip = "IPSTR, GOOD_IP2STR(wificonf->ap_ip.ip.addr)); - dbg("[WiFi] SoftAP.mask = "IPSTR, GOOD_IP2STR(wificonf->ap_ip.netmask.addr)); - dbg("[WiFi] SoftAP.gw = "IPSTR, GOOD_IP2STR(wificonf->ap_ip.gw.addr)); + 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_ip)) { + if (!wifi_set_ip_info(SOFTAP_IF, &wificonf->ap_addr)) { error("[WiFi] IP set fail!"); return; } @@ -128,14 +126,14 @@ configure_ap(void) 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_lease_time); + 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_lease_time)) { + if (!wifi_softap_set_dhcps_lease_time(wificonf->ap_dhcp_time)) { error("[WiFi] DHCP lease time set fail!"); return; } @@ -161,23 +159,34 @@ wifimgr_apply_settings(void) // Force wifi cycle // Disconnect - may not be needed? WIFI_MODE opmode = wifi_get_opmode(); - if (opmode == STATIONAP_MODE || opmode == STATION_MODE) { - wifi_station_disconnect(); + + 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; } - // This should hopefully deinit everything - wifi_set_opmode_current(NULL_MODE); - wifi_set_opmode_current(wificonf->opmode); + if (opmode != wificonf->opmode) { + wifi_set_opmode_current(wificonf->opmode); + } // Configure the client - if (wificonf->opmode == STATIONAP_MODE || wificonf->opmode == STATION_MODE) { + if (is_sta && wifi_change_flags.sta) { configure_station(); } // Configure the AP - if (wificonf->opmode == STATIONAP_MODE || wificonf->opmode == SOFTAP_MODE) { + 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 index fa616d7..79a4246 100644 --- a/user/wifimgr.h +++ b/user/wifimgr.h @@ -10,6 +10,9 @@ #include #include "cgi_wifi.h" +#define SSID_LEN 32 +#define PASSWORD_LEN 64 + /** * A structure holding all configured WiFi parameters * and the active state. @@ -22,23 +25,29 @@ typedef struct { // --- AP config --- u8 ap_channel; - u8 ap_ssid[32]; - u8 ap_password[32]; + u8 ap_ssid[SSID_LEN]; + u8 ap_password[PASSWORD_LEN]; bool ap_hidden; - u16 ap_dhcp_lease_time; // in minutes + u16 ap_dhcp_time; // in minutes struct dhcps_lease ap_dhcp_range; - struct ip_info ap_ip; + struct ip_info ap_addr; // --- Client config --- - u8 sta_ssid[32]; - u8 sta_password[64]; - u8 sta_hostname[32]; // hostname set via the API. This does not seem to have much effect. + u8 sta_ssid[SSID_LEN]; + u8 sta_password[PASSWORD_LEN]; bool sta_dhcp_enable; - struct ip_info sta_ip; + 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); From 856a694f0b1cfbbf30378bd04547ddfc185edc3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sun, 16 Jul 2017 20:37:31 +0200 Subject: [PATCH 06/19] almost complete wifi page redesign --- html_orig/.gitignore | 2 +- html_orig/_debug_replacements.php | 29 + html_orig/{_test_env.php.example => _env.php} | 0 html_orig/_env.php.example | 3 + html_orig/_pages.php | 25 + html_orig/_start.php | 144 ---- html_orig/css/app.css | 664 +++++++++++++----- html_orig/index.php | 64 ++ html_orig/js/app.js | 27 +- html_orig/jssrc/chibi.js | 5 +- html_orig/jssrc/utils.js | 12 + html_orig/jssrc/wifi.js | 2 +- html_orig/messages/en.php | 35 + html_orig/pages/_cfg_menu.php | 15 + html_orig/pages/_head.php | 26 + html_orig/pages/_tail.php | 5 + html_orig/pages/cfg_network.php | 0 html_orig/pages/cfg_wifi.php | 123 ++++ html_orig/pages/term.php | 37 + html_orig/sass/_layout.scss | 178 ----- html_orig/sass/app.scss | 19 +- html_orig/sass/form/_form_elements.scss | 41 ++ html_orig/sass/form/_form_layout.scss | 55 +- html_orig/sass/form/_index.scss | 2 +- html_orig/sass/layout/_base.scss | 31 + html_orig/sass/layout/_box.scss | 91 +++ html_orig/sass/layout/_content.scss | 45 ++ .../sass/layout/_espterm_specific_old.scss | 76 ++ html_orig/sass/layout/_index.scss | 9 + html_orig/sass/layout/_loader.scss | 18 + html_orig/sass/layout/_menu.scss | 98 +++ html_orig/sass/{ => layout}/_modal.scss | 6 +- html_orig/sass/layout/_outer-wrap.scss | 22 + html_orig/sass/pages/_term.scss | 20 +- html_orig/sass/pages/_wifi.scss | 125 +++- html_orig/sass/utils/_background-tiling.scss | 68 ++ html_orig/sass/utils/_index.scss | 3 + html_orig/sass/utils/_misc.scss | 34 + html_orig/sass/utils/_pointer.scss | 26 + html_orig/server.sh | 2 +- user/cgi_wifi.c | 154 ++-- 41 files changed, 1691 insertions(+), 650 deletions(-) create mode 100644 html_orig/_debug_replacements.php rename html_orig/{_test_env.php.example => _env.php} (100%) create mode 100644 html_orig/_env.php.example create mode 100644 html_orig/_pages.php delete mode 100644 html_orig/_start.php create mode 100644 html_orig/index.php create mode 100644 html_orig/messages/en.php create mode 100644 html_orig/pages/_cfg_menu.php create mode 100644 html_orig/pages/_head.php create mode 100644 html_orig/pages/_tail.php create mode 100644 html_orig/pages/cfg_network.php create mode 100644 html_orig/pages/cfg_wifi.php create mode 100644 html_orig/pages/term.php delete mode 100644 html_orig/sass/_layout.scss create mode 100755 html_orig/sass/layout/_base.scss create mode 100755 html_orig/sass/layout/_box.scss create mode 100755 html_orig/sass/layout/_content.scss create mode 100644 html_orig/sass/layout/_espterm_specific_old.scss create mode 100755 html_orig/sass/layout/_index.scss create mode 100644 html_orig/sass/layout/_loader.scss create mode 100755 html_orig/sass/layout/_menu.scss rename html_orig/sass/{ => layout}/_modal.scss (92%) create mode 100755 html_orig/sass/layout/_outer-wrap.scss create mode 100755 html_orig/sass/utils/_background-tiling.scss create mode 100755 html_orig/sass/utils/_index.scss create mode 100755 html_orig/sass/utils/_misc.scss create mode 100755 html_orig/sass/utils/_pointer.scss 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..8b59dca --- /dev/null +++ b/html_orig/_debug_replacements.php @@ -0,0 +1,29 @@ + 'ESP8266 Wireless Terminal', + + '%b1%' => '1', + '%b2%' => '2', + '%b3%' => '3', + '%b4%' => '4', + '%b5%' => '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%' => 'LibraryFreeWifi', + '%sta_password%' => 'windows XP is The Best', + '%sta_active_ip%' => '', + + '%sta_enable%' => '0', +]; 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 @@ + $bc, + 'path' => $path, + 'label' => tr("menu.$key"), + ]; +} + +pg('cfg_wifi', 'cfg', '/cfg/wifi'); +pg('cfg_network', 'cfg', '/cfg/network'); +pg('cfg_term', 'cfg', '/cfg/term'); +pg('about', 'cfg', '/about'); +pg('help', 'cfg', '/help'); +pg('term', 'term', '/'); + +// technical +pg('wifi_set', '', '/cfg/wifi/set'); + +return $pages; diff --git a/html_orig/_start.php b/html_orig/_start.php deleted file mode 100644 index 31bab8a..0000000 --- a/html_orig/_start.php +++ /dev/null @@ -1,144 +0,0 @@ - [ $prod ? '/status' : '/page_status.php', 'Home' ], - 'wifi' => [ $prod ? '/wifi' : '/page_wifi.php', 'WiFi config' ], - 'about' => [ $prod ? '/about' : '/page_about.php', 'About' ], -]; - -$appname = 'Current Analyser'; - -function e($s) { - return htmlspecialchars($s, ENT_HTML5|ENT_QUOTES); -} - -?> - - - - - - - <?= e($menu[$page][1]) ?> - <?= e($appname) ?> - - - - - - -
- -
- Loading… - - - - - - - - - - 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/css/app.css b/html_orig/css/app.css index d058ead..9075115 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. @@ -313,84 +314,6 @@ html { *, *::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 +337,131 @@ 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; } + +/* 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::before { + content: "▸"; + padding-right: .5rem; + position: relative; + top: -0.1rem; } + @media screen and (max-width: 544px) { + #menu a { + display: none; } } + #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; } @media screen and (max-width: 544px) { - h1 { - font-size: 1.42383em; + #content h1 { + font-size: 1.80203em; 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 h2 { + font-size: 1.42383em; + margin-bottom: 0.61805rem; } + #content td, #content th { + padding: 0.38198rem; } + #content tbody th { + text-align: right; + width: 160px; + color: white; } #loader { position: absolute; @@ -496,20 +476,122 @@ 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); } + @media screen and (max-width: 544px) { + .Box { + margin-top: 0.61805rem; } } + h1 + .Box { + margin-top: 0; } + .Box h2 { + margin-top: 0; } + .Box.wide { + width: initial; + max-width: initial; } + .Box.medium { + max-width: 1200px; } + .Box.str { + position: relative; } + .Box.str .Row.buttons { + position: absolute; + right: 1rem; + top: 2.7em; + margin: 12px auto; } + @media screen and (min-width: 545px) { + .Box.str .Row.buttons { + right: 0; + top: 0; } } -#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; + margin-bottom: 0 !important; } + .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.expanded .Row { + display: flex; } } +.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; } + +.NotifyMsg { + position: fixed; + bottom: 2.61792rem; + padding: 0.61805rem 1rem; + left: 50%; + -webkit-transform: translate(-50%, 0); + -moz-transform: translate(-50%, 0); + -ms-transform: translate(-50%, 0); + -o-transform: translate(-50%, 0); + transform: translate(-50%, 0); + -webkit-font-smoothing: subpixel-antialiased; + -webkit-transform: translateZ(0) scale(1, 1); + background: #37a349; + 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; @@ -583,8 +665,8 @@ button, input[type=submit], .button { 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; @@ -592,13 +674,154 @@ input[type="number"], input[type="password"], input[type="text"], textarea, sele -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 { + 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"] { + width: 125px; } + +form .Row { + vertical-align: middle; + margin: 12px auto; + text-align: left; + display: flex; + flex-direction: row; + align-items: center; } + form .Row:first-child { + margin-top: 0; } + form .Row:last-child { + margin-bottom: 0; } + form .Row .spacer { + width: 160px; } + @media screen and (max-width: 544px) { + form .Row .spacer { + display: none; } } + form .Row.buttons { + margin: 16px auto; } + form .Row.buttons input, form .Row.buttons .button { + margin-right: 0.61805rem; } + form .Row.centered { + justify-content: center; } + form .Row.message { + font-size: 1em; + text-shadow: 1px 1px 3px black; + text-align: center; } + form .Row.message.error { + color: crimson; } + form .Row.message.ok { + color: #0fe851; } + form .Row.separator { + padding-top: 14px; + border-top: 2px solid rgba(255, 255, 255, 0.1); } + form .Row textarea { + display: inline-block; + vertical-align: top; + min-height: 10rem; + flex-grow: 1; + resize: vertical; } + form .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; } + form .Row input[type="range"] { + width: 200px; } + @media screen and (max-width: 544px) { + form .Row { + flex-direction: column; } + form .Row.buttons, form .Row.centered, form .Row.checkbox { + flex-direction: row; } + form .Row.buttons { + justify-content: center; } + form .Row.buttons :last-child { + margin-right: 0; } + form .Row label { + padding-left: 0; + text-align: left; + width: auto; } + form .Row .checkbox-wrap { + order: 1; + text-align: left; + padding-bottom: 0; + border-radius: .4px; + width: auto; } + form .Row .checkbox-wrap + label { + width: auto; } + form .Row input[type="number"], form .Row input[type="password"], form .Row input[type="text"], form .Row textarea, form .Row input[type="range"], form .Row textarea { + 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; @@ -629,6 +852,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 +894,55 @@ 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 .wrap { + flex-direction: row; + background: #eee !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 { + padding-bottom: 0; } + .AP-preview .wrap .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,18 +954,18 @@ 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%; @@ -711,6 +973,16 @@ input[type="number"], input[type="password"], input[type="text"], textarea, sele 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; } @@ -866,3 +1138,5 @@ input[type="number"], input[type="password"], input[type="text"], textarea, sele @media screen and (max-width: 1000px) { .mq-normal-min { display: none; } } + +/*# sourceMappingURL=app.css.map */ diff --git a/html_orig/index.php b/html_orig/index.php new file mode 100644 index 0000000..8402ea6 --- /dev/null +++ b/html_orig/index.php @@ -0,0 +1,64 @@ +label . ' :: ' . APP_NAME); +define('BODYCLASS', $_pages[CUR_PAGE]->bodyclass); + +/** URL (dev or production) */ +function url($name, $root=false) { + global $_pages; + if ($root) return LIVE_ROOT . $_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 $_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; +} + +require 'pages/_head.php'; +$_pf = 'pages/'.CUR_PAGE.'.php'; +if (file_exists($_pf)) { + $f = file_get_contents($_pf); + $reps = require('_debug_replacements.php'); + $str = str_replace(array_keys($reps), array_values($reps), $f); + include_str($str); +} else { + echo "404"; +} +require 'pages/_tail.php'; diff --git a/html_orig/js/app.js b/html_orig/js/app.js index 7aed10d..055c41f 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 @@ -795,6 +796,18 @@ String.prototype.format = function () { return out; }; +function e(str) { + return String(str) + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(//g, '>'); +} + +function undef(x) { + return typeof x == 'undefined'; +} /** Module for toggling a modal overlay */ (function () { var modal = {}; @@ -1088,7 +1101,7 @@ $._loader = function(vis) { console.warn("SOCKET CLOSED, code "+evt.code+". Reconnecting..."); setTimeout(function() { init(); - }, 2000); + }, 1000); } function onMessage(evt) { @@ -1211,7 +1224,7 @@ $._loader = function(vis) { // clear the AP list var $list = $('#ap-list'); // remove old APs - $('.AP').remove(); + $('#ap-list .AP').remove(); $list.toggle(done); $('#ap-loader').toggle(!done); @@ -1309,9 +1322,9 @@ $._loader = function(vis) { } $('#modeswitch').html([ - 'Client+AP AP only', - 'Client+AP', - 'Client only AP only' + 'Client+AP AP only', + 'Client+AP', + 'Client only AP only' ][obj.mode-1]); }; 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/utils.js b/html_orig/jssrc/utils.js index 4a7fd0b..9bec8c7 100755 --- a/html_orig/jssrc/utils.js +++ b/html_orig/jssrc/utils.js @@ -93,3 +93,15 @@ String.prototype.format = function () { return out; }; +function e(str) { + return String(str) + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(//g, '>'); +} + +function undef(x) { + return typeof x == 'undefined'; +} diff --git a/html_orig/jssrc/wifi.js b/html_orig/jssrc/wifi.js index 9fcf4c0..27f9924 100644 --- a/html_orig/jssrc/wifi.js +++ b/html_orig/jssrc/wifi.js @@ -20,7 +20,7 @@ // clear the AP list var $list = $('#ap-list'); // remove old APs - $('.AP').remove(); + $('#ap-list .AP').remove(); $list.toggle(done); $('#ap-loader').toggle(!done); diff --git a/html_orig/messages/en.php b/html_orig/messages/en.php new file mode 100644 index 0000000..90560f4 --- /dev/null +++ b/html_orig/messages/en.php @@ -0,0 +1,35 @@ + 'ESPTerm', + + 'menu.cfg_wifi' => 'WiFi Settings', + 'menu.cfg_network' => 'Network Configuration', + 'menu.cfg_term' => 'Terminal Settings', + 'menu.about' => 'About ESPTerm', + 'menu.help' => 'Help', + 'menu.term' => 'Back to Terminal', + + 'box.ap' => 'Built-in Access Point', + 'box.sta' => 'Client Mode', + + '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 Network:', + + 'wifi.sta_ssid' => 'Network SSID:', + 'wifi.sta_password' => 'Password:', + 'wifi.not_conn' => 'Not connected.', + 'wifi.forget' => '', + + 'wifi.submit' => 'Apply!', + + 'enabled' => 'Enabled', + 'disabled' => 'Disabled', + 'yes' => 'Yes', + 'no' => 'No', +]; diff --git a/html_orig/pages/_cfg_menu.php b/html_orig/pages/_cfg_menu.php new file mode 100644 index 0000000..69b7fe7 --- /dev/null +++ b/html_orig/pages/_cfg_menu.php @@ -0,0 +1,15 @@ + + diff --git a/html_orig/pages/_head.php b/html_orig/pages/_head.php new file mode 100644 index 0000000..f1b3c2e --- /dev/null +++ b/html_orig/pages/_head.php @@ -0,0 +1,26 @@ + + + + + + + <?= PAGE_TITLE ?> + + + + + +
+bodyclass == 'cfg') { + $cfg = true; + require __DIR__ . '/_cfg_menu.php'; +} +?> + +
+Loading… + +

+ diff --git a/html_orig/pages/_tail.php b/html_orig/pages/_tail.php new file mode 100644 index 0000000..560c6a1 --- /dev/null +++ b/html_orig/pages/_tail.php @@ -0,0 +1,5 @@ +
+ +
+ + diff --git a/html_orig/pages/cfg_network.php b/html_orig/pages/cfg_network.php new file mode 100644 index 0000000..e69de29 diff --git a/html_orig/pages/cfg_wifi.php b/html_orig/pages/cfg_wifi.php new file mode 100644 index 0000000..151ec2f --- /dev/null +++ b/html_orig/pages/cfg_wifi.php @@ -0,0 +1,123 @@ +
+

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

+ +
+ + +
+ + + + +
+ +
+
+
+
+
+
+
+ × +
+
+
+ +
+ +
+
+ + diff --git a/html_orig/pages/term.php b/html_orig/pages/term.php new file mode 100644 index 0000000..9c6619c --- /dev/null +++ b/html_orig/pages/term.php @@ -0,0 +1,37 @@ + + +

%term_title%

+ +
+
+ +
+ +
+
+ + + + 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..b36f320 100755 --- a/html_orig/sass/app.scss +++ b/html_orig/sass/app.scss @@ -6,15 +6,15 @@ @import "utils"; -$form-label-w: 130px; +$form-label-w: 160px; $form-label-gap: 8px; $form-field-w: 250px; $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 +24,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 diff --git a/html_orig/sass/form/_form_elements.scss b/html_orig/sass/form/_form_elements.scss index cbc8ef2..eec1261 100755 --- a/html_orig/sass/form/_form_elements.scss +++ b/html_orig/sass/form/_form_elements.scss @@ -16,6 +16,47 @@ } } +.Row.checkbox { + $h: 27px; + line-height: $h; + + .box { + 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..c6aa39d 100755 --- a/html_orig/sass/form/_form_layout.scss +++ b/html_orig/sass/form/_form_layout.scss @@ -5,13 +5,18 @@ form { @include naked(); } width: $form-field-w; } +input[type="number"] { + width: $form-field-w/2; +} + form .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 +35,7 @@ form .Row { } &.buttons { + margin: 16px auto; input, .button { margin-right: dist(-1); } @@ -80,34 +86,39 @@ 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 { - 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; - &.buttons, &.centered { + &.buttons, &.centered, &.checkbox { flex-direction: row; } @@ -139,7 +150,7 @@ form .Row { } } - #{$all-text-inputs}, textarea { + #{$all-text-inputs}, input[type="range"], textarea { 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..4527f14 --- /dev/null +++ b/html_orig/sass/layout/_base.scss @@ -0,0 +1,31 @@ +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; +} diff --git a/html_orig/sass/layout/_box.scss b/html_orig/sass/layout/_box.scss new file mode 100755 index 0000000..1aa3381 --- /dev/null +++ b/html_orig/sass/layout/_box.scss @@ -0,0 +1,91 @@ +.Box { + display: block; + max-width: 900px; + + margin-top: dist(0); + padding: dist(-1) dist(0); + + @include media($phone) { + margin-top: dist(-1); + } + + h1 + & { + margin-top: 0; + } + + h2 { + 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; + //} + + &.str { + position: relative; + + .Row.buttons { + position: absolute; + right: dist(0); + top: 2.7em; + margin: 12px auto; + } + + @include media($tablet-min) { + .Row.buttons { + //position: absolute; + right: 0; + top: 0; + //margin: 12px auto; + } + } + } +} + +@include media($phone) { + .Box.mobcol { + h2 { + position: relative; + cursor: pointer; + + &::after { + position: absolute; + right: 0; + content: '▸'; + + top:50%; + font-size: 120%; + font-weight: bold; + transform: translate(0,-50%) rotate(90deg); + } + + margin-bottom: 0 !important; + } + + &.expanded h2::after { + transform: translate(-25%,-50%) rotate(-90deg); + margin-bottom: dist(0); + } + + .Row { + display: none; + } + + &.expanded .Row { + display: flex; + } + } +} diff --git a/html_orig/sass/layout/_content.scss b/html_orig/sass/layout/_content.scss new file mode 100755 index 0000000..f283350 --- /dev/null +++ b/html_orig/sass/layout/_content.scss @@ -0,0 +1,45 @@ +#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); + } + + @include media($phone) { + h1 { + font-size: fsize(5); + margin-bottom: dist(-1); + } + } + + h2 { + font-size: fsize(3); + 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/_espterm_specific_old.scss b/html_orig/sass/layout/_espterm_specific_old.scss new file mode 100644 index 0000000..f2263bd --- /dev/null +++ b/html_orig/sass/layout/_espterm_specific_old.scss @@ -0,0 +1,76 @@ + +ul > * { + padding-top: .1em; + padding-bottom: .1em; +} + +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; + } + } +} + +body { + position: relative; + + padding: dist(0); + @include media($phone) { + padding: dist(-1); + } + + overflow-y: auto; + + & > * { + margin-left: auto; + margin-right: auto; + } +} 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..766a6ac --- /dev/null +++ b/html_orig/sass/layout/_menu.scss @@ -0,0 +1,98 @@ +#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); + } + + &::before { + content: "▸"; + padding-right: .5rem; + position: relative; + top: -0.1rem; + } + + @include media($phone) { + display: none; + } + } + + &.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 92% rename from html_orig/sass/_modal.scss rename to html_orig/sass/layout/_modal.scss index c0c72fb..781d833 100755 --- a/html_orig/sass/_modal.scss +++ b/html_orig/sass/layout/_modal.scss @@ -25,8 +25,8 @@ //min-height: 15rem; background: #1c1c1e; - border-left: 6px solid $c-form-highlight-a; - border-right: 6px solid $c-form-highlight-a; + border-left: 6px solid $c-form-highlight; + border-right: 6px solid $c-form-highlight; box-shadow: 0 0 2px 0 #434349, 0 0 6px 0 black; border-radius: 6px; @@ -40,7 +40,6 @@ } } -/* // "toast" .NotifyMsg { position: fixed; @@ -75,4 +74,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/_term.scss b/html_orig/sass/pages/_term.scss index d642e6d..34b332c 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) { @@ -42,6 +42,24 @@ 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..9a392e0 100755 --- a/html_orig/sass/pages/_wifi.scss +++ b/html_orig/sass/pages/_wifi.scss @@ -43,6 +43,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 +100,70 @@ 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 { + .wrap { + @extend %ap-inner; - background: #afafaf; - transition: background-color 0.5s; - &:hover { background: white } + flex-direction: row; + background: #eee !important; // override the hover effect #43de81 + cursor: default; + top: 0 !important; // no click effect + overflow: hidden; - display: flex; + .inner { + display: flex; + flex-direction: column; - & > * { - padding: dist(-1); - @include nowrap; + & > * { + 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 { + padding-bottom: 0; } - .auth { - flex: 0 0 15%; + .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/user/cgi_wifi.c b/user/cgi_wifi.c index 6347325..2ec3306 100644 --- a/user/cgi_wifi.c +++ b/user/cgi_wifi.c @@ -444,14 +444,38 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiSetParams(HttpdConnData *connData) } } + 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 - wificonf->tpw = (u8) tpw; - wifi_change_flags.ap = true; + 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,"); @@ -464,8 +488,10 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiSetParams(HttpdConnData *connData) info("ap_channel = %s", buff); int channel = atoi(buff); if (channel > 0 && channel < 15) { - wificonf->ap_channel = (u8) channel; - wifi_change_flags.ap = true; + 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,"); @@ -485,9 +511,11 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiSetParams(HttpdConnData *connData) buff[i] = 0; if (strlen(buff) > 0) { - info("Setting SSID to \"%s\"", buff); - strncpy_safe(wificonf->ap_ssid, buff, SSID_LEN); - wifi_change_flags.ap = true; + 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,"); @@ -500,9 +528,11 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiSetParams(HttpdConnData *connData) // 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)) { - info("Setting AP password to \"%s\"", buff); - strncpy_safe(wificonf->ap_password, buff, PASSWORD_LEN); - wifi_change_flags.ap = true; + 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,"); @@ -514,8 +544,10 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiSetParams(HttpdConnData *connData) if (GET_ARG("ap_hidden")) { dbg("AP hidden = %s", buff); int hidden = atoi(buff); - wificonf->ap_hidden = (hidden != 0); - wifi_change_flags.ap = true; + if (hidden != wificonf->ap_hidden) { + wificonf->ap_hidden = (hidden != 0); + wifi_change_flags.ap = true; + } } // ---- AP DHCP server lease time ---- @@ -524,8 +556,10 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiSetParams(HttpdConnData *connData) dbg("Setting DHCP lease time to: %s min.", buff); int min = atoi(buff); if (min >= 1 && min <= 2880) { - wificonf->ap_dhcp_time = (u16) min; - wifi_change_flags.ap = true; + 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,"); @@ -534,27 +568,31 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiSetParams(HttpdConnData *connData) // ---- AP DHCP start and end IP ---- - if (GET_ARG("ap_dhcp_range_start")) { + if (GET_ARG("ap_dhcp_start")) { dbg("Setting DHCP range start IP to: \"%s\"", buff); u32 ip = ipaddr_addr(buff); if (ip != 0) { - wificonf->ap_dhcp_range.start_ip.addr = ip; - wifi_change_flags.ap = true; + 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_range_start,"); + redir_url += sprintf(redir_url, "ap_dhcp_start,"); } } - if (GET_ARG("ap_dhcp_range_end")) { + if (GET_ARG("ap_dhcp_end")) { dbg("Setting DHCP range end IP to: \"%s\"", buff); u32 ip = ipaddr_addr(buff); if (ip != 0) { - wificonf->ap_dhcp_range.end_ip.addr = ip; - wifi_change_flags.ap = true; + 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_range_end,"); + redir_url += sprintf(redir_url, "ap_dhcp_end,"); } } @@ -564,9 +602,11 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiSetParams(HttpdConnData *connData) dbg("Setting AP local IP to: \"%s\"", buff); u32 ip = ipaddr_addr(buff); if (ip != 0) { - 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; + 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,"); @@ -577,10 +617,12 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiSetParams(HttpdConnData *connData) dbg("Setting AP local IP netmask to: \"%s\"", buff); u32 ip = ipaddr_addr(buff); if (ip != 0) { - // 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; + 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,"); @@ -590,19 +632,23 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiSetParams(HttpdConnData *connData) // ---- Station SSID (to connect to) ---- if (GET_ARG("sta_ssid")) { - // 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; + 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")) { - // 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 (!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; + } } // ---- Station enable/disable DHCP ---- @@ -611,8 +657,10 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiSetParams(HttpdConnData *connData) if (GET_ARG("sta_dhcp_enable")) { dbg("DHCP enable = %s", buff); int enable = atoi(buff); - wificonf->sta_dhcp_enable = (enable != 0); - wifi_change_flags.sta = true; + if (wificonf->sta_dhcp_enable != enable) { + wificonf->sta_dhcp_enable = (bool)enable; + wifi_change_flags.sta = true; + } } // ---- Station IP config (Static IP) ---- @@ -621,8 +669,10 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiSetParams(HttpdConnData *connData) dbg("Setting Station mode static IP to: \"%s\"", buff); u32 ip = ipaddr_addr(buff); if (ip != 0) { - wificonf->sta_addr.ip.addr = ip; - wifi_change_flags.sta = true; + 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,"); @@ -633,8 +683,10 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiSetParams(HttpdConnData *connData) dbg("Setting Station mode static IP netmask to: \"%s\"", buff); u32 ip = ipaddr_addr(buff); if (ip != 0 && ip != 0xFFFFFFFFUL) { - wificonf->sta_addr.netmask.addr = ip; - wifi_change_flags.sta = true; + 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,"); @@ -645,8 +697,10 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiSetParams(HttpdConnData *connData) dbg("Setting Station mode static IP default gateway to: \"%s\"", buff); u32 ip = ipaddr_addr(buff); if (ip != 0) { - wificonf->sta_addr.gw.addr = ip; - wifi_change_flags.sta = true; + 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,"); @@ -700,6 +754,12 @@ httpd_cgi_state ICACHE_FLASH_ATTR tplWlan(HttpdConnData *connData, char *token, 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); } @@ -718,10 +778,10 @@ httpd_cgi_state ICACHE_FLASH_ATTR tplWlan(HttpdConnData *connData, char *token, else if (streq(token, "ap_dhcp_time")) { sprintf(buff, "%d", wificonf->ap_dhcp_time); } - else if (streq(token, "ap_dhcp_range_start")) { + 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_range_end")) { + 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")) { From 357a9b6513c1da25f1d0d344e9167ac0dd110212 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sun, 16 Jul 2017 21:07:52 +0200 Subject: [PATCH 07/19] css for Box folding, impl network forget --- html_orig/css/app.css | 40 ++++++++++++++++++----------- html_orig/messages/en.php | 4 +-- html_orig/pages/cfg_wifi.php | 29 ++++++++++++++++++--- html_orig/sass/app.scss | 8 +++--- html_orig/sass/layout/_box.scss | 20 ++++++--------- html_orig/sass/layout/_content.scss | 13 +++++++--- html_orig/sass/pages/_wifi.scss | 7 +++++ 7 files changed, 80 insertions(+), 41 deletions(-) diff --git a/html_orig/css/app.css b/html_orig/css/app.css index 9075115..0e2390b 100644 --- a/html_orig/css/app.css +++ b/html_orig/css/app.css @@ -449,13 +449,16 @@ a:hover { 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 h2 { - font-size: 1.42383em; - margin-bottom: 0.61805rem; } #content td, #content th { padding: 0.38198rem; } #content tbody th { @@ -497,20 +500,21 @@ a:hover { max-width: 1200px; } .Box.str { position: relative; } - .Box.str .Row.buttons { + .Box.str .Row.mq-phone { position: absolute; right: 1rem; - top: 2.7em; - margin: 12px auto; } - @media screen and (min-width: 545px) { - .Box.str .Row.buttons { - right: 0; - top: 0; } } + margin: 1rem auto; } + .Box.str .Row.mq-no-phone { + position: absolute; + right: 0; + top: 0; + margin-top: 0.61805rem; } @media screen and (max-width: 544px) { .Box.mobcol h2 { position: relative; cursor: pointer; + padding-right: 1.3rem; margin-bottom: 0 !important; } .Box.mobcol h2::after { position: absolute; @@ -899,6 +903,12 @@ form span.required { white-space: nowrap; word-wrap: normal; } +.AP-preview-nil { + padding: 0.38198rem 0.61805rem; + background: rgba(0, 0, 0, 0.1); + border-radius: 3px; + border: 2px dashed black; } + .AP-preview .wrap { flex-direction: row; background: #eee !important; @@ -1128,15 +1138,15 @@ body.term #botnav { @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/messages/en.php b/html_orig/messages/en.php index 90560f4..1fb7650 100644 --- a/html_orig/messages/en.php +++ b/html_orig/messages/en.php @@ -11,7 +11,7 @@ return [ 'menu.term' => 'Back to Terminal', 'box.ap' => 'Built-in Access Point', - 'box.sta' => 'Client Mode', + 'box.sta' => 'Connect to External Network', 'wifi.enable' => 'Enabled:', 'wifi.tpw' => 'Transmit Power:', @@ -24,7 +24,7 @@ return [ 'wifi.sta_ssid' => 'Network SSID:', 'wifi.sta_password' => 'Password:', 'wifi.not_conn' => 'Not connected.', - 'wifi.forget' => '', + 'wifi.sta_none' => 'None', 'wifi.submit' => 'Apply!', diff --git a/html_orig/pages/cfg_wifi.php b/html_orig/pages/cfg_wifi.php index 151ec2f..5e7f7c9 100644 --- a/html_orig/pages/cfg_wifi.php +++ b/html_orig/pages/cfg_wifi.php @@ -1,6 +1,10 @@

+
+ +
+
@@ -28,7 +32,7 @@ - +
@@ -37,7 +41,7 @@
-
+
@@ -45,6 +49,10 @@

+
+ +
+
@@ -56,7 +64,7 @@
-
+ -
+
@@ -111,7 +122,17 @@ }); }); + $('#forget-sta').on('click', function() { + $('#sta_ssid').val(''); + $('#sta_password').val(''); + + wifiShowSelected('', '', ''); + }); + function wifiShowSelected(name, password, ip) { + $('#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 .passwd').html(e(password)).toggleClass('hidden', nopw); diff --git a/html_orig/sass/app.scss b/html_orig/sass/app.scss index b36f320..325b25f 100755 --- a/html_orig/sass/app.scss +++ b/html_orig/sass/app.scss @@ -36,17 +36,17 @@ $c-form-highlight-a: #2ea1f9; // 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/layout/_box.scss b/html_orig/sass/layout/_box.scss index 1aa3381..f14448c 100755 --- a/html_orig/sass/layout/_box.scss +++ b/html_orig/sass/layout/_box.scss @@ -36,21 +36,16 @@ &.str { position: relative; - - .Row.buttons { + .Row.mq-phone { position: absolute; right: dist(0); - top: 2.7em; - margin: 12px auto; + margin: 1rem auto; } - - @include media($tablet-min) { - .Row.buttons { - //position: absolute; - right: 0; - top: 0; - //margin: 12px auto; - } + .Row.mq-no-phone { + position: absolute; + right: 0; + top: 0; + margin-top: dist(-1); } } } @@ -60,6 +55,7 @@ h2 { position: relative; cursor: pointer; + padding-right: 1.3rem; &::after { position: absolute; diff --git a/html_orig/sass/layout/_content.scss b/html_orig/sass/layout/_content.scss index f283350..b1d6b96 100755 --- a/html_orig/sass/layout/_content.scss +++ b/html_orig/sass/layout/_content.scss @@ -21,16 +21,21 @@ 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(3); - margin-bottom: dist(-1); + h2 { + font-size: fsize(2); + margin-bottom: dist(-1); + } } td, th { diff --git a/html_orig/sass/pages/_wifi.scss b/html_orig/sass/pages/_wifi.scss index 9a392e0..6e46a6a 100755 --- a/html_orig/sass/pages/_wifi.scss +++ b/html_orig/sass/pages/_wifi.scss @@ -110,6 +110,13 @@ } } +.AP-preview-nil { + padding: dist(-2) dist(-1); + background: rgba(black, .1); + border-radius: 3px; + border: 2px dashed black; +} + .AP-preview { .wrap { @extend %ap-inner; From 232e8802e25318cbfcf992928e121650ddaa6df9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sun, 16 Jul 2017 22:34:22 +0200 Subject: [PATCH 08/19] wifi scan --- html_orig/_debug_replacements.php | 8 +- html_orig/_pages.php | 3 +- html_orig/css/app.css | 18 ++- html_orig/index.php | 2 +- html_orig/messages/en.php | 7 +- html_orig/pages/cfg_wifi.php | 186 +++++++++++++++++++++++++++--- html_orig/sass/layout/_box.scss | 14 ++- html_orig/sass/pages/_wifi.scss | 14 ++- 8 files changed, 218 insertions(+), 34 deletions(-) diff --git a/html_orig/_debug_replacements.php b/html_orig/_debug_replacements.php index 8b59dca..d9c7dbd 100644 --- a/html_orig/_debug_replacements.php +++ b/html_orig/_debug_replacements.php @@ -21,9 +21,11 @@ return [ '%ap_ssid%' => 'ESP-123456', '%ap_password%' => 'Passw0rd!', '%ap_hidden%' => '0', - '%sta_ssid%' => 'LibraryFreeWifi', + '%sta_ssid%' => 'Chlivek', '%sta_password%' => 'windows XP is The Best', - '%sta_active_ip%' => '', + '%sta_active_ip%' => '1.2.3.4', + '%sta_active_ssid%' => 'Chlivek', - '%sta_enable%' => '0', + '%sta_enable%' => '1', + '%opmode%' => '3', ]; diff --git a/html_orig/_pages.php b/html_orig/_pages.php index e15e2c7..37e0ee7 100644 --- a/html_orig/_pages.php +++ b/html_orig/_pages.php @@ -20,6 +20,7 @@ pg('help', 'cfg', '/help'); pg('term', 'term', '/'); // technical -pg('wifi_set', '', '/cfg/wifi/set'); +pg('wifi_set', '', '/wifi/set');//'/cfg/wifi/set'); +pg('wifi_scan', '', '/wifi/scan');//'/cfg/wifi/scan'); return $pages; diff --git a/html_orig/css/app.css b/html_orig/css/app.css index 0e2390b..1e206f9 100644 --- a/html_orig/css/app.css +++ b/html_orig/css/app.css @@ -529,8 +529,12 @@ a:hover { margin-bottom: 1rem; } .Box.mobcol .Row { display: none; } + .Box.mobcol #ap-box { + display: none; } .Box.mobcol.expanded .Row { - display: flex; } } + display: flex; } + .Box.mobcol.expanded #ap-box { + display: block; } } .Modal { position: fixed; width: 100%; @@ -847,7 +851,12 @@ form span.required { 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; @@ -905,13 +914,12 @@ form span.required { .AP-preview-nil { padding: 0.38198rem 0.61805rem; - background: rgba(0, 0, 0, 0.1); border-radius: 3px; border: 2px dashed black; } .AP-preview .wrap { flex-direction: row; - background: #eee !important; + background: #ddd !important; cursor: default; top: 0 !important; overflow: hidden; } @@ -944,7 +952,7 @@ form span.required { padding-top: calc(0.61805rem + 1px); } .AP-preview .wrap .essid, .AP-preview .wrap .passwd { padding-bottom: 0; } - .AP-preview .wrap .passwd { + .AP-preview .wrap .x-passwd { font-family: monospace; } body.term h1 { diff --git a/html_orig/index.php b/html_orig/index.php index 8402ea6..ddb1499 100644 --- a/html_orig/index.php +++ b/html_orig/index.php @@ -19,7 +19,7 @@ define('BODYCLASS', $_pages[CUR_PAGE]->bodyclass); /** URL (dev or production) */ function url($name, $root=false) { global $_pages; - if ($root) return LIVE_ROOT . $_pages[$name]->path; + if ($root) return $_pages[$name]->path; if (DEBUG) return "/index.php?page=$name"; else return $_pages[$name]->path; diff --git a/html_orig/messages/en.php b/html_orig/messages/en.php index 1fb7650..1fdd2b3 100644 --- a/html_orig/messages/en.php +++ b/html_orig/messages/en.php @@ -19,17 +19,22 @@ return [ 'wifi.ap_ssid' => 'AP SSID:', 'wifi.ap_password' => 'Password:', 'wifi.ap_hidden' => 'Hide SSID:', - 'wifi.sta_info' => 'Selected Network:', + 'wifi.sta_info' => 'Selected:', 'wifi.sta_ssid' => 'Network SSID:', 'wifi.sta_password' => 'Password:', 'wifi.not_conn' => 'Not connected.', 'wifi.sta_none' => 'None', + 'wifi.sta_active_pw' => '🔒', 'wifi.submit' => 'Apply!', + 'wifi.scanning' => 'Scanning', + 'wifi.cant_scan_no_sta' => 'Can\'t scan with Client mode disabled.', + 'wifi.select_ssid' => 'Available networks:', 'enabled' => 'Enabled', 'disabled' => 'Disabled', 'yes' => 'Yes', 'no' => 'No', + 'confirm' => 'OK', ]; diff --git a/html_orig/pages/cfg_wifi.php b/html_orig/pages/cfg_wifi.php index 5e7f7c9..4c6613c 100644 --- a/html_orig/pages/cfg_wifi.php +++ b/html_orig/pages/cfg_wifi.php @@ -5,6 +5,10 @@
+
+ +
+
@@ -40,10 +44,6 @@ -->
- -
- -
@@ -53,14 +53,18 @@
+
+ +
+
- - + +
@@ -68,7 +72,7 @@
-
+
 
× @@ -79,12 +83,27 @@
-
- +
+ +
.
+ +
+ + diff --git a/html_orig/sass/layout/_box.scss b/html_orig/sass/layout/_box.scss index f14448c..60e1cbc 100755 --- a/html_orig/sass/layout/_box.scss +++ b/html_orig/sass/layout/_box.scss @@ -80,8 +80,18 @@ display: none; } - &.expanded .Row { - display: flex; + #ap-box { + display: none; + } + + &.expanded { + .Row { + display: flex; + } + + #ap-box { + display: block; + } } } } diff --git a/html_orig/sass/pages/_wifi.scss b/html_orig/sass/pages/_wifi.scss index 6e46a6a..8e97dde 100755 --- a/html_orig/sass/pages/_wifi.scss +++ b/html_orig/sass/pages/_wifi.scss @@ -25,7 +25,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 { @@ -112,7 +119,6 @@ .AP-preview-nil { padding: dist(-2) dist(-1); - background: rgba(black, .1); border-radius: 3px; border: 2px dashed black; } @@ -122,7 +128,7 @@ @extend %ap-inner; flex-direction: row; - background: #eee !important; // override the hover effect #43de81 + background: #ddd !important; // override the hover effect #43de81 cursor: default; top: 0 !important; // no click effect overflow: hidden; @@ -169,7 +175,7 @@ padding-bottom: 0; } - .passwd { + .x-passwd { font-family: monospace; } } From 21b304de9ff441884471a34bc042e6ddd1752697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sun, 16 Jul 2017 22:55:45 +0200 Subject: [PATCH 09/19] bump lib version + fix wifipage url --- html_orig/pages/term.php | 2 +- libesphttpd | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/html_orig/pages/term.php b/html_orig/pages/term.php index 9c6619c..afa77b4 100644 --- a/html_orig/pages/term.php +++ b/html_orig/pages/term.php @@ -22,7 +22,7 @@
diff --git a/libesphttpd b/libesphttpd index 38c6c91..daa039f 160000 --- a/libesphttpd +++ b/libesphttpd @@ -1 +1 @@ -Subproject commit 38c6c91f50e5a5cfba8df8309a95e814695accba +Subproject commit daa039fb14bc9816a27d10be9798fc5be01616f8 From f6621252f645e1de37da505cd9cc09bdee9aac74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sat, 22 Jul 2017 13:23:57 +0200 Subject: [PATCH 10/19] frontend improvements --- html_orig/_debug_replacements.php | 11 + html_orig/_pages.php | 8 +- html_orig/css/app.css | 4 + html_orig/index.php | 14 +- html_orig/js/app.js | 258 ++++-------------- html_orig/jssrc/appcommon.js | 41 ++- html_orig/jssrc/notif.js | 32 +++ html_orig/jssrc/term.js | 4 +- html_orig/jssrc/utils.js | 35 +-- html_orig/jssrc/wifi.js | 56 +--- html_orig/{messages => lang}/en.php | 12 +- html_orig/packjs.sh | 3 +- html_orig/pages/_cfg_menu.php | 3 +- html_orig/pages/_head.php | 4 +- html_orig/{about.html => pages/about.php} | 51 ++-- html_orig/pages/cfg_wifi.php | 27 +- html_orig/pages/cfg_wifi_conn.php | 53 ++++ html_orig/{help.html => pages/help.php} | 37 +-- html_orig/sass/layout/_base.scss | 5 + .../sass/layout/_espterm_specific_old.scss | 76 ------ html_orig/term.html | 53 ---- html_orig/term_test.php | 17 -- html_orig/wifi.html | 89 ------ html_orig/wifi_conn.html | 26 -- html_orig/wifi_test.php | 15 - 25 files changed, 271 insertions(+), 663 deletions(-) create mode 100644 html_orig/jssrc/notif.js rename html_orig/{messages => lang}/en.php (67%) rename html_orig/{about.html => pages/about.php} (51%) create mode 100755 html_orig/pages/cfg_wifi_conn.php rename html_orig/{help.html => pages/help.php} (86%) delete mode 100644 html_orig/sass/layout/_espterm_specific_old.scss delete mode 100644 html_orig/term.html delete mode 100644 html_orig/term_test.php delete mode 100644 html_orig/wifi.html delete mode 100755 html_orig/wifi_conn.html delete mode 100644 html_orig/wifi_test.php diff --git a/html_orig/_debug_replacements.php b/html_orig/_debug_replacements.php index d9c7dbd..dbd22a9 100644 --- a/html_orig/_debug_replacements.php +++ b/html_orig/_debug_replacements.php @@ -1,5 +1,10 @@ 'ESP8266 Wireless Terminal', @@ -28,4 +33,10 @@ return [ '%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', ]; diff --git a/html_orig/_pages.php b/html_orig/_pages.php index 37e0ee7..d72a90e 100644 --- a/html_orig/_pages.php +++ b/html_orig/_pages.php @@ -13,14 +13,16 @@ function pg($key, $bc, $path) { } pg('cfg_wifi', 'cfg', '/cfg/wifi'); +pg('cfg_wifi_conn', '', '/wifi/connecting'); // page without menu that tries to show the connection progress pg('cfg_network', 'cfg', '/cfg/network'); pg('cfg_term', 'cfg', '/cfg/term'); -pg('about', 'cfg', '/about'); -pg('help', 'cfg', '/help'); +pg('about', 'cfg page-about', '/about'); +pg('help', 'cfg page-help', '/help'); pg('term', 'term', '/'); -// technical +// ajax API pg('wifi_set', '', '/wifi/set');//'/cfg/wifi/set'); pg('wifi_scan', '', '/wifi/scan');//'/cfg/wifi/scan'); +pg('wifi_connstatus', '', '/wifi/connstatus'); return $pages; diff --git a/html_orig/css/app.css b/html_orig/css/app.css index 1e206f9..a326c3e 100644 --- a/html_orig/css/app.css +++ b/html_orig/css/app.css @@ -343,6 +343,10 @@ a:hover { [onclick] { cursor: pointer; } +ul > * { + padding-top: .2em; + padding-bottom: .2em; } + /* Main outer container */ #outer { display: flex; diff --git a/html_orig/index.php b/html_orig/index.php index ddb1499..c779c8d 100644 --- a/html_orig/index.php +++ b/html_orig/index.php @@ -1,15 +1,19 @@ _env.php.example to _env.php and check the settings inside!"); +} require '_env.php'; + $prod = defined('STDIN'); define ('DEBUG', !$prod); -$root = DEBUG ? ESP_IP : ''; -define ('LIVE_ROOT', $root); +$root = DEBUG ? json_encode(ESP_IP) : 'window.location.href'; +define ('JS_WEB_ROOT', $root); define('CUR_PAGE', $_GET['page'] ?: 'term'); define('LOCALE', $_GET['locale'] ?: 'en'); -$_messages = require(__DIR__ . '/messages/' . LOCALE . '.php'); +$_messages = require(__DIR__ . '/lang/' . LOCALE . '.php'); $_pages = require('_pages.php'); define('APP_NAME', 'ESPTerm'); @@ -17,9 +21,9 @@ define('PAGE_TITLE', $_pages[CUR_PAGE]->label . ' :: ' . APP_NAME); define('BODYCLASS', $_pages[CUR_PAGE]->bodyclass); /** URL (dev or production) */ -function url($name, $root=false) { +function url($name, $relative=false) { global $_pages; - if ($root) return $_pages[$name]->path; + if ($relative) return $_pages[$name]->path; if (DEBUG) return "/index.php?page=$name"; else return $_pages[$name]->path; diff --git a/html_orig/js/app.js b/html_orig/js/app.js index 055c41f..fc1db8c 100644 --- a/html_orig/js/app.js +++ b/html_orig/js/app.js @@ -705,20 +705,16 @@ 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); -} - /** Extend an objects with options */ function extend(defaults, options) { var target = {}; @@ -739,23 +735,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; }; @@ -796,18 +792,25 @@ String.prototype.format = function () { return out; }; +/** HTML escape */ function e(str) { - return String(str) - .replace(/&/g, '&') - .replace(/"/g, '"') - .replace(/'/g, ''') - .replace(//g, '>'); + 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 = {}; @@ -851,6 +854,27 @@ function undef(x) { })(); /** 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); + + $(x).on('click', function() { + inp.value = 1 - inp.value; + $(box).toggleClass('checked', inp.value) + }); + }); + + // Expanding boxes on mobile + $('.Box.mobcol').forEach(function(x) { + var h = x.querySelector('h2'); + $(h).on('click', function() { + $(x).toggleClass('expanded'); + }); + }); + // loader dots... setInterval(function () { $('.anim-dots').each(function (x) { @@ -863,12 +887,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 { @@ -877,14 +902,14 @@ $.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(); @@ -894,10 +919,7 @@ $.ready(function () { }); $._loader = function(vis) { - if(vis) - $('#loader').addClass('show'); - else - $('#loader').removeClass('show'); + $('#loader').toggleClass('show', vis); }; (function() { /** @@ -1036,7 +1058,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); @@ -1182,7 +1204,7 @@ $._loader = function(vis) { } }); - qa('#buttons button').forEach(function(s) { + qsa('#buttons button').forEach(function(s) { s.addEventListener('click', function() { sendBtnMsg(+this.dataset['n']); }); @@ -1202,181 +1224,3 @@ $._loader = function(vis) { 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-list .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..a40faa6 100644 --- a/html_orig/jssrc/appcommon.js +++ b/html_orig/jssrc/appcommon.js @@ -1,5 +1,26 @@ /** 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); + + $(x).on('click', function() { + inp.value = 1 - inp.value; + $(box).toggleClass('checked', inp.value) + }); + }); + + // Expanding boxes on mobile + $('.Box.mobcol').forEach(function(x) { + var h = x.querySelector('h2'); + $(h).on('click', function() { + $(x).toggleClass('expanded'); + }); + }); + // loader dots... setInterval(function () { $('.anim-dots').each(function (x) { @@ -12,12 +33,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,14 +48,14 @@ $.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(); @@ -43,8 +65,5 @@ $.ready(function () { }); $._loader = function(vis) { - if(vis) - $('#loader').addClass('show'); - else - $('#loader').removeClass('show'); + $('#loader').toggleClass('show', vis); }; 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..0965105 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']); }); diff --git a/html_orig/jssrc/utils.js b/html_orig/jssrc/utils.js index 9bec8c7..bf6d114 100755 --- a/html_orig/jssrc/utils.js +++ b/html_orig/jssrc/utils.js @@ -2,20 +2,16 @@ 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); -} - /** Extend an objects with options */ function extend(defaults, options) { var target = {}; @@ -36,23 +32,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,15 +89,22 @@ String.prototype.format = function () { return out; }; +/** HTML escape */ function e(str) { - return String(str) - .replace(/&/g, '&') - .replace(/"/g, '"') - .replace(/'/g, ''') - .replace(//g, '>'); + 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 27f9924..5e442dd 100644 --- a/html_orig/jssrc/wifi.js +++ b/html_orig/jssrc/wifi.js @@ -11,9 +11,9 @@ return; } - resp = JSON.parse(resp); + resp = jsp(resp); - var done = !bool(resp.result.inProgress) && (resp.result.APs.length > 0); + var done = resp && !bool(resp.result.inProgress) && (resp.result.APs.length > 0); rescan(done ? 15000 : 1000); if (!done) return; // no redraw yet @@ -29,7 +29,7 @@ resp.result.APs.sort(function (a, b) { return b.rssi - a.rssi; }).forEach(function (ap) { - ap.enc = intval(ap.enc); + ap.enc = parseInt(ap.enc); if (ap.enc > 4) return; // hide unsupported auths @@ -124,55 +124,5 @@ ][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/messages/en.php b/html_orig/lang/en.php similarity index 67% rename from html_orig/messages/en.php rename to html_orig/lang/en.php index 1fdd2b3..64c16ff 100644 --- a/html_orig/messages/en.php +++ b/html_orig/lang/en.php @@ -7,8 +7,9 @@ return [ 'menu.cfg_network' => 'Network Configuration', 'menu.cfg_term' => 'Terminal Settings', 'menu.about' => 'About ESPTerm', - 'menu.help' => 'Help', + 'menu.help' => 'Terminal Help', 'menu.term' => 'Back to Terminal', + 'menu.cfg_wifi_conn' => 'Connecting to External Network', 'box.ap' => 'Built-in Access Point', 'box.sta' => 'Connect to External Network', @@ -32,6 +33,15 @@ return [ '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.idle' =>"Preparing to connect", + 'wifi.conn.success' => "Connected! Received IP ", + 'wifi.conn.working' => "Connecting to selected AP", + 'wifi.conn.fail' => "Connection failed, check your password and try again.", + 'enabled' => 'Enabled', 'disabled' => 'Disabled', 'yes' => 'Yes', diff --git a/html_orig/packjs.sh b/html_orig/packjs.sh index 37c976c..53c9e69 100755 --- a/html_orig/packjs.sh +++ b/html_orig/packjs.sh @@ -6,5 +6,4 @@ cat jssrc/chibi.js \ jssrc/utils.js \ jssrc/modal.js \ jssrc/appcommon.js \ - jssrc/term.js \ - jssrc/wifi.js > js/app.js + jssrc/term.js > js/app.js diff --git a/html_orig/pages/_cfg_menu.php b/html_orig/pages/_cfg_menu.php index 69b7fe7..2a020ac 100644 --- a/html_orig/pages/_cfg_menu.php +++ b/html_orig/pages/_cfg_menu.php @@ -4,7 +4,8 @@ $page) { - if ($page->bodyclass !== 'cfg') continue; + if (strpos($page->bodyclass, 'cfg') === false) continue; + $sel = (CUR_PAGE == $k) ? ' class="selected"' : ''; $text = $page->label; $url = e(url($k)); diff --git a/html_orig/pages/_head.php b/html_orig/pages/_head.php index f1b3c2e..ff5b31d 100644 --- a/html_orig/pages/_head.php +++ b/html_orig/pages/_head.php @@ -7,13 +7,13 @@ <?= PAGE_TITLE ?> - +
bodyclass == 'cfg') { +if (strpos($_pages[CUR_PAGE]->bodyclass, 'cfg') !== false) { $cfg = true; require __DIR__ . '/_cfg_menu.php'; } diff --git a/html_orig/about.html b/html_orig/pages/about.php similarity index 51% rename from html_orig/about.html rename to html_orig/pages/about.php index d83f202..57a4c71 100644 --- a/html_orig/about.html +++ b/html_orig/pages/about.php @@ -1,34 +1,26 @@ - - - - - About - ESP8266 Remote Terminal - - - - - - - -

About

-

ESP8266 Remote Terminal

-

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

+

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

-

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

+

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

-

Firmware

+

Version

- - + + @@ -55,7 +47,14 @@

Contributing

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

+ +

+ You can donate on PayPal or + LiberaPay to help keep + the project going.

@@ -66,15 +65,7 @@ esphttpd library by Jeroen Domburg (Sprite_tm).

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

- - - - - diff --git a/html_orig/pages/cfg_wifi.php b/html_orig/pages/cfg_wifi.php index 4c6613c..e040fe9 100644 --- a/html_orig/pages/cfg_wifi.php +++ b/html_orig/pages/cfg_wifi.php @@ -1,4 +1,4 @@ - +

@@ -46,7 +46,7 @@
- +

@@ -103,27 +103,6 @@
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

    -
  • Communication UART on pins Rx, Tx at 115200-8-1-N
  • -
  • Debug log on pin GPIO2 at 115200-8-1-N
  • -
  • Use 3.3V logic, or 5V with protection resistors (470R or more)
  • -
  • If the "LVD" LED on the ESP Term board lights up, the module doesn't get enough power. Check your connections.
  • +
  • Communication UART is on pins Rx, Tx at 115200-8-1-N. The baud rate can be changed in Terminal Settings. +
  • Debug log is on pin GPIO2 (P2) at 115200-8-1-N. This baud rate is fixed. +
  • Compatible with 3.3 V and 5 V logic. For 5 V, 470 R protection resistors are recommended. +
  • If the "LVD" LED on the ESPTerm module lights up, it doesn't get enough power to run correctly. Check your connections.
  • Connect Rx and Tx with a piece of wire to test the terminal alone, you should see what you type in the browser. - NOTE: This won't work if your ESP8266 board has a built-in USB-serial (like NodeMCU).
  • -
  • For best performance, use the module in the Client mode. In AP mode, check that the channel used is clear; - interference may cause lag in the terminal.
  • + NOTE: This won't work if your ESP8266 board has a built-in USB-serial converter (like NodeMCU). +
  • For best performance, use the module in Client mode (connected to external network). +
  • In AP mode, check that the channel used is clear; interference may cause a flaky connection.
@@ -328,12 +314,3 @@
Firmwarev%vers_fw%, build %date% at %time%ESPTermv%vers_fw%, build %date% at %time%
libesphttpd
- - - - - diff --git a/html_orig/sass/layout/_base.scss b/html_orig/sass/layout/_base.scss index 4527f14..eda4c7e 100755 --- a/html_orig/sass/layout/_base.scss +++ b/html_orig/sass/layout/_base.scss @@ -29,3 +29,8 @@ a:hover { [onclick] { cursor: pointer; } + +ul > * { + padding-top: .2em; + padding-bottom: .2em; +} diff --git a/html_orig/sass/layout/_espterm_specific_old.scss b/html_orig/sass/layout/_espterm_specific_old.scss deleted file mode 100644 index f2263bd..0000000 --- a/html_orig/sass/layout/_espterm_specific_old.scss +++ /dev/null @@ -1,76 +0,0 @@ - -ul > * { - padding-top: .1em; - padding-bottom: .1em; -} - -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; - } - } -} - -body { - position: relative; - - padding: dist(0); - @include media($phone) { - padding: dist(-1); - } - - overflow-y: auto; - - & > * { - margin-left: auto; - margin-right: auto; - } -} 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 @@ - Date: Sat, 22 Jul 2017 13:56:14 +0200 Subject: [PATCH 11/19] scan on demand --- html_orig/css/app.css | 10 +++-- html_orig/lang/en.php | 1 + html_orig/pages/cfg_wifi.php | 20 ++++++--- html_orig/pages/cfg_wifi_conn.php | 74 +++++++++++++++---------------- html_orig/sass/pages/_wifi.scss | 10 +++-- 5 files changed, 63 insertions(+), 52 deletions(-) diff --git a/html_orig/css/app.css b/html_orig/css/app.css index a326c3e..43b5322 100644 --- a/html_orig/css/app.css +++ b/html_orig/css/app.css @@ -845,7 +845,7 @@ form span.required { #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; @@ -917,9 +917,11 @@ form span.required { word-wrap: normal; } .AP-preview-nil { - padding: 0.38198rem 0.61805rem; - border-radius: 3px; - border: 2px dashed black; } + padding: 8px; + border-radius: 5px; + border: 1px dashed #ddd; + width: 250px; + height: 94px; } .AP-preview .wrap { flex-direction: row; diff --git a/html_orig/lang/en.php b/html_orig/lang/en.php index 64c16ff..668e0ee 100644 --- a/html_orig/lang/en.php +++ b/html_orig/lang/en.php @@ -30,6 +30,7 @@ return [ 'wifi.submit' => 'Apply!', '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:', diff --git a/html_orig/pages/cfg_wifi.php b/html_orig/pages/cfg_wifi.php index e040fe9..6bed2f7 100644 --- a/html_orig/pages/cfg_wifi.php +++ b/html_orig/pages/cfg_wifi.php @@ -85,7 +85,8 @@
-
.
+
+
@@ -238,6 +239,13 @@ }); } + 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+'', onScan); @@ -263,12 +271,12 @@ obj.mode = +obj.mode; $('#ap-noscan').toggleClass('hidden', obj.mode != 2); - $('#ap-loader').toggleClass('hidden', obj.mode == 2); + $('#ap-scan').toggleClass('hidden', obj.mode == 2); - // scan if not AP - if (obj.mode != 2) { - scanAPs(); - } +// // scan if not AP +// if (obj.mode != 2) { +// scanAPs(); +// } } wifiInit({mode: '%opmode%'}); diff --git a/html_orig/pages/cfg_wifi_conn.php b/html_orig/pages/cfg_wifi_conn.php index bcbf1fd..e12cfa2 100755 --- a/html_orig/pages/cfg_wifi_conn.php +++ b/html_orig/pages/cfg_wifi_conn.php @@ -6,48 +6,46 @@
diff --git a/html_orig/sass/pages/_wifi.scss b/html_orig/sass/pages/_wifi.scss index 8e97dde..9bc4359 100755 --- a/html_orig/sass/pages/_wifi.scss +++ b/html_orig/sass/pages/_wifi.scss @@ -13,7 +13,7 @@ margin: 0 (- dist(-3)); } -#ap-loader, #ap-noscan { +#ap-loader, #ap-noscan, #ap-scan { background: rgba(white, .1); border-radius: 5px; padding: dist(-2); @@ -118,9 +118,11 @@ } .AP-preview-nil { - padding: dist(-2) dist(-1); - border-radius: 3px; - border: 2px dashed black; + padding: 8px; + border-radius: 5px; + border: 1px dashed #ddd; + width: 250px; + height: 94px; } .AP-preview { From 3f09cc3601d2f4e34821144c3d1ddd8918c579b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sat, 22 Jul 2017 14:16:12 +0200 Subject: [PATCH 12/19] wifi sta picker improvements and better transl --- html_orig/css/app.css | 22 ++++++++++++---------- html_orig/lang/en.php | 2 ++ html_orig/pages/cfg_wifi.php | 17 +++++++++++------ html_orig/sass/layout/_box.scss | 22 +++++++++++++--------- html_orig/sass/pages/_wifi.scss | 2 +- 5 files changed, 39 insertions(+), 26 deletions(-) diff --git a/html_orig/css/app.css b/html_orig/css/app.css index 43b5322..79949aa 100644 --- a/html_orig/css/app.css +++ b/html_orig/css/app.css @@ -504,15 +504,17 @@ ul > * { max-width: 1200px; } .Box.str { position: relative; } - .Box.str .Row.mq-phone { - position: absolute; - right: 1rem; - margin: 1rem auto; } - .Box.str .Row.mq-no-phone { - position: absolute; - right: 0; - top: 0; - margin-top: 0.61805rem; } + .Box.str .Row.buttons { + position: absolute; } + @media screen and (max-width: 544px) { + .Box.str .Row.buttons { + right: 1rem; + margin: 1rem auto; } } + @media screen and (min-width: 545px) { + .Box.str .Row.buttons { + right: 0; + top: 0; + margin-top: 0.61805rem; } } @media screen and (max-width: 544px) { .Box.mobcol h2 { @@ -956,7 +958,7 @@ form span.required { .AP-preview .wrap .forget:active { position: relative; padding-top: calc(0.61805rem + 1px); } - .AP-preview .wrap .essid, .AP-preview .wrap .passwd { + .AP-preview .wrap .essid, .AP-preview .wrap .passwd, .AP-preview .wrap .nopasswd { padding-bottom: 0; } .AP-preview .wrap .x-passwd { font-family: monospace; } diff --git a/html_orig/lang/en.php b/html_orig/lang/en.php index 668e0ee..608cbc7 100644 --- a/html_orig/lang/en.php +++ b/html_orig/lang/en.php @@ -27,6 +27,8 @@ return [ '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.submit' => 'Apply!', 'wifi.scanning' => 'Scanning', diff --git a/html_orig/pages/cfg_wifi.php b/html_orig/pages/cfg_wifi.php index 6bed2f7..f7370f8 100644 --- a/html_orig/pages/cfg_wifi.php +++ b/html_orig/pages/cfg_wifi.php @@ -73,6 +73,7 @@
 
+
× @@ -141,7 +142,8 @@ var nopw = undef(password) || password.length == 0; $('#sta-nw .x-passwd').html(e(password)); $('#sta-nw .passwd').toggleClass('hidden', nopw); - $('#sta-nw .ip').html(ip.length>0 ? 'IP = '+ip : ''); + $('#sta-nw .nopasswd').toggleClass('hidden', !nopw); + $('#sta-nw .ip').html(ip.length>0 ? ''+ip : ''); } selectSta('%sta_ssid%', '%sta_password%', '%sta_active_ip%'); @@ -149,12 +151,15 @@ var authStr = ['Open', 'WEP', 'WPA', 'WPA2', 'WPA/WPA2']; var curSSID = '%sta_active_ssid%'; - function submitPskModal(e) { + function submitPskModal(e, open) { var passwd = $('#conn-passwd').val(); var ssid = $('#conn-ssid').val(); - $('#sta_password').val(passwd); - $('#sta_ssid').val(ssid); - selectSta(ssid, passwd, ''); + + if (open || passwd.length) { + $('#sta_password').val(passwd); + $('#sta_ssid').val(ssid); + selectSta(ssid, passwd, ''); + } if (e) e.preventDefault(); Modal.hide('#psk-modal'); @@ -229,7 +234,7 @@ $('#conn-passwd')[0].focus(); } else { //Modal.show('#reset-modal'); - submitPskModal(); + submitPskModal(null, true); } }); diff --git a/html_orig/sass/layout/_box.scss b/html_orig/sass/layout/_box.scss index 60e1cbc..def421f 100755 --- a/html_orig/sass/layout/_box.scss +++ b/html_orig/sass/layout/_box.scss @@ -34,18 +34,22 @@ // min-width: 10em; //} + // Submit Top Right &.str { position: relative; - .Row.mq-phone { + .Row.buttons { position: absolute; - right: dist(0); - margin: 1rem auto; - } - .Row.mq-no-phone { - position: absolute; - right: 0; - top: 0; - margin-top: dist(-1); + + @include media($phone) { + right: dist(0); + margin: 1rem auto; + } + + @include media($tablet-min) { + right: 0; + top: 0; + margin-top: dist(-1); + } } } } diff --git a/html_orig/sass/pages/_wifi.scss b/html_orig/sass/pages/_wifi.scss index 9bc4359..b2d16e8 100755 --- a/html_orig/sass/pages/_wifi.scss +++ b/html_orig/sass/pages/_wifi.scss @@ -173,7 +173,7 @@ } } - .essid, .passwd { + .essid, .passwd, .nopasswd { padding-bottom: 0; } From b238390ca8cf17e59094717bb0440ddfe3572e35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sun, 23 Jul 2017 00:57:31 +0200 Subject: [PATCH 13/19] network settings page --- html_orig/_debug_replacements.php | 11 +++++ html_orig/_pages.php | 33 ++++++++------ html_orig/base.php | 71 +++++++++++++++++++++++++++++ html_orig/build_html.php | 19 ++++++++ html_orig/index.php | 65 ++++---------------------- html_orig/lang/en.php | 35 ++++++++++++-- html_orig/pages/_cfg_menu.php | 2 +- html_orig/pages/_head.php | 8 ++-- html_orig/pages/cfg_network.php | 76 +++++++++++++++++++++++++++++++ html_orig/pages/cfg_wifi.php | 20 +++----- html_orig/pages/term.php | 2 +- 11 files changed, 248 insertions(+), 94 deletions(-) create mode 100644 html_orig/base.php create mode 100644 html_orig/build_html.php diff --git a/html_orig/_debug_replacements.php b/html_orig/_debug_replacements.php index dbd22a9..3cde458 100644 --- a/html_orig/_debug_replacements.php +++ b/html_orig/_debug_replacements.php @@ -39,4 +39,15 @@ return [ '%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', ]; diff --git a/html_orig/_pages.php b/html_orig/_pages.php index d72a90e..30e66f5 100644 --- a/html_orig/_pages.php +++ b/html_orig/_pages.php @@ -2,27 +2,32 @@ $pages = []; -/** Add a page */ -function pg($key, $bc, $path) { - global $pages; - $pages[$key] = (object) [ - 'bodyclass' => $bc, - 'path' => $path, - 'label' => tr("menu.$key"), - ]; +if (! function_exists('pg')) { + /** Add a page */ + function pg($key, $bc, $path, $titleKey = null) + { + global $pages; + $pages[$key] = (object) [ + 'key' => $key, + 'bodyclass' => $bc, + 'path' => $path, + 'label' => tr("menu.$key"), + 'title' => $titleKey ? tr($titleKey) : tr("menu.$key"), + ]; + } } pg('cfg_wifi', 'cfg', '/cfg/wifi'); pg('cfg_wifi_conn', '', '/wifi/connecting'); // page without menu that tries to show the connection progress pg('cfg_network', 'cfg', '/cfg/network'); -pg('cfg_term', 'cfg', '/cfg/term'); -pg('about', 'cfg page-about', '/about'); +//pg('cfg_term', 'cfg', '/cfg/term'); pg('help', 'cfg page-help', '/help'); -pg('term', 'term', '/'); +pg('about', 'cfg page-about', '/about'); +pg('term', 'term', '/', 'title.term'); // ajax API -pg('wifi_set', '', '/wifi/set');//'/cfg/wifi/set'); -pg('wifi_scan', '', '/wifi/scan');//'/cfg/wifi/scan'); -pg('wifi_connstatus', '', '/wifi/connstatus'); +pg('wifi_set', 'api', '/wifi/set');//'/cfg/wifi/set'); +pg('wifi_scan', 'api', '/wifi/scan');//'/cfg/wifi/scan'); +pg('wifi_connstatus', 'api', '/wifi/connstatus'); return $pages; diff --git a/html_orig/base.php b/html_orig/base.php new file mode 100644 index 0000000..0b99593 --- /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) : 'window.location.href'; +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 100644 index 0000000..48be82a --- /dev/null +++ b/html_orig/build_html.php @@ -0,0 +1,19 @@ + $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 + 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/index.php b/html_orig/index.php index c779c8d..0a8836a 100644 --- a/html_orig/index.php +++ b/html_orig/index.php @@ -1,68 +1,21 @@ _env.php.example to _env.php and check the settings inside!"); -} -require '_env.php'; - -$prod = defined('STDIN'); -define ('DEBUG', !$prod); -$root = DEBUG ? json_encode(ESP_IP) : 'window.location.href'; -define ('JS_WEB_ROOT', $root); - -define('CUR_PAGE', $_GET['page'] ?: 'term'); -define('LOCALE', $_GET['locale'] ?: 'en'); - -$_messages = require(__DIR__ . '/lang/' . LOCALE . '.php'); -$_pages = require('_pages.php'); +require_once __DIR__ . '/base.php'; -define('APP_NAME', 'ESPTerm'); -define('PAGE_TITLE', $_pages[CUR_PAGE]->label . ' :: ' . APP_NAME); -define('BODYCLASS', $_pages[CUR_PAGE]->bodyclass); +if (!isset($_GET['page'])) $_GET['page'] = 'term'; -/** 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; -} +$_GET['PAGE_TITLE'] = $_pages[$_GET['page']]->title . ' :: ' . APP_NAME; +$_GET['BODYCLASS'] = $_pages[$_GET['page']]->bodyclass; -function e($s) { - return htmlspecialchars($s, ENT_HTML5|ENT_QUOTES); -} - -function tr($key) { - global $_messages; - return $_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; -} - -require 'pages/_head.php'; -$_pf = 'pages/'.CUR_PAGE.'.php'; +require __DIR__ . '/pages/_head.php'; +$_pf = __DIR__ . '/pages/'.$_GET['page'].'.php'; if (file_exists($_pf)) { $f = file_get_contents($_pf); - $reps = require('_debug_replacements.php'); + $reps = DEBUG ? require(__DIR__ . '/_debug_replacements.php') : []; $str = str_replace(array_keys($reps), array_values($reps), $f); include_str($str); } else { echo "404"; } -require 'pages/_tail.php'; + +require __DIR__ . '/pages/_tail.php'; diff --git a/html_orig/lang/en.php b/html_orig/lang/en.php index 608cbc7..08bfd23 100644 --- a/html_orig/lang/en.php +++ b/html_orig/lang/en.php @@ -7,12 +7,39 @@ return [ 'menu.cfg_network' => 'Network Configuration', 'menu.cfg_term' => 'Terminal Settings', 'menu.about' => 'About ESPTerm', - 'menu.help' => 'Terminal Help', + 'menu.help' => 'Quick Reference', 'menu.term' => 'Back to Terminal', 'menu.cfg_wifi_conn' => 'Connecting to External Network', - 'box.ap' => 'Built-in Access Point', - 'box.sta' => 'Connect to External Network', + 'title.term' => 'Terminal', + + 'net.ap' => 'Access Point DHCP Config', + 'net.sta' => 'Client IP Config', + + 'net.explain_sta' => ' + Those settings affect the built-in DHCP client. Switching it 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' => 'Enable DHCP', + '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:', @@ -30,7 +57,6 @@ return [ 'wifi.sta_active_nopw' => '🔓 Open access', 'wifi.connected_ip_is' => 'Connected, IP is ', - 'wifi.submit' => 'Apply!', 'wifi.scanning' => 'Scanning', 'wifi.scan_now' => 'Start scanning!', 'wifi.cant_scan_no_sta' => 'Can\'t scan with Client mode disabled.', @@ -45,6 +71,7 @@ return [ 'wifi.conn.working' => "Connecting to selected AP", 'wifi.conn.fail' => "Connection failed, check your password and try again.", + 'apply' => 'Apply!', 'enabled' => 'Enabled', 'disabled' => 'Disabled', 'yes' => 'Yes', diff --git a/html_orig/pages/_cfg_menu.php b/html_orig/pages/_cfg_menu.php index 2a020ac..569ae99 100644 --- a/html_orig/pages/_cfg_menu.php +++ b/html_orig/pages/_cfg_menu.php @@ -6,7 +6,7 @@ foreach($_pages as $k => $page) { if (strpos($page->bodyclass, 'cfg') === false) continue; - $sel = (CUR_PAGE == $k) ? ' class="selected"' : ''; + $sel = ($_GET['page'] == $k) ? ' class="selected"' : ''; $text = $page->label; $url = e(url($k)); echo "$text"; diff --git a/html_orig/pages/_head.php b/html_orig/pages/_head.php index ff5b31d..3134c1c 100644 --- a/html_orig/pages/_head.php +++ b/html_orig/pages/_head.php @@ -4,16 +4,16 @@ - <?= PAGE_TITLE ?> + <?= $_GET['PAGE_TITLE'] ?> - +
bodyclass, 'cfg') !== false) { +if (strpos($_GET['BODYCLASS'], 'cfg') !== false) { $cfg = true; require __DIR__ . '/_cfg_menu.php'; } @@ -22,5 +22,5 @@ if (strpos($_pages[CUR_PAGE]->bodyclass, 'cfg') !== false) {
Loading… -

+

diff --git a/html_orig/pages/cfg_network.php b/html_orig/pages/cfg_network.php index e69de29..cf7672b 100644 --- a/html_orig/pages/cfg_network.php +++ b/html_orig/pages/cfg_network.php @@ -0,0 +1,76 @@ + + +
+

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

+ +
+ +
+ +
+
+ +
+ +
+ + +
+ +
+ + required> +
+ +
+ + required> +
+ +
+ + required> +
+
diff --git a/html_orig/pages/cfg_wifi.php b/html_orig/pages/cfg_wifi.php index f7370f8..43fa995 100644 --- a/html_orig/pages/cfg_wifi.php +++ b/html_orig/pages/cfg_wifi.php @@ -1,12 +1,8 @@
-

+

-
- -
- -
- +
+
@@ -47,14 +43,10 @@
-

- -
- -
+

-
- +
+
diff --git a/html_orig/pages/term.php b/html_orig/pages/term.php index afa77b4..427efb5 100644 --- a/html_orig/pages/term.php +++ b/html_orig/pages/term.php @@ -7,7 +7,7 @@ }, 2000); -

%term_title%

+

%term_title%

From 3c202a1f5090189282aa56e844bf8e7f476e30e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sun, 23 Jul 2017 01:47:18 +0200 Subject: [PATCH 14/19] error indication via GET arg err=... --- CMakeLists.txt | 8 ++- html_orig/css/app.css | 19 +++--- html_orig/js/app.js | 51 ++++++++++++++ html_orig/jssrc/appcommon.js | 19 ++++++ html_orig/lang/en.php | 10 +-- html_orig/packjs.sh | 1 + html_orig/pages/_head.php | 5 ++ html_orig/pages/_tail.php | 4 ++ html_orig/sass/form/_form_layout.scss | 15 +++++ html_orig/sass/layout/_modal.scss | 9 +-- user/cgi_appcfg.c | 97 +++++++++++++++++++++++++++ user/cgi_appcfg.h | 9 +++ user/cgi_wifi.c | 12 ++-- 13 files changed, 235 insertions(+), 24 deletions(-) create mode 100644 user/cgi_appcfg.c create mode 100644 user/cgi_appcfg.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 7fa43d7..2701a1e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -93,6 +93,8 @@ set(SOURCE_FILES user/io.h user/cgi_wifi.c user/cgi_wifi.h + user/cgi_appcfg.c + user/cgi_appcfg.h user/cgi_ping.c user/cgi_reset.c user/uart_driver.c @@ -113,7 +115,11 @@ set(SOURCE_FILES user/cgi_sockets.h user/ansi_parser_callbacks.c user/ansi_parser_callbacks.h - user/user_main.h user/wifimgr.c user/wifimgr.h user/persist.c user/persist.h) + user/user_main.h + user/wifimgr.c + user/wifimgr.h + user/persist.c + user/persist.h) include_directories(include) include_directories(user) diff --git a/html_orig/css/app.css b/html_orig/css/app.css index 79949aa..f425e9a 100644 --- a/html_orig/css/app.css +++ b/html_orig/css/app.css @@ -579,17 +579,12 @@ ul > * { .NotifyMsg { position: fixed; - bottom: 2.61792rem; + top: 1.618rem; + right: 2.61792rem; padding: 0.61805rem 1rem; - left: 50%; - -webkit-transform: translate(-50%, 0); - -moz-transform: translate(-50%, 0); - -ms-transform: translate(-50%, 0); - -o-transform: translate(-50%, 0); - transform: translate(-50%, 0); -webkit-font-smoothing: subpixel-antialiased; -webkit-transform: translateZ(0) scale(1, 1); - background: #37a349; + background: #3887d0; color: white; text-shadow: 0 0 2px black; box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.6); @@ -740,6 +735,12 @@ input[type="number"], input[type="password"], input[type="text"], textarea, sele input[type="number"] { width: 125px; } +.Box.errors .list { + color: crimson; + font-weight: bold; } +.Box.errors .lead { + color: white; } + form .Row { vertical-align: middle; margin: 12px auto; @@ -794,6 +795,8 @@ form .Row { user-select: none; white-space: nowrap; word-wrap: normal; } + form .Row label.error { + color: crimson; } form .Row input[type="range"] { width: 200px; } @media screen and (max-width: 544px) { diff --git a/html_orig/js/app.js b/html_orig/js/app.js index fc1db8c..204e008 100644 --- a/html_orig/js/app.js +++ b/html_orig/js/app.js @@ -852,6 +852,38 @@ function jsp() { 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) @@ -915,7 +947,26 @@ $.ready(function () { 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(); }); $._loader = function(vis) { diff --git a/html_orig/jssrc/appcommon.js b/html_orig/jssrc/appcommon.js index a40faa6..ee4569c 100644 --- a/html_orig/jssrc/appcommon.js +++ b/html_orig/jssrc/appcommon.js @@ -61,7 +61,26 @@ $.ready(function () { 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(); }); $._loader = function(vis) { diff --git a/html_orig/lang/en.php b/html_orig/lang/en.php index 08bfd23..f866c36 100644 --- a/html_orig/lang/en.php +++ b/html_orig/lang/en.php @@ -13,11 +13,12 @@ return [ 'title.term' => 'Terminal', - 'net.ap' => 'Access Point DHCP Config', - 'net.sta' => 'Client IP Config', + 'net.ap' => 'DHCP Server', + 'net.sta' => 'DHCP Client', 'net.explain_sta' => ' - Those settings affect the built-in DHCP client. Switching it off + 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.', @@ -33,7 +34,7 @@ return [ 'net.ap_addr_ip' => 'Own IP address', 'net.ap_addr_mask' => 'Subnet mask', - 'net.sta_dhcp_enable' => 'Enable DHCP', + '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', @@ -77,4 +78,5 @@ return [ 'yes' => 'Yes', 'no' => 'No', 'confirm' => 'OK', + 'form_errors' => 'Validation errors for:', ]; diff --git a/html_orig/packjs.sh b/html_orig/packjs.sh index 53c9e69..f1652fd 100755 --- a/html_orig/packjs.sh +++ b/html_orig/packjs.sh @@ -5,5 +5,6 @@ echo "Packing js..." cat jssrc/chibi.js \ jssrc/utils.js \ jssrc/modal.js \ + jssrc/notif.js \ jssrc/appcommon.js \ jssrc/term.js > js/app.js diff --git a/html_orig/pages/_head.php b/html_orig/pages/_head.php index 3134c1c..a6d428d 100644 --- a/html_orig/pages/_head.php +++ b/html_orig/pages/_head.php @@ -23,4 +23,9 @@ if (strpos($_GET['BODYCLASS'], 'cfg') !== false) { Loading…

+ + + diff --git a/html_orig/pages/_tail.php b/html_orig/pages/_tail.php index 560c6a1..dfdca17 100644 --- a/html_orig/pages/_tail.php +++ b/html_orig/pages/_tail.php @@ -1,5 +1,9 @@ + + +
+ diff --git a/html_orig/sass/form/_form_layout.scss b/html_orig/sass/form/_form_layout.scss index c6aa39d..2f0d570 100755 --- a/html_orig/sass/form/_form_layout.scss +++ b/html_orig/sass/form/_form_layout.scss @@ -9,6 +9,17 @@ input[type="number"] { width: $form-field-w/2; } +.Box.errors { + .list { + color: crimson; + font-weight: bold; + } + + .lead { + color: white; + } +} + form .Row { vertical-align: middle; margin: 12px auto; @@ -89,6 +100,10 @@ form .Row { @include nowrap; } + label.error { + color: crimson; + } + //.checkbox-wrap { // display: inline-block; // width: $form-label-w; diff --git a/html_orig/sass/layout/_modal.scss b/html_orig/sass/layout/_modal.scss index 781d833..21bdab1 100755 --- a/html_orig/sass/layout/_modal.scss +++ b/html_orig/sass/layout/_modal.scss @@ -43,17 +43,18 @@ // "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; } diff --git a/user/cgi_appcfg.c b/user/cgi_appcfg.c new file mode 100644 index 0000000..87c1b73 --- /dev/null +++ b/user/cgi_appcfg.c @@ -0,0 +1,97 @@ +/* +Cgi/template routines for configuring non-wifi settings +*/ + +#include +#include "cgi_wifi.h" +#include "wifimgr.h" +#include "persist.h" + +// strcpy that adds 0 at the end of the buffer. Returns void. +#define strncpy_safe(dst, src, n) do { strncpy((char *)(dst), (char *)(src), (n)); (dst)[(n)-1]=0; } while (0) + +/** + * Universal CGI endpoint to set WiFi params. + * Note that some may cause a (delayed) restart. + */ +httpd_cgi_state ICACHE_FLASH_ATTR cgiAppCfgSet(HttpdConnData *connData) +{ + static ETSTimer timer; + + char buff[50]; + +#define REDIR_BASE_URL "/wifi?err=" + + char redir_url_buf[300]; + char *redir_url = redir_url_buf; + redir_url += sprintf(redir_url, REDIR_BASE_URL); + // 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; + } + +#define GET_ARG(key) (httpdFindArg(connData->getArgs, key, buff, sizeof(buff)) > 0) + + // TODO + 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 (redir_url_buf[strlen(REDIR_BASE_URL)] == 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, "/wifi"); + } 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 tplAppCfg(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 + + // TODO + if (streq(token, "opmode_name")) { + strcpy(buff, opmode2str(wificonf->opmode)); + } + + 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..fe4fa79 --- /dev/null +++ b/user/cgi_appcfg.h @@ -0,0 +1,9 @@ +#ifndef CGIAPPCFG_H +#define CGIAPPCFG_H + +#include "httpd.h" + +httpd_cgi_state cgiAppCfgSet(HttpdConnData *connData); +httpd_cgi_state tplAppCfg(HttpdConnData *connData, char *token, void **arg); + +#endif diff --git a/user/cgi_wifi.c b/user/cgi_wifi.c index 2ec3306..31b1ed7 100644 --- a/user/cgi_wifi.c +++ b/user/cgi_wifi.c @@ -13,17 +13,13 @@ Cgi/template routines for the /wifi url. * File adapted and improved by Ondřej Hruška */ -// TODO convert to work with WiFi Manager -// TODO make changes write to wificonf and apply when a different CGI is run (/wifi/apply or something) -// TODO (connection will trigger this immediately, with some delayto show the connecting page. Then polling cna proceed as usual) - #include #include "cgi_wifi.h" #include "wifimgr.h" #include "persist.h" // strcpy that adds 0 at the end of the buffer. Returns void. -#define strncpy_safe(dst, src, n) do { strncpy((char *)(dst), (char *)(src), (n)); dst[(n)-1]=0; } while (0) +#define strncpy_safe(dst, src, n) do { strncpy((char *)(dst), (char *)(src), (n)); (dst)[(n)-1]=0; } while (0) /** WiFi access point data */ typedef struct { @@ -419,9 +415,11 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiSetParams(HttpdConnData *connData) char buff[50]; +#define REDIR_BASE_URL "/wifi?err=" + char redir_url_buf[300]; char *redir_url = redir_url_buf; - redir_url += sprintf(redir_url, "/wifi?err="); + redir_url += sprintf(redir_url, REDIR_BASE_URL); // we'll test if anything was printed by looking for \0 in failed_keys_buf if (connData->conn == NULL) { @@ -707,7 +705,7 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiSetParams(HttpdConnData *connData) } } - if (redir_url_buf[10] == 0) { + if (redir_url_buf[strlen(REDIR_BASE_URL)] == 0) { // All was OK info("Set WiFi params - success, applying in 1000 ms"); From 5f3cb6685df3593f07d03c4c0f437f625b38b732 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sun, 23 Jul 2017 14:59:59 +0200 Subject: [PATCH 15/19] terminal settings page and backend --- CMakeLists.txt | 4 +- html_orig/_debug_replacements.php | 19 +- html_orig/_pages.php | 23 +- html_orig/css/app.css | 77 +++--- html_orig/lang/en.php | 65 +++-- html_orig/pages/cfg_app.php | 71 ++++++ html_orig/pages/cfg_network.php | 21 +- html_orig/pages/cfg_wifi_conn.php | 2 + html_orig/pages/term.php | 10 +- html_orig/sass/form/_form_layout.scss | 7 +- html_orig/sass/layout/_box.scss | 24 +- include/helpers.h | 22 ++ user/cgi_appcfg.c | 162 +++++++++---- user/cgi_network.c | 251 +++++++++++++++++++ user/cgi_network.h | 9 + user/cgi_wifi.c | 337 +++----------------------- user/cgi_wifi.h | 26 +- 17 files changed, 691 insertions(+), 439 deletions(-) create mode 100644 html_orig/pages/cfg_app.php create mode 100644 include/helpers.h create mode 100644 user/cgi_network.c create mode 100644 user/cgi_network.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 2701a1e..8e4efcc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -93,6 +93,8 @@ set(SOURCE_FILES user/io.h user/cgi_wifi.c user/cgi_wifi.h + user/cgi_network.c + user/cgi_network.h user/cgi_appcfg.c user/cgi_appcfg.h user/cgi_ping.c @@ -119,7 +121,7 @@ set(SOURCE_FILES user/wifimgr.c user/wifimgr.h user/persist.c - user/persist.h) + user/persist.h include/helpers.h) include_directories(include) include_directories(user) diff --git a/html_orig/_debug_replacements.php b/html_orig/_debug_replacements.php index 3cde458..e693ea1 100644 --- a/html_orig/_debug_replacements.php +++ b/html_orig/_debug_replacements.php @@ -8,11 +8,12 @@ return [ '%term_title%' => 'ESP8266 Wireless Terminal', - '%b1%' => '1', - '%b2%' => '2', - '%b3%' => '3', - '%b4%' => '4', - '%b5%' => '5', + '%btn1%' => '1', + '%btn2%' => '2', + '%btn3%' => '3', + '%btn4%' => '4', + '%btn5%' => '5', + '%screenData%' => '{ "w": 26, "h": 10, "x": 0, "y": 0, @@ -50,4 +51,12 @@ return [ '%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/_pages.php b/html_orig/_pages.php index 30e66f5..2db1299 100644 --- a/html_orig/_pages.php +++ b/html_orig/_pages.php @@ -17,17 +17,22 @@ if (! function_exists('pg')) { } } -pg('cfg_wifi', 'cfg', '/cfg/wifi'); -pg('cfg_wifi_conn', '', '/wifi/connecting'); // page without menu that tries to show the connection progress -pg('cfg_network', 'cfg', '/cfg/network'); -//pg('cfg_term', 'cfg', '/cfg/term'); -pg('help', 'cfg page-help', '/help'); +pg('cfg_wifi', 'cfg', '/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', '/cfg/network'); +pg('network_set', 'api', '/cfg/network/set'); + +pg('cfg_app', 'cfg', '/cfg/app'); +pg('app_set', 'api', '/cfg/app/set'); + +pg('help', 'cfg page-help', '/help'); pg('about', 'cfg page-about', '/about'); -pg('term', 'term', '/', 'title.term'); +pg('term', 'term', '/', 'title.term'); // ajax API -pg('wifi_set', 'api', '/wifi/set');//'/cfg/wifi/set'); -pg('wifi_scan', 'api', '/wifi/scan');//'/cfg/wifi/scan'); -pg('wifi_connstatus', 'api', '/wifi/connstatus'); return $pages; diff --git a/html_orig/css/app.css b/html_orig/css/app.css index f425e9a..bf34372 100644 --- a/html_orig/css/app.css +++ b/html_orig/css/app.css @@ -489,14 +489,17 @@ ul > * { margin-top: 1rem; padding: 0.61805rem 1rem; border-radius: 3px; - background-color: rgba(255, 255, 255, 0.07); } + 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; } } h1 + .Box { margin-top: 0; } .Box h2 { - margin-top: 0; } + margin-top: 0; + margin-bottom: 0 !important; } .Box.wide { width: initial; max-width: initial; } @@ -515,13 +518,26 @@ ul > * { 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; - margin-bottom: 0 !important; } + padding-right: 1.3rem; } .Box.mobcol h2::after { position: absolute; right: 0; @@ -732,7 +748,7 @@ form { input[type="number"], input[type="password"], input[type="text"], textarea, select, label.select-wrap { width: 250px; } -input[type="number"] { +input[type="number"], input.short { width: 125px; } .Box.errors .list { @@ -741,46 +757,46 @@ input[type="number"] { .Box.errors .lead { color: white; } -form .Row { +.Row { vertical-align: middle; margin: 12px auto; text-align: left; display: flex; flex-direction: row; align-items: center; } - form .Row:first-child { + .Row:first-child { margin-top: 0; } - form .Row:last-child { + .Row:last-child { margin-bottom: 0; } - form .Row .spacer { + .Row .spacer { width: 160px; } @media screen and (max-width: 544px) { - form .Row .spacer { + .Row .spacer { display: none; } } - form .Row.buttons { + .Row.buttons { margin: 16px auto; } - form .Row.buttons input, form .Row.buttons .button { + .Row.buttons input, .Row.buttons .button { margin-right: 0.61805rem; } - form .Row.centered { + .Row.centered { justify-content: center; } - form .Row.message { + .Row.message { font-size: 1em; text-shadow: 1px 1px 3px black; text-align: center; } - form .Row.message.error { + .Row.message.error { color: crimson; } - form .Row.message.ok { + .Row.message.ok { color: #0fe851; } - form .Row.separator { + .Row.separator { padding-top: 14px; border-top: 2px solid rgba(255, 255, 255, 0.1); } - form .Row textarea { + .Row textarea { display: inline-block; vertical-align: top; min-height: 10rem; flex-grow: 1; resize: vertical; } - form .Row label { + .Row label { font-weight: bold; color: white; display: inline-block; @@ -795,32 +811,33 @@ form .Row { user-select: none; white-space: nowrap; word-wrap: normal; } - form .Row label.error { + .Row label.error { color: crimson; } - form .Row input[type="range"] { + .Row input[type="range"] { width: 200px; } @media screen and (max-width: 544px) { - form .Row { - flex-direction: column; } - form .Row.buttons, form .Row.centered, form .Row.checkbox { + .Row { + flex-direction: column; + margin: 6px auto; } + .Row.buttons, .Row.centered, .Row.checkbox { flex-direction: row; } - form .Row.buttons { + .Row.buttons { justify-content: center; } - form .Row.buttons :last-child { + .Row.buttons :last-child { margin-right: 0; } - form .Row label { + .Row label { padding-left: 0; text-align: left; width: auto; } - form .Row .checkbox-wrap { + .Row .checkbox-wrap { order: 1; text-align: left; padding-bottom: 0; border-radius: .4px; width: auto; } - form .Row .checkbox-wrap + label { + .Row .checkbox-wrap + label { width: auto; } - form .Row input[type="number"], form .Row input[type="password"], form .Row input[type="text"], form .Row textarea, form .Row input[type="range"], form .Row textarea { + .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 { diff --git a/html_orig/lang/en.php b/html_orig/lang/en.php index f866c36..3c5c115 100644 --- a/html_orig/lang/en.php +++ b/html_orig/lang/en.php @@ -5,7 +5,7 @@ return [ 'menu.cfg_wifi' => 'WiFi Settings', 'menu.cfg_network' => 'Network Configuration', - 'menu.cfg_term' => 'Terminal Settings', + 'menu.cfg_app' => 'Terminal Settings', 'menu.about' => 'About ESPTerm', 'menu.help' => 'Quick Reference', 'menu.term' => 'Back to Terminal', @@ -13,8 +13,46 @@ return [ 'title.term' => 'Terminal', - 'net.ap' => 'DHCP Server', - 'net.sta' => 'DHCP Client', + '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 @@ -42,16 +80,14 @@ return [ '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.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.sta_ssid' => 'Network SSID:', - 'wifi.sta_password' => 'Password:', 'wifi.not_conn' => 'Not connected.', 'wifi.sta_none' => 'None', 'wifi.sta_active_pw' => '🔒', @@ -67,10 +103,11 @@ return [ 'wifi.conn.back_to_config' => 'Back to WiFi config', 'wifi.conn.telemetry_lost' => 'Telemetry lost, something went wrong. Try again...', - 'wifi.conn.idle' =>"Preparing to connect", + '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 your password and try again.", + 'wifi.conn.fail' => "Connection failed, check settings & try again. Cause: ", 'apply' => 'Apply!', 'enabled' => 'Enabled', diff --git a/html_orig/pages/cfg_app.php b/html_orig/pages/cfg_app.php new file mode 100644 index 0000000..f030da4 --- /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 index cf7672b..9e687a1 100644 --- a/html_orig/pages/cfg_network.php +++ b/html_orig/pages/cfg_network.php @@ -2,15 +2,14 @@ $ipmask='pattern="^([0-9]{1,3}\.){3}[0-9]{1,3}$"'; ?> -
+

-
-
+
@@ -41,15 +40,14 @@ $ipmask='pattern="^([0-9]{1,3}\.){3}[0-9]{1,3}$"';
-
+

-
-
+
@@ -74,3 +72,14 @@ $ipmask='pattern="^([0-9]{1,3}\.){3}[0-9]{1,3}$"'; required>
+ +
+

+ +
+ +
+
+ +
+
diff --git a/html_orig/pages/cfg_wifi_conn.php b/html_orig/pages/cfg_wifi_conn.php index e12cfa2..a5cab5a 100755 --- a/html_orig/pages/cfg_wifi_conn.php +++ b/html_orig/pages/cfg_wifi_conn.php @@ -10,6 +10,7 @@ var abortTmeo; var messages = tr('wifi.conn.disabled'), 'idle' => tr('wifi.conn.idle'), 'success' => tr('wifi.conn.success'), 'working' => tr('wifi.conn.working'), @@ -25,6 +26,7 @@ var done = false; var msg = messages[data.status] || '...'; if (data.status == 'success') msg += data.ip; + if (data.status == 'fail') msg += data.cause; $("#status").html(msg); diff --git a/html_orig/pages/term.php b/html_orig/pages/term.php index 427efb5..1dbf074 100644 --- a/html_orig/pages/term.php +++ b/html_orig/pages/term.php @@ -13,11 +13,11 @@
- +
diff --git a/html_orig/sass/form/_form_layout.scss b/html_orig/sass/form/_form_layout.scss index 2f0d570..ec373a7 100755 --- a/html_orig/sass/form/_form_layout.scss +++ b/html_orig/sass/form/_form_layout.scss @@ -5,7 +5,7 @@ form { @include naked(); } width: $form-field-w; } -input[type="number"] { +input[type="number"], input.short { width: $form-field-w/2; } @@ -20,7 +20,7 @@ input[type="number"] { } } -form .Row { +.Row { vertical-align: middle; margin: 12px auto; text-align: left; @@ -132,6 +132,7 @@ form .Row { // special phone style @include media($phone) { flex-direction: column; + margin: 6px auto; &.buttons, &.centered, &.checkbox { flex-direction: row; @@ -165,7 +166,7 @@ form .Row { } } - #{$all-text-inputs}, input[type="range"], textarea { + #{$all-text-inputs}, input[type="range"], textarea, select { width: 100%; } } diff --git a/html_orig/sass/layout/_box.scss b/html_orig/sass/layout/_box.scss index def421f..cdd80b3 100755 --- a/html_orig/sass/layout/_box.scss +++ b/html_orig/sass/layout/_box.scss @@ -15,10 +15,13 @@ h2 { margin-top: 0; + margin-bottom: 0 !important; } border-radius: 3px; background-color: rgba(white, .07); + box-shadow: 0 0 4px black; + border: 1px solid #4f4f4f; &.wide { width: initial; @@ -52,6 +55,25 @@ } } } + + &.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) { @@ -71,8 +93,6 @@ font-weight: bold; transform: translate(0,-50%) rotate(90deg); } - - margin-bottom: 0 !important; } &.expanded h2::after { diff --git a/include/helpers.h b/include/helpers.h new file mode 100644 index 0000000..37eeba8 --- /dev/null +++ b/include/helpers.h @@ -0,0 +1,22 @@ +// +// Created by MightyPork on 2017/07/23. +// + +#ifndef ESP_VT100_FIRMWARE_HELPERS_H +#define ESP_VT100_FIRMWARE_HELPERS_H + +// strcpy that adds 0 at the end of the buffer. Returns void. +#define strncpy_safe(dst, src, n) do { strncpy((char *)(dst), (char *)(src), (n)); (dst)[(n)-1]=0; } while (0) + +/** + * Convert IP hex to arguments for printf. + * Library IP2STR(ip) does not work correctly due to unaligned memory access. + */ +#define GOOD_IP2STR(ip) ((ip)>>0)&0xff, ((ip)>>8)&0xff, ((ip)>>16)&0xff, ((ip)>>24)&0xff + +/** + * Helper that retrieves an arg from `connData->getArgs` and stores it in `buff`. Returns 1 on success + */ +#define GET_ARG(key) (httpdFindArg(connData->getArgs, key, buff, sizeof(buff)) > 0) + +#endif //ESP_VT100_FIRMWARE_HELPERS_H diff --git a/user/cgi_appcfg.c b/user/cgi_appcfg.c index 87c1b73..0c3fe50 100644 --- a/user/cgi_appcfg.c +++ b/user/cgi_appcfg.c @@ -3,28 +3,24 @@ Cgi/template routines for configuring non-wifi settings */ #include -#include "cgi_wifi.h" -#include "wifimgr.h" +#include "cgi_appcfg.h" #include "persist.h" +#include "screen.h" +#include "helpers.h" -// strcpy that adds 0 at the end of the buffer. Returns void. -#define strncpy_safe(dst, src, n) do { strncpy((char *)(dst), (char *)(src), (n)); (dst)[(n)-1]=0; } while (0) +#define SET_REDIR_SUC "/cfg/term" +#define SET_REDIR_ERR SET_REDIR_SUC"?err=" /** - * Universal CGI endpoint to set WiFi params. - * Note that some may cause a (delayed) restart. + * Universal CGI endpoint to set Terminal params. */ httpd_cgi_state ICACHE_FLASH_ATTR cgiAppCfgSet(HttpdConnData *connData) { - static ETSTimer timer; - char buff[50]; -#define REDIR_BASE_URL "/wifi?err=" - char redir_url_buf[300]; char *redir_url = redir_url_buf; - redir_url += sprintf(redir_url, REDIR_BASE_URL); + 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) { @@ -32,40 +28,98 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiAppCfgSet(HttpdConnData *connData) return HTTPD_CGI_DONE; } -#define GET_ARG(key) (httpdFindArg(connData->getArgs, key, buff, sizeof(buff)) > 0) + // 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", buff); + // 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,"); + } + } - // TODO - 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; + 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 opmode value \"%s\"", buff); - redir_url += sprintf(redir_url, "opmode,"); + warn("Bad color %s", buff); + redir_url += sprintf(redir_url, "default_fg,"); } } - if (redir_url_buf[strlen(REDIR_BASE_URL)] == 0) { + 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 WiFi params - success, applying in 1000 ms"); + info("Set app params - success, saving..."); - // 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, "/wifi"); + httpdRedirect(connData, SET_REDIR_SUC); } else { - warn("Some WiFi settings did not validate, asking for correction"); + warn("Some settings did not validate, asking for correction"); // Some errors, appended to the URL as ?err= httpdRedirect(connData, redir_url_buf); } @@ -73,12 +127,10 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiAppCfgSet(HttpdConnData *connData) } -//Template code for the WLAN page. httpd_cgi_state ICACHE_FLASH_ATTR tplAppCfg(HttpdConnData *connData, char *token, void **arg) { - char buff[100]; - int x; - int connectStatus; +#define BUFLEN 100 + char buff[BUFLEN]; if (token == NULL) { // We're done @@ -87,9 +139,35 @@ httpd_cgi_state ICACHE_FLASH_ATTR tplAppCfg(HttpdConnData *connData, char *token strcpy(buff, ""); // fallback - // TODO - if (streq(token, "opmode_name")) { - strcpy(buff, opmode2str(wificonf->opmode)); + 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_bg); + } + 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); diff --git a/user/cgi_network.c b/user/cgi_network.c new file mode 100644 index 0000000..5067c49 --- /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, "ap_addr_mask")) { + sprintf(buff, IPSTR, GOOD_IP2STR(wificonf->sta_addr.netmask.addr)); + } + else if (streq(token, "ap_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_wifi.c b/user/cgi_wifi.c index 31b1ed7..2a45e2a 100644 --- a/user/cgi_wifi.c +++ b/user/cgi_wifi.c @@ -17,9 +17,10 @@ Cgi/template routines for the /wifi url. #include "cgi_wifi.h" #include "wifimgr.h" #include "persist.h" +#include "helpers.h" -// strcpy that adds 0 at the end of the buffer. Returns void. -#define strncpy_safe(dst, src, n) do { strncpy((char *)(dst), (char *)(src), (n)); (dst)[(n)-1]=0; } while (0) +#define SET_REDIR_SUC "/cfg/wifi" +#define SET_REDIR_ERR SET_REDIR_SUC"?err=" /** WiFi access point data */ typedef struct { @@ -40,17 +41,6 @@ typedef struct { /** Static scan status storage. */ static ScanResultData cgiWifiAps; -/** Progress of connection to AP enum */ -typedef enum { - CONNTRY_IDLE = 0, - CONNTRY_WORKING = 1, - CONNTRY_SUCCESS = 2, - CONNTRY_FAIL = 3, -} ConnTry; - -/** Connection result var */ -static ConnTry connTryStatus = CONNTRY_IDLE; - /** Connection to AP periodic check timer */ static os_timer_t staCheckTimer; @@ -257,102 +247,6 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiScan(HttpdConnData *connData) } } -/** - * This routine is ran some time after a connection attempt to an access point. If - * the connect succeeds, this gets the module in STA-only mode. - */ -static void ICACHE_FLASH_ATTR staCheckConnStatus(void *arg) -{ - int x = wifi_station_get_connect_status(); - if (x == STATION_GOT_IP) { - info("Connected to AP."); - connTryStatus = CONNTRY_SUCCESS; - - // This would enter STA only mode, but that kills the browser page if using STA+AP. - // Instead we stay in the current mode and let the user switch manually. - - //wifi_set_opmode(STATION_MODE); - //system_restart(); - } - else { - connTryStatus = CONNTRY_FAIL; - error("Connection failed."); - } -} - -/** - * Delayed connect callback - */ -static void ICACHE_FLASH_ATTR cgiWiFiConnect_do(void *arg) -{ - int x; - struct station_config cfg; - - dbg("Try to connect to AP..."); - - strncpy_safe(cfg.password, wificonf->sta_password, PASSWORD_LEN); - strncpy_safe(cfg.ssid, wificonf->sta_ssid, SSID_LEN); - cfg.bssid_set = 0; - - wifi_station_disconnect(); - wifi_station_set_config(&cfg); - wifi_station_connect(); - - x = wifi_get_opmode(); - connTryStatus = CONNTRY_WORKING; - // Assumption: - // if we're in station mode, no need to check: the browser will be disconnected - // and the user finds out whether it succeeded or not by checking if they can connect - if (x != STATION_MODE) { - os_timer_disarm(&staCheckTimer); - os_timer_setfn(&staCheckTimer, staCheckConnStatus, NULL); - os_timer_arm(&staCheckTimer, 15000, 0); //time out after 15 secs of trying to connect - } -} - -/** - * This cgi uses the routines above to connect to a specific access point with the - * given ESSID using the given password. - * - * Args: - * - essid = SSID to connect to - * - passwd = password to connect with - */ -httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiConnect(HttpdConnData *connData) -{ - char ssid[100]; - char password[100]; - static os_timer_t reassTimer; - - if (connData->conn == NULL) { - //Connection aborted. Clean up. - return HTTPD_CGI_DONE; - } - - int ssilen = httpdFindArg(connData->post->buff, "sta_ssid", ssid, sizeof(ssid)); - int passlen = httpdFindArg(connData->post->buff, "sta_password", password, sizeof(password)); - - if (ssilen == -1 || passlen == -1) { - error("Did not receive the required arguments!"); - httpdRedirect(connData, "/wifi"); - } - else { - strncpy_safe(wificonf->sta_ssid, ssid, SSID_LEN); - strncpy_safe(wificonf->sta_password, password, PASSWORD_LEN); - info("Try to connect to AP \"%s\" pw \"%s\".", ssid, password); - - //Schedule disconnect/connect - os_timer_disarm(&reassTimer); - os_timer_setfn(&reassTimer, cgiWiFiConnect_do, NULL); - // redirect & start connecting a little bit later - os_timer_arm(&reassTimer, 1000, 0); // was 500, increased so the connecting page has time to load - - connTryStatus = CONNTRY_IDLE; - httpdRedirect(connData, "/wifi/connecting"); // this page is meant to show progress - } - return HTTPD_CGI_DONE; -} - /** * Cgi to get connection status. * @@ -363,45 +257,56 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiConnect(HttpdConnData *connData) httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiConnStatus(HttpdConnData *connData) { char buff[100]; - int len; struct ip_info info; - int st = wifi_station_get_connect_status(); httpdStartResponse(connData, 200); httpdHeader(connData, "Content-Type", "application/json"); httpdEndHeaders(connData); - if (connTryStatus == CONNTRY_IDLE) { - len = sprintf(buff, "{\"status\": \"idle\"}"); + // 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; } - else if (connTryStatus == CONNTRY_WORKING || connTryStatus == CONNTRY_SUCCESS) { - if (st == STATION_GOT_IP) { + + 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); - len = sprintf(buff, "{\"status\": \"success\", \"ip\": \"" - IPSTR - "\"}", GOOD_IP2STR(info.ip.addr)); - os_timer_disarm(&staCheckTimer); - os_timer_setfn(&staCheckTimer, staCheckConnStatus, NULL); - os_timer_arm(&staCheckTimer, 1000, 0); - } else { - len = sprintf(buff, "{\"status\": \"working\"}"); - } - } - else { - len = sprintf(buff, "{\"status\": \"fail\"}"); + sprintf(buff, "{\"status\": \"success\", \"ip\": \""IPSTR"\"}", GOOD_IP2STR(info.ip.addr)); + break; } - httpdSend(connData, buff, len); + httpdSend(connData, buff, -1); return HTTPD_CGI_DONE; } -/** reset_later() timer */ - /** * Callback for async timer */ static void ICACHE_FLASH_ATTR applyWifiSettingsLaterCb(void *arg) { + (void*)arg; wifimgr_apply_settings(); } @@ -415,11 +320,9 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiSetParams(HttpdConnData *connData) char buff[50]; -#define REDIR_BASE_URL "/wifi?err=" - char redir_url_buf[300]; char *redir_url = redir_url_buf; - redir_url += sprintf(redir_url, REDIR_BASE_URL); + 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) { @@ -427,8 +330,6 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiSetParams(HttpdConnData *connData) return HTTPD_CGI_DONE; } -#define GET_ARG(key) (httpdFindArg(connData->getArgs, key, buff, sizeof(buff)) > 0) - // ---- WiFi opmode ---- if (GET_ARG("opmode")) { @@ -548,85 +449,6 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiSetParams(HttpdConnData *connData) } } - // ---- 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 SSID (to connect to) ---- if (GET_ARG("sta_ssid")) { @@ -649,63 +471,7 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiSetParams(HttpdConnData *connData) } } - // ---- 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(REDIR_BASE_URL)] == 0) { + if (redir_url_buf[strlen(SET_REDIR_ERR)] == 0) { // All was OK info("Set WiFi params - success, applying in 1000 ms"); @@ -722,7 +488,7 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiWiFiSetParams(HttpdConnData *connData) os_timer_setfn(&timer, applyWifiSettingsLaterCb, NULL); os_timer_arm(&timer, 1000, false); - httpdRedirect(connData, "/wifi"); + httpdRedirect(connData, SET_REDIR_SUC); } else { warn("Some WiFi settings did not validate, asking for correction"); // Some errors, appended to the URL as ?err= @@ -773,39 +539,12 @@ httpd_cgi_state ICACHE_FLASH_ATTR tplWlan(HttpdConnData *connData, char *token, else if (streq(token, "ap_hidden")) { sprintf(buff, "%d", wificonf->ap_hidden); } - else 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_ssid")) { sprintf(buff, "%s", wificonf->sta_ssid); } else if (streq(token, "sta_password")) { sprintf(buff, "%s", wificonf->sta_password); } - 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, "ap_addr_mask")) { - sprintf(buff, IPSTR, GOOD_IP2STR(wificonf->sta_addr.netmask.addr)); - } - else if (streq(token, "ap_addr_gw")) { - sprintf(buff, IPSTR, GOOD_IP2STR(wificonf->sta_addr.gw.addr)); - } else if (streq(token, "sta_rssi")) { sprintf(buff, "%d", wifi_station_get_rssi()); } diff --git a/user/cgi_wifi.h b/user/cgi_wifi.h index a300d2a..53999dc 100644 --- a/user/cgi_wifi.h +++ b/user/cgi_wifi.h @@ -2,32 +2,12 @@ #define CGIWIFI_H #include "httpd.h" - -/** - * Convert IP hex to arguments for printf. - * Library IP2STR(ip) does not work correctly due to unaligned memory access. - */ -#define GOOD_IP2STR(ip) ((ip)>>0)&0xff, ((ip)>>8)&0xff, ((ip)>>16)&0xff, ((ip)>>24)&0xff +#include "helpers.h" httpd_cgi_state cgiWiFiScan(HttpdConnData *connData); -httpd_cgi_state cgiWiFiConnect(HttpdConnData *connData); -httpd_cgi_state cgiWiFiConnStatus(HttpdConnData *connData); + httpd_cgi_state cgiWiFiSetParams(HttpdConnData *connData); httpd_cgi_state tplWlan(HttpdConnData *connData, char *token, void **arg); - -// WiFi config options: -// - Persistent -// - channel -// - AP ssid -// - opmode -// - AP to connect to -// - Temporary -// - sta_hostname (sta) -// - tpw (ap, sta+ap?) -// - dhcp_lt (ap, sta+ap) -// - static IP -// - static mask -// - static gw -// - dhcp enable or disable +httpd_cgi_state cgiWiFiConnStatus(HttpdConnData *connData); #endif From 7f8f7d8ad169e8c1037ab8cc61061f10525b4eea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sun, 23 Jul 2017 18:22:57 +0200 Subject: [PATCH 16/19] all accessible and nice, also added icons --- html_orig/_pages.php | 32 +++-- html_orig/css/app.css | 120 ++++++++++++++++-- html_orig/fontello/fontello-9ba19eb0.zip | Bin 0 -> 48080 bytes html_orig/js/app.js | 42 +++++- html_orig/jssrc/appcommon.js | 29 ++++- html_orig/jssrc/utils.js | 13 ++ html_orig/lang/en.php | 19 +++ html_orig/pages/_cfg_menu.php | 17 ++- html_orig/pages/cfg_admin.php | 34 +++++ html_orig/pages/cfg_app.php | 10 +- html_orig/pages/cfg_network.php | 24 ++-- html_orig/pages/cfg_wifi.php | 38 +++--- html_orig/sass/_fontello-embedded.scss | 57 +++++++++ html_orig/sass/app.scss | 2 + html_orig/sass/form/_buttons.scss | 11 +- html_orig/sass/form/_fancy_button_mixins.scss | 2 +- html_orig/sass/form/_form_elements.scss | 5 +- html_orig/sass/layout/_box.scss | 5 + html_orig/sass/layout/_menu.scss | 20 ++- html_orig/sass/layout/_modal.scss | 4 +- 20 files changed, 400 insertions(+), 84 deletions(-) create mode 100644 html_orig/fontello/fontello-9ba19eb0.zip create mode 100644 html_orig/pages/cfg_admin.php create mode 100644 html_orig/sass/_fontello-embedded.scss diff --git a/html_orig/_pages.php b/html_orig/_pages.php index 2db1299..47ae0f2 100644 --- a/html_orig/_pages.php +++ b/html_orig/_pages.php @@ -4,34 +4,40 @@ $pages = []; if (! function_exists('pg')) { /** Add a page */ - function pg($key, $bc, $path, $titleKey = null) + function pg($key, $bc, $icon, $path, $titleKey = null) { global $pages; $pages[$key] = (object) [ 'key' => $key, 'bodyclass' => $bc, 'path' => $path, + 'icon' => $icon ? "icn-$icon" : '', 'label' => tr("menu.$key"), 'title' => $titleKey ? tr($titleKey) : tr("menu.$key"), ]; } } -pg('cfg_wifi', 'cfg', '/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_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', '/cfg/network'); -pg('network_set', 'api', '/cfg/network/set'); +pg('cfg_network', 'cfg', 'network', '/cfg/network'); +pg('network_set', 'api', '', '/cfg/network/set'); -pg('cfg_app', 'cfg', '/cfg/app'); -pg('app_set', 'api', '/cfg/app/set'); +pg('cfg_app', 'cfg', 'terminal', '/cfg/app'); +pg('app_set', 'api', '', '/cfg/app/set'); -pg('help', 'cfg page-help', '/help'); -pg('about', 'cfg page-about', '/about'); -pg('term', 'term', '/', 'title.term'); +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 diff --git a/html_orig/css/app.css b/html_orig/css/app.css index bf34372..5baf098 100644 --- a/html_orig/css/app.css +++ b/html_orig/css/app.css @@ -308,6 +308,87 @@ 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; } @@ -416,14 +497,17 @@ ul > * { #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 { - content: "▸"; - padding-right: .5rem; - position: relative; - top: -0.1rem; } + vertical-align: -2px; + margin-left: 0; + margin-right: 15px; } @media screen and (max-width: 544px) { #menu a { - display: none; } } + display: none; } + #menu a::before { + margin-left: 10px; } } #menu.expanded a { display: block; } @media screen and (min-width: 545px) and (max-width: 1000px) { @@ -495,6 +579,8 @@ ul > * { @media screen and (max-width: 544px) { .Box { margin-top: 0.61805rem; } } + .Box h1, .Box h2 { + overflow: hidden; } h1 + .Box { margin-top: 0; } .Box h2 { @@ -512,6 +598,7 @@ ul > * { @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 { @@ -586,7 +673,9 @@ ul > * { 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-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; } @@ -625,7 +714,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; @@ -634,7 +722,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; } @@ -643,6 +731,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 { @@ -652,6 +743,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; @@ -660,7 +753,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; @@ -669,7 +761,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; } @@ -678,6 +770,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 { @@ -687,6 +782,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; } input[type="number"], input[type="password"], input[type="text"], textarea, select { border: 0 none; @@ -695,8 +792,6 @@ input[type="number"], input[type="password"], input[type="text"], textarea, sele 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: #2ea1f9; } @@ -704,6 +799,7 @@ input[type="number"], input[type="password"], input[type="text"], textarea, sele .Row.checkbox { line-height: 27px; } .Row.checkbox .box { + overflow: hidden; width: 27px; height: 27px; border: 1px solid #808080; diff --git a/html_orig/fontello/fontello-9ba19eb0.zip b/html_orig/fontello/fontello-9ba19eb0.zip new file mode 100644 index 0000000000000000000000000000000000000000..1c9259782ac3234b0cc8bd759b9f50d618fbf84b GIT binary patch literal 48080 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 550) { + qsa('.Box h2').forEach(function (x) { + x.removeAttribute('tabindex'); + }); + + qs('#brand').removeAttribute('tabindex'); + } }); $._loader = function(vis) { diff --git a/html_orig/jssrc/appcommon.js b/html_orig/jssrc/appcommon.js index ee4569c..0a97d82 100644 --- a/html_orig/jssrc/appcommon.js +++ b/html_orig/jssrc/appcommon.js @@ -7,18 +7,30 @@ $.ready(function () { $(box).toggleClass('checked', inp.value); - $(x).on('click', function() { + 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'); - $(h).on('click', function() { + + 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... @@ -81,6 +93,15 @@ $.ready(function () { Modal.init(); Notify.init(); + + // remove tabindixes from h2 if wide + if (window.innerWidth > 550) { + qsa('.Box h2').forEach(function (x) { + x.removeAttribute('tabindex'); + }); + + qs('#brand').removeAttribute('tabindex'); + } }); $._loader = function(vis) { diff --git a/html_orig/jssrc/utils.js b/html_orig/jssrc/utils.js index bf6d114..6fa4b7d 100755 --- a/html_orig/jssrc/utils.js +++ b/html_orig/jssrc/utils.js @@ -12,6 +12,19 @@ function bool(x) { return (x === 1 || x === '1' || x === true || x === 'true'); } +/** + * 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 */ function extend(defaults, options) { var target = {}; diff --git a/html_orig/lang/en.php b/html_orig/lang/en.php index 3c5c115..269e678 100644 --- a/html_orig/lang/en.php +++ b/html_orig/lang/en.php @@ -9,6 +9,7 @@ return [ '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', 'title.term' => 'Terminal', @@ -93,6 +94,7 @@ return [ '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!', @@ -109,6 +111,23 @@ return [ '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', diff --git a/html_orig/pages/_cfg_menu.php b/html_orig/pages/_cfg_menu.php index 569ae99..c96af23 100644 --- a/html_orig/pages/_cfg_menu.php +++ b/html_orig/pages/_cfg_menu.php @@ -1,16 +1,19 @@ - + + 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 index f030da4..d967641 100644 --- a/html_orig/pages/cfg_app.php +++ b/html_orig/pages/cfg_app.php @@ -1,10 +1,6 @@ -
+

-
- -
-
@@ -68,4 +64,8 @@
+ +
+ +
diff --git a/html_orig/pages/cfg_network.php b/html_orig/pages/cfg_network.php index 9e687a1..fd24f36 100644 --- a/html_orig/pages/cfg_network.php +++ b/html_orig/pages/cfg_network.php @@ -2,12 +2,8 @@ $ipmask='pattern="^([0-9]{1,3}\.){3}[0-9]{1,3}$"'; ?> -
-

- -
- -
+ +

@@ -38,14 +34,14 @@ $ipmask='pattern="^([0-9]{1,3}\.){3}[0-9]{1,3}$"'; required>
-
- -
-

- +
+
+ +
+

@@ -53,7 +49,7 @@ $ipmask='pattern="^([0-9]{1,3}\.){3}[0-9]{1,3}$"';
+ -->
@@ -71,6 +67,10 @@ $ipmask='pattern="^([0-9]{1,3}\.){3}[0-9]{1,3}$"'; required>
+ +
+ +
diff --git a/html_orig/pages/cfg_wifi.php b/html_orig/pages/cfg_wifi.php index 43fa995..7b8a495 100644 --- a/html_orig/pages/cfg_wifi.php +++ b/html_orig/pages/cfg_wifi.php @@ -1,13 +1,9 @@ -
-

- -
- -
+ +

+ -->
@@ -37,21 +33,21 @@
+ -->
-
- -
-

- +
+
+ +
+

+ -->
@@ -68,7 +64,7 @@
- × + ×
@@ -78,11 +74,15 @@
-
+
+ +
+ +
diff --git a/html_orig/sass/layout/_box.scss b/html_orig/sass/layout/_box.scss index e449065..a61c850 100755 --- a/html_orig/sass/layout/_box.scss +++ b/html_orig/sass/layout/_box.scss @@ -5,6 +5,13 @@ margin-top: dist(0); padding: dist(-1) dist(0); + // clear floats + &::after { + content: ''; + display: block; + clear: both + } + @include media($phone) { margin-top: dist(-1); } @@ -22,6 +29,10 @@ 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; 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 34b332c..17634ba 100755 --- a/html_orig/sass/pages/_term.scss +++ b/html_orig/sass/pages/_term.scss @@ -34,10 +34,11 @@ body.term { 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; } diff --git a/html_orig/sass/pages/_wifi.scss b/html_orig/sass/pages/_wifi.scss index b2d16e8..90a758d 100755 --- a/html_orig/sass/pages/_wifi.scss +++ b/html_orig/sass/pages/_wifi.scss @@ -18,6 +18,7 @@ border-radius: 5px; padding: dist(-2); margin-bottom: dist(-2); + margin-top: dist(-2); } #ap-noscan { diff --git a/include/helpers.h b/include/helpers.h index 37eeba8..39a756b 100644 --- a/include/helpers.h +++ b/include/helpers.h @@ -19,4 +19,7 @@ */ #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/user/cgi_appcfg.c b/user/cgi_appcfg.c index 0c3fe50..e766139 100644 --- a/user/cgi_appcfg.c +++ b/user/cgi_appcfg.c @@ -8,13 +8,14 @@ Cgi/template routines for configuring non-wifi settings #include "screen.h" #include "helpers.h" -#define SET_REDIR_SUC "/cfg/term" +#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 cgiAppCfgSet(HttpdConnData *connData) +httpd_cgi_state ICACHE_FLASH_ATTR +cgiAppCfgSetParams(HttpdConnData *connData) { char buff[50]; @@ -49,7 +50,7 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiAppCfgSet(HttpdConnData *connData) redir_url += sprintf(redir_url, "term_width,"); } } else { - warn("Missing height arg", buff); + warn("Missing height arg!"); // this wont happen normally when the form is used redir_url += sprintf(redir_url, "term_width,term_height,"); } @@ -115,6 +116,7 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiAppCfgSet(HttpdConnData *connData) // All was OK info("Set app params - success, saving..."); + terminal_apply_settings(); persist_store(); httpdRedirect(connData, SET_REDIR_SUC); @@ -127,7 +129,8 @@ httpd_cgi_state ICACHE_FLASH_ATTR cgiAppCfgSet(HttpdConnData *connData) } -httpd_cgi_state ICACHE_FLASH_ATTR tplAppCfg(HttpdConnData *connData, char *token, void **arg) +httpd_cgi_state ICACHE_FLASH_ATTR +tplAppCfg(HttpdConnData *connData, char *token, void **arg) { #define BUFLEN 100 char buff[BUFLEN]; @@ -149,7 +152,7 @@ httpd_cgi_state ICACHE_FLASH_ATTR tplAppCfg(HttpdConnData *connData, char *token sprintf(buff, "%d", termconf->default_bg); } else if (streq(token, "default_fg")) { - sprintf(buff, "%d", termconf->default_bg); + sprintf(buff, "%d", termconf->default_fg); } else if (streq(token, "term_title")) { strncpy_safe(buff, termconf->title, BUFLEN); diff --git a/user/cgi_appcfg.h b/user/cgi_appcfg.h index fe4fa79..823a0f1 100644 --- a/user/cgi_appcfg.h +++ b/user/cgi_appcfg.h @@ -3,7 +3,7 @@ #include "httpd.h" -httpd_cgi_state cgiAppCfgSet(HttpdConnData *connData); +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 index 5067c49..ad4ac21 100644 --- a/user/cgi_network.c +++ b/user/cgi_network.c @@ -231,10 +231,10 @@ httpd_cgi_state ICACHE_FLASH_ATTR tplNetwork(HttpdConnData *connData, char *toke else if (streq(token, "sta_addr_ip")) { sprintf(buff, IPSTR, GOOD_IP2STR(wificonf->sta_addr.ip.addr)); } - else if (streq(token, "ap_addr_mask")) { + else if (streq(token, "sta_addr_mask")) { sprintf(buff, IPSTR, GOOD_IP2STR(wificonf->sta_addr.netmask.addr)); } - else if (streq(token, "ap_addr_gw")) { + else if (streq(token, "sta_addr_gw")) { sprintf(buff, IPSTR, GOOD_IP2STR(wificonf->sta_addr.gw.addr)); } else if (streq(token, "sta_mac")) { 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 index 2a45e2a..653f928 100644 --- a/user/cgi_wifi.c +++ b/user/cgi_wifi.c @@ -571,10 +571,12 @@ httpd_cgi_state ICACHE_FLASH_ATTR tplWlan(HttpdConnData *connData, char *token, else { struct ip_info info; wifi_get_ip_info(STATION_IF, &info); - sprintf(buff, "ip: "IPSTR", mask: "IPSTR", gw: "IPSTR, - GOOD_IP2STR(info.ip.addr), - GOOD_IP2STR(info.netmask.addr), - GOOD_IP2STR(info.gw.addr)); + 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)); } } diff --git a/user/persist.c b/user/persist.c index c7ec05c..c1b7541 100644 --- a/user/persist.c +++ b/user/persist.c @@ -11,9 +11,6 @@ PersistBlock persist; #define PERSIST_SECTOR_ID 0x3D -// This is used to force-erase the config area (when it's changed) -#define CHECKSUM_SALT 0x02 - //region Persist and restore individual modules static void ICACHE_FLASH_ATTR @@ -81,6 +78,11 @@ 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 @@ -90,15 +92,15 @@ persist_load(void) if (hard_reset || (compute_checksum(&persist.defaults) != persist.defaults.checksum) || (compute_checksum(&persist.current) != persist.current.checksum)) { - dbg("[Persist] Checksum verification: FAILED"); + error("[Persist] Checksum verification: FAILED"); hard_reset = true; } else { - dbg("[Persist] Checksum verification: PASSED"); + info("[Persist] Checksum verification: PASSED"); } if (hard_reset) { persist_restore_hard_default(); - // this also stores them to flash and applies to modues + // this also stores them to flash and applies to modules } else { apply_live_settings(); } @@ -161,8 +163,8 @@ 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 index d9bc487..be334e4 100644 --- a/user/persist.h +++ b/user/persist.h @@ -13,15 +13,31 @@ #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; - // ... - // other settings here - // ... + + // --- 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 diff --git a/user/routes.c b/user/routes.c index 2f3dc68..ee2fcd7 100644 --- a/user/routes.c +++ b/user/routes.c @@ -10,6 +10,9 @@ #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" @@ -26,28 +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_CGI("/wifi/scan", cgiWiFiScan), - ROUTE_CGI("/wifi/connect", cgiWiFiConnect), - ROUTE_CGI("/wifi/connstatus", cgiWiFiConnStatus), - ROUTE_FILE("/wifi/connecting", "/wifi_conn.tpl"), - ROUTE_CGI("/wifi/set", cgiWiFiSetParams), + 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_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 e2e07f1..ce9075d 100644 --- a/user/screen.c +++ b/user/screen.c @@ -17,7 +17,7 @@ void terminal_restore_defaults(void) termconf->default_fg = 7; termconf->width = 26; termconf->height = 10; - sprintf(termconf->title, "ESP8266 Wireless Terminal"); + sprintf(termconf->title, "ESPTerm"); sprintf(termconf->btn1, "1"); sprintf(termconf->btn2, "2"); sprintf(termconf->btn3, "3"); diff --git a/user/wifimgr.c b/user/wifimgr.c index 3e9bc82..5a4b132 100644 --- a/user/wifimgr.c +++ b/user/wifimgr.c @@ -43,6 +43,11 @@ wifimgr_restore_defaults(void) 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 From ff6f4e35c4c8768d9d9b62c588a7c77870bb797c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Mon, 24 Jul 2017 00:40:15 +0200 Subject: [PATCH 19/19] bumped esplib version --- libesphttpd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libesphttpd b/libesphttpd index daa039f..f3dd1a2 160000 --- a/libesphttpd +++ b/libesphttpd @@ -1 +1 @@ -Subproject commit daa039fb14bc9816a27d10be9798fc5be01616f8 +Subproject commit f3dd1a25993775bec062a1906ced7b07a7fc9db1