diff --git a/.cproject b/.cproject index 8637e407..b12dcf52 100644 --- a/.cproject +++ b/.cproject @@ -1,96 +1,198 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - make - - all - true - true - true - - - make - - size-components - true - true - true - - - make - - flash - true - true - true - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + make + + + + all + + true + + true + + true + + + + + + make + + + + size-components + + true + + true + + true + + + + + + make + + + + flash + + true + + true + + true + + + + + + + diff --git a/.gitignore b/.gitignore index 40520377..75d2bfa7 100644 --- a/.gitignore +++ b/.gitignore @@ -66,4 +66,3 @@ libs/ /cdump.cmd /_* sdkconfig -*_history/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..928a2595 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "components/libwebsockets"] + path = components/libwebsockets + url = https://github.com/warmcat/libwebsockets.git +[submodule "components/mbedtls"] + path = components/mbedtls + url = https://github.com/lws-team/mbedtls.git +[submodule "components/lws-esp32"] + path = components/lws-esp32 + url = https://github.com/huming2207/lws-esp32.git diff --git a/.settings/language.settings.xml b/.settings/language.settings.xml index a70f0757..bbdb7cd9 100644 --- a/.settings/language.settings.xml +++ b/.settings/language.settings.xml @@ -1,15 +1,28 @@ - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..529885da --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,3 @@ +cmake_minimum_required(VERSION 3.5) +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(squeezelite-esp32) \ No newline at end of file diff --git a/Makefile b/Makefile index c29f086b..70212238 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,4 @@ # PROJECT_NAME := squeezelite - include $(IDF_PATH)/make/project.mk - diff --git a/README.md b/README.md index 2dcbc5d7..c2785f9b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,3 @@ -TODO -- when IP changes, best is to reboot at this point - MOST IMPORTANT: create the right default config file - make defconfig Then adapt the config file to your wifi/BT/I2C device (can alos be done on the command line) @@ -23,11 +20,6 @@ nvs_set autoexec2 str -v "squeezelite -o I2S -b 500:2000 -d all=info -m ESP32" nvs_set autoexec u8 -v 1 -4/ set bluetooth & airplaysink name (if not set in menuconfig) - -nvs_set bt_sink_name str -v "" -nvs_set airplay_sink_name str -v "" - The "join" and "squeezelite" commands can also be typed at the prompt to start manually. Use "help" to see the list. The squeezelite options are very similar to the regular Linux ones. Differences are : @@ -43,7 +35,6 @@ To add options that require quotes ("), escape them with \". For example, so use nvs_set autoexec2 str -v "squeezelite -o \"BT -n 'MySpeaker'\" -b 500:2000 -R -u m -Z 192000 -r \"44100-44100\"" # Additional misc notes to do you build -- as of this writing, ESP-IDF has a bug int he way the PLL values are calculated for i2s, so you *must* use the i2s.c file in the patch directory - for all libraries, add -mlongcalls. - audio libraries are complicated to rebuild, open an issue if you really want to - libmad, libflac (no esp's version), libvorbis (tremor - not esp's version), alac work diff --git a/components/cmd_i2c/CMakeLists.txt b/components/cmd_i2c/CMakeLists.txt index 12202eb7..14069736 100644 --- a/components/cmd_i2c/CMakeLists.txt +++ b/components/cmd_i2c/CMakeLists.txt @@ -1,4 +1,5 @@ set(COMPONENT_SRCS "cmd_i2ctools.c") set(COMPONENT_ADD_INCLUDEDIRS ".") +set(COMPONENT_REQUIRES console spi_flash) register_component() diff --git a/components/cmd_nvs/cmd_nvs.c b/components/cmd_nvs/cmd_nvs.c index ffaf5a2e..daff6e7b 100644 --- a/components/cmd_nvs/cmd_nvs.c +++ b/components/cmd_nvs/cmd_nvs.c @@ -47,7 +47,7 @@ static const type_str_pair_t type_str_pair[] = { static const size_t TYPE_STR_PAIR_SIZE = sizeof(type_str_pair) / sizeof(type_str_pair[0]); static const char *ARG_TYPE_STR = "type can be: i8, u8, i16, u16 i32, u32 i64, u64, str, blob"; -char current_namespace[16] = "storage"; +char current_namespace[16] = "espwifimgr"; static const char * TAG = "platform_esp32"; static struct { diff --git a/components/cmd_system/cmd_system.c b/components/cmd_system/cmd_system.c index 570f0b96..32f1f5c9 100644 --- a/components/cmd_system/cmd_system.c +++ b/components/cmd_system/cmd_system.c @@ -28,6 +28,12 @@ #ifdef CONFIG_FREERTOS_USE_STATS_FORMATTING_FUNCTIONS #define WITH_TASKS_INFO 1 #endif +#define LWS_MAGIC_REBOOT_TYPE_ADS 0x50001ffc +#define LWS_MAGIC_REBOOT_TYPE_REQ_FACTORY 0xb00bcafe +#define LWS_MAGIC_REBOOT_TYPE_FORCED_FACTORY 0xfaceb00b +#define LWS_MAGIC_REBOOT_TYPE_FORCED_FACTORY_BUTTON 0xf0cedfac +#define LWS_MAGIC_REBOOT_TYPE_REQ_FACTORY_ERASE_OTA 0xfac0eeee + static const char * TAG = "platform_esp32"; @@ -37,6 +43,7 @@ static void register_version(); static void register_restart(); static void register_deep_sleep(); static void register_light_sleep(); +static void register_factory_boot(); #if WITH_TASKS_INFO static void register_tasks(); #endif @@ -49,6 +56,7 @@ void register_system() register_restart(); register_deep_sleep(); register_light_sleep(); + register_factory_boot(); #if WITH_TASKS_INFO register_tasks(); #endif @@ -91,7 +99,20 @@ static int restart(int argc, char **argv) ESP_LOGI(TAG, "Restarting"); esp_restart(); } +void guided_factory() +{ + ESP_LOGI(TAG, "Rebooting to factory."); + uint32_t *p_force_factory_magic = (uint32_t *)LWS_MAGIC_REBOOT_TYPE_ADS; + *p_force_factory_magic = LWS_MAGIC_REBOOT_TYPE_REQ_FACTORY; + esp_restart(); + +} +static int restart_factory(int argc, char **argv) +{ + guided_factory(); + return 1; +} static void register_restart() { const esp_console_cmd_t cmd = { @@ -103,6 +124,16 @@ static void register_restart() ESP_ERROR_CHECK( esp_console_cmd_register(&cmd) ); } +static void register_factory_boot() +{ + const esp_console_cmd_t cmd = { + .command = "factory", + .help = "Resets and boot to factory (if available)", + .hint = NULL, + .func = &restart_factory, + }; + ESP_ERROR_CHECK( esp_console_cmd_register(&cmd) ); +} /** 'free' command prints available heap memory */ static int free_mem(int argc, char **argv) diff --git a/components/cmd_system/cmd_system.h b/components/cmd_system/cmd_system.h index be746a22..6f4fc906 100644 --- a/components/cmd_system/cmd_system.h +++ b/components/cmd_system/cmd_system.h @@ -14,6 +14,7 @@ extern "C" { // Register system functions void register_system(); +void guided_factory(); #ifdef __cplusplus } diff --git a/components/driver_bt/CMakeLists.txt b/components/driver_bt/CMakeLists.txt index cb35ab91..e907e11c 100644 --- a/components/driver_bt/CMakeLists.txt +++ b/components/driver_bt/CMakeLists.txt @@ -1,7 +1,6 @@ -set(COMPONENT_ADD_INCLUDEDIRS . ) +idf_component_register(SRCS "bt_app_core.c" "bt_app_sink.c" "bt_app_source.c" + INCLUDE_DIRS . ../tools/ + REQUIRES esp_common + PRIV_REQUIRES freertos bt io nvs_flash esp32 spi_flash newlib log console pthread +) -set(COMPONENT_SRCS "bt_app_core.c" ) -set(REQUIRES esp_common) -set(REQUIRES_COMPONENTS freertos nvs_flash esp32 spi_flash newlib log console ) - -register_component() diff --git a/components/io/CMakeLists.txt b/components/io/CMakeLists.txt new file mode 100644 index 00000000..8cf791a9 --- /dev/null +++ b/components/io/CMakeLists.txt @@ -0,0 +1,6 @@ +idf_component_register(SRCS "led.c" + INCLUDE_DIRS . ../tools/ + +) + + diff --git a/components/io/led.h b/components/io/led.h index 2744242c..ff53e3b1 100644 --- a/components/io/led.h +++ b/components/io/led.h @@ -20,7 +20,7 @@ */ #ifndef LED_H - +#define LED_H #include "driver/gpio.h" enum { LED_GREEN = 0, LED_RED }; @@ -35,4 +35,4 @@ bool led_unconfig(int idx); bool led_blink_core(int idx, int ontime, int offtime, bool push); bool led_unpush(int idx); -#endif \ No newline at end of file +#endif diff --git a/components/wifi-manager/CMakeLists.txt b/components/wifi-manager/CMakeLists.txt new file mode 100644 index 00000000..3701ba23 --- /dev/null +++ b/components/wifi-manager/CMakeLists.txt @@ -0,0 +1,6 @@ +idf_component_register(SRCS "dns_server.c" "http_server.c" "json.c" "wifi_manager.c" + INCLUDE_DIRS . + REQUIRES esp_common + PRIV_REQUIRES newlib freertos spi_flash nvs_flash mdns pthread wpa_supplicant cmd_system + EMBED_FILES style.css jquery.gz code.js index.html +) \ No newline at end of file diff --git a/components/wifi-manager/Kconfig.projbuild b/components/wifi-manager/Kconfig.projbuild new file mode 100644 index 00000000..3f5760dc --- /dev/null +++ b/components/wifi-manager/Kconfig.projbuild @@ -0,0 +1,67 @@ +menu "Wifi Manager Configuration" + +config WIFI_MANAGER_TASK_PRIORITY + int "RTOS Task Priority for the wifi_manager" + default 5 + help + Tasks spawn by the manager will have a priority of WIFI_MANAGER_TASK_PRIORITY-1. For this particular reason, minimum recommended task priority is 2. + +config WIFI_MANAGER_MAX_RETRY + int "Max Retry on failed connection" + default 2 + help + Defines when a connection is lost/attempt to connect is made, how many retries should be made before giving up. + +config DEFAULT_AP_SSID + string "Access Point SSID" + default "esp32" + help + SSID (network name) the the esp32 will broadcast. + +config DEFAULT_AP_PASSWORD + string "Access Point Password" + default "esp32pwd" + help + Password used for the Access Point. Leave empty and set AUTH MODE to WIFI_AUTH_OPEN for no password. + +config DEFAULT_AP_CHANNEL + int "Access Point WiFi Channel" + default 1 + help + Be careful you might not see the access point if you use a channel not allowed in your country. + +config DEFAULT_AP_IP + string "Access Point IP Address" + default "10.10.0.1" + help + This is used for the redirection to the captive portal. It is recommended to leave unchanged. + +config DEFAULT_AP_GATEWAY + string "Access Point IP Gateway" + default "10.10.0.1" + help + This is used for the redirection to the captive portal. It is recommended to leave unchanged. + +config DEFAULT_AP_NETMASK + string "Access Point Netmask" + default "255.255.255.0" + help + This is used for the redirection to the captive portal. It is recommended to leave unchanged. + +config DEFAULT_AP_MAX_CONNECTIONS + int "Access Point Max Connections" + default 4 + help + Max is 4. + +config DEFAULT_AP_BEACON_INTERVAL + int "Access Point Beacon Interval (ms)" + default 100 + help + 100ms is the recommended default. +config DEFAULT_COMMAND_LINE + string "Default command line to execute" + default "squeezelite -o I2S -b 500:2000 -d all=info" + help + This is the command to run when starting the device +endmenu diff --git a/components/wifi-manager/LICENSE.md b/components/wifi-manager/LICENSE.md new file mode 100644 index 00000000..5f2ac0bc --- /dev/null +++ b/components/wifi-manager/LICENSE.md @@ -0,0 +1,19 @@ +Copyright (c) 2017-2019 Tony Pottier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/components/wifi-manager/README.md b/components/wifi-manager/README.md new file mode 100644 index 00000000..d1652c15 --- /dev/null +++ b/components/wifi-manager/README.md @@ -0,0 +1,41 @@ +# What is esp32-wifi-manager? +*esp32-wifi-manager* is an esp32 program that enables easy management of wifi networks through a web application. + +*esp32-wifi-manager* is **lightweight** (8KB of task stack in total) and barely uses any CPU power through a completely event driven architecture. It's an all in one wifi scanner, http server & dns daemon living in the least amount of RAM possible. + +For real time constrained applications, *esp32-wifi-manager* can live entirely on PRO CPU, leaving the entire APP CPU untouched for your own needs. + +*esp32-wifi-manager* will automatically attempt to re-connect to a previously saved network on boot, and it will start its own wifi access point through which you can manage wifi networks if a saved network cannot be found and/or if the connection is lost. + +*esp32-wifi-manager* is an esp-idf project that compiles successfully with the esp-idf 3.2 release. You can simply copy the project and start adding your own code to it. + +# Demo +[![esp32-wifi-manager demo](http://img.youtube.com/vi/hxlZi15bym4/0.jpg)](http://www.youtube.com/watch?v=hxlZi15bym4) + +# Look and Feel +![esp32-wifi-manager on an mobile device](https://idyl.io/wp-content/uploads/2017/11/esp32-wifi-manager-password.png "esp32-wifi-manager") ![esp32-wifi-manager on an mobile device](https://idyl.io/wp-content/uploads/2017/11/esp32-wifi-manager-connected-to.png "esp32-wifi-manager") + +# Adding esp32-wifi-manager to your code +Ther are effectively three different ways you can embed esp32-wifi-manager with your code: +* Just forget about it and poll in your code for wifi connectivity status +* Use event callbacks +* Modify esp32-wifi-manager code directly to fit your needs + +**Event callbacks** are the cleanest way to use the wifi manager and that's the recommended way to do it. A typical use-case would be to get notified when wifi manager finally gets a connection an access point. In order to do this you can simply define a callback function: + +```c +void cb_connection_ok(void *pvParameter){ + ESP_LOGI(TAG, "I have a connection!"); +} +``` + +Then just register it by calling: + +```c +wifi_manager_set_callback(EVENT_STA_GOT_IP, &cb_connection_ok); +``` + +That's it! Now everytime the event is triggered it will call this function. + +# License +*esp32-wifi-manager* is MIT licensed. As such, it can be included in any project, commercial or not, as long as you retain original copyright. Please make sure to read the license file. diff --git a/components/wifi-manager/ap.json b/components/wifi-manager/ap.json new file mode 100644 index 00000000..de61f86a --- /dev/null +++ b/components/wifi-manager/ap.json @@ -0,0 +1,12 @@ +[ +{"ssid":"Pantum-AP-A6D49F","chan":11,"rssi":-55,"auth":4}, +{"ssid":"a0308","chan":1,"rssi":-56,"auth":3}, +{"ssid":"dlink-D9D8","chan":11,"rssi":-82,"auth":4}, +{"ssid":"Linksys06730","chan":7,"rssi":-85,"auth":3}, +{"ssid":"SINGTEL-5171","chan":9,"rssi":-88,"auth":4}, +{"ssid":"1126-1","chan":11,"rssi":-89,"auth":4}, +{"ssid":"The Shah 5GHz-2","chan":1,"rssi":-90,"auth":3}, +{"ssid":"SINGTEL-1D28 (2G)","chan":11,"rssi":-91,"auth":3}, +{"ssid":"dlink-F864","chan":1,"rssi":-92,"auth":4}, +{"ssid":"dlink-74F0","chan":1,"rssi":-93,"auth":4} +] \ No newline at end of file diff --git a/components/wifi-manager/code.js b/components/wifi-manager/code.js new file mode 100644 index 00000000..f50a994b --- /dev/null +++ b/components/wifi-manager/code.js @@ -0,0 +1,439 @@ +var commandHeader = 'squeezelite -b 500:2000 -d all=info '; + +// First, checks if it isn't implemented yet. +if (!String.prototype.format) { + String.prototype.format = function() { + var args = arguments; + return this.replace(/{(\d+)}/g, function(match, number) { + return typeof args[number] != 'undefined' + ? args[number] + : match + ; + }); + }; +} + +var apList = null; +var selectedSSID = ""; +var refreshAPInterval = null; +var checkStatusInterval = null; + +var StatusIntervalActive = false; +var ConfigIntervalActive = false; +var RefreshAPIIntervalActive = false; + + +function stopCheckStatusInterval(){ + if(checkStatusInterval != null){ + clearTimeout(checkStatusInterval); + checkStatusInterval = null; + } + StatusIntervalActive = false; +} + +function stopRefreshAPInterval(){ + + if(refreshAPInterval != null){ + clearTimeout(refreshAPInterval); + refreshAPInterval = null; + } + RefreshAPIIntervalActive = false; +} + + +function startCheckStatusInterval(){ + StatusIntervalActive = true; + checkStatusInterval = setTimeout(checkStatus, 950); +} + +function startRefreshAPInterval(){ + RefreshAPIIntervalActive = true; + refreshAPInterval = setTimeout(refreshAP, 2800); +} + + +function RepeatCheckStatusInterval(){ + if(StatusIntervalActive) + startCheckStatusInterval(); +} + +function RepeatCheckConfigInterval(){ + if(ConfigIntervalActive) + startCheckConfigInterval(); +} + +function RepeatRefreshAPInterval(){ + if(RefreshAPIIntervalActive) + startRefreshAPInterval() +} + +$(document).ready(function(){ + $("#wifi-status").on("click", ".ape", function() { + $( "#wifi" ).slideUp( "fast", function() {}); + $( "#connect-details" ).slideDown( "fast", function() {}); + }); + + $("#manual_add").on("click", ".ape", function() { + selectedSSID = $(this).text(); + $( "#ssid-pwd" ).text(selectedSSID); + $( "#wifi" ).slideUp( "fast", function() {}); + $( "#connect_manual" ).slideDown( "fast", function() {}); + $( "#connect" ).slideUp( "fast", function() {}); + + //update wait screen + $( "#loading" ).show(); + $( "#connect-success" ).hide(); + $( "#connect-fail" ).hide(); + }); + + $("#wifi-list").on("click", ".ape", function() { + selectedSSID = $(this).text(); + $( "#ssid-pwd" ).text(selectedSSID); + $( "#wifi" ).slideUp( "fast", function() {}); + $( "#connect_manual" ).slideUp( "fast", function() {}); + $( "#connect" ).slideDown( "fast", function() {}); + + //update wait screen + $( "#loading" ).show(); + $( "#connect-success" ).hide(); + $( "#connect-fail" ).hide(); + }); + + $("#cancel").on("click", function() { + selectedSSID = ""; + $( "#connect" ).slideUp( "fast", function() {}); + $( "#connect_manual" ).slideUp( "fast", function() {}); + $( "#wifi" ).slideDown( "fast", function() {}); + }); + + $("#manual_cancel").on("click", function() { + selectedSSID = ""; + $( "#connect" ).slideUp( "fast", function() {}); + $( "#connect_manual" ).slideUp( "fast", function() {}); + $( "#wifi" ).slideDown( "fast", function() {}); + }); + + $("#join").on("click", function() { + performConnect(); + }); + + $("#manual_join").on("click", function() { + performConnect($(this).data('connect')); + }); + + $("#ok-details").on("click", function() { + $( "#connect-details" ).slideUp( "fast", function() {}); + $( "#wifi" ).slideDown( "fast", function() {}); + + }); + + $("#ok-credits").on("click", function() { + $( "#credits" ).slideUp( "fast", function() {}); + $( "#app" ).slideDown( "fast", function() {}); + }); + + $("#acredits").on("click", function(event) { + event.preventDefault(); + $( "#app" ).slideUp( "fast", function() {}); + $( "#credits" ).slideDown( "fast", function() {}); + }); + + $("#ok-connect").on("click", function() { + $( "#connect-wait" ).slideUp( "fast", function() {}); + $( "#wifi" ).slideDown( "fast", function() {}); + }); + + $("#disconnect").on("click", function() { + $( "#connect-details-wrap" ).addClass('blur'); + $( "#diag-disconnect" ).slideDown( "fast", function() {}); + }); + + $("#no-disconnect").on("click", function() { + $( "#diag-disconnect" ).slideUp( "fast", function() {}); + $( "#connect-details-wrap" ).removeClass('blur'); + }); + + $("#yes-disconnect").on("click", function() { + + stopCheckStatusInterval(); + selectedSSID = ""; + + $( "#diag-disconnect" ).slideUp( "fast", function() {}); + $( "#connect-details-wrap" ).removeClass('blur'); + + $.ajax({ + url: '/connect.json', + dataType: 'json', + method: 'DELETE', + cache: false, + data: { 'timestamp': Date.now()} + }); + + startCheckStatusInterval(); + + $( "#connect-details" ).slideUp( "fast", function() {}); + $( "#wifi" ).slideDown( "fast", function() {}) + }); + + $("#update-command").click(function() { + updateAutoexec(); + }); + + $("#generate-command").click(function() { + generateCommand(); + }); + + $('[name=audio]').click(function(){ + selectOutput(this); + }); + + //first time the page loads: attempt get the connection status and start the wifi scan + refreshAP(); + startCheckStatusInterval(); + startRefreshAPInterval(); + getConfig(); +}); + +function performConnect(conntype){ + + //stop the status refresh. This prevents a race condition where a status + //request would be refreshed with wrong ip info from a previous connection + //and the request would automatically shows as succesful. + stopCheckStatusInterval(); + + //stop refreshing wifi list + stopRefreshAPInterval(); + + var pwd; + if (conntype == 'manual') { + //Grab the manual SSID and PWD + selectedSSID=$('#manual_ssid').val(); + pwd = $("#manual_pwd").val(); + }else{ + pwd = $("#pwd").val(); + } + //reset connection + $( "#loading" ).show(); + $( "#connect-success" ).hide(); + $( "#connect-fail" ).hide(); + + $( "#ok-connect" ).prop("disabled",true); + $( "#ssid-wait" ).text(selectedSSID); + $( "#connect" ).slideUp( "fast", function() {}); + $( "#connect_manual" ).slideUp( "fast", function() {}); + $( "#connect-wait" ).slideDown( "fast", function() {}); + + + $.ajax({ + url: '/connect.json', + dataType: 'json', + method: 'POST', + cache: false, + headers: { 'X-Custom-ssid': selectedSSID, 'X-Custom-pwd': pwd }, + data: { 'timestamp': Date.now()} + }); + + + //now we can re-set the intervals regardless of result + startCheckStatusInterval(); + startRefreshAPInterval(); +} + + + +function rssiToIcon(rssi){ + if(rssi >= -60){ + return 'w0'; + } + else if(rssi >= -67){ + return 'w1'; + } + else if(rssi >= -75){ + return 'w2'; + } + else{ + return 'w3'; + } +} + +function refreshAP(){ + $.getJSON( "/ap.json", function( data ) { + if(data.length > 0){ + //sort by signal strength + data.sort(function (a, b) { + var x = a["rssi"]; var y = b["rssi"]; + return ((x < y) ? 1 : ((x > y) ? -1 : 0)); + }); + apList = data; + refreshAPHTML(apList); + + } + }); + RepeatRefreshAPInterval(); +} + +function refreshAPHTML(data){ + var h = ""; + data.forEach(function(e, idx, array) { + h += '
{3}
'.format(idx === array.length - 1?'':' brdb', rssiToIcon(e.rssi), e.auth==0?'':'pw',e.ssid); + h += "\n"; + }); + + $( "#wifi-list" ).html(h) +} + +function checkStatus(){ + $.getJSON( "/status.json", function( data ) { + if(data.hasOwnProperty('autoexec1') && data['autoexec1'] != ""){ + $("#autoexec1_current").text(data["autoexec1"]); + } + if(data.hasOwnProperty('ssid') && data['ssid'] != ""){ + if(data["ssid"] === selectedSSID){ + //that's a connection attempt + if(data["urc"] === 0){ + //got connection + $("#connected-to span").text(data["ssid"]); + $("#connect-details h1").text(data["ssid"]); + $("#ip").text(data["ip"]); + $("#netmask").text(data["netmask"]); + $("#gw").text(data["gw"]); + $("#wifi-status").slideDown( "fast", function() {}); + + //unlock the wait screen if needed + $( "#ok-connect" ).prop("disabled",false); + + //update wait screen + $( "#loading" ).hide(); + $( "#connect-success" ).show(); + $( "#connect-fail" ).hide(); + } + else if(data["urc"] === 1){ + //failed attempt + $("#connected-to span").text(''); + $("#connect-details h1").text(''); + $("#ip").text('0.0.0.0'); + $("#netmask").text('0.0.0.0'); + $("#gw").text('0.0.0.0'); + + //don't show any connection + $("#wifi-status").slideUp( "fast", function() {}); + + //unlock the wait screen + $( "#ok-connect" ).prop("disabled",false); + + //update wait screen + $( "#loading" ).hide(); + $( "#connect-fail" ).show(); + $( "#connect-success" ).hide(); + } + } + else if(data.hasOwnProperty('urc') && data['urc'] === 0){ + //ESP32 is already connected to a wifi without having the user do anything + if( !($("#wifi-status").is(":visible")) ){ + $("#connected-to span").text(data["ssid"]); + $("#connect-details h1").text(data["ssid"]); + $("#ip").text(data["ip"]); + $("#netmask").text(data["netmask"]); + $("#gw").text(data["gw"]); + $("#wifi-status").slideDown( "fast", function() {}); + } + } + } + else if(data.hasOwnProperty('urc') && data['urc'] === 2){ + //that's a manual disconnect + if($("#wifi-status").is(":visible")){ + $("#wifi-status").slideUp( "fast", function() {}); + } + } + }) + .fail(function() { + //don't do anything, the server might be down while esp32 recalibrates radio + }); + + RepeatCheckStatusInterval(); +} + +function getConfig() { + $.getJSON("/config.json", function(data) { + if (data.hasOwnProperty('autoexec')) { + if (data["autoexec"] === 1) { + console.log('turn on autoexec'); + $("#autoexec-cb")[0].checked=true; + } else { + console.log('turn off autoexec'); + $("#autoexec-cb")[0].checked=false; + $("#autoexec-command").hide(200); + } + } + if (data.hasOwnProperty('list')) { + data.list.forEach(function(line) { + let key = Object.keys(line)[0]; + let val = Object.values(line)[0]; + console.log(key, val); + if (key == 'autoexec1') { + $("#autoexec1").val(val); + } + }); + } + }) + .fail(function() { + console.log("failed to fetch config!"); + }); +} + +function updateAutoexec(){ + autoexec = ($("#autoexec-cb")[0].checked)?1:0; + autoexec1 = $("#autoexec1").val(); + + $.ajax({ + url: '/config.json', + dataType: 'json', + method: 'POST', + cache: false, + headers: { "X-Custom-autoexec": autoexec, "X-Custom-autoexec1": autoexec1 }, + data: { 'timestamp': Date.now() } + }); + console.log('sent config JSON with headers:', autoexec, autoexec1); +} + +function performFactory(){ +// $( "#ok-connect" ).prop("disabled",true); +// $( "#ssid-wait" ).text(selectedSSID); +// $( "#connect" ).slideUp( "fast", function() {}); +// $( "#connect_manual" ).slideUp( "fast", function() {}); +// $( "#connect-wait" ).slideDown( "fast", function() {}); +// // todo: should we update the UI here? + + $.ajax({ + url: '/factory.json', + dataType: 'json', + method: 'POST', + cache: false, + data: { 'timestamp': Date.now()} + }); +} + +var output = ''; +function selectOutput(el) { + if ($(el).attr('id') == 'bt') { + $("#btsinkdiv").show(200); + output = 'bt'; + } else { + $("#btsinkdiv").hide(200); + output = 'i2s'; + } +} + +function generateCommand() { + var commandLine = commandHeader + '-n ' + $("#player").val(); + + if (output == 'bt') { + commandLine += ' -o "BT -n \'' + $("#btsink").val() + '\'" -R -u m -Z 192000 -r "44100-44100"'; + } else { + commandLine += ' -o I2S'; + } + if ($("#optional").val() != '') { + commandLine += ' ' + $("#optional").val(); + } + $("#autoexec1").val(commandLine); +} diff --git a/components/wifi-manager/component.mk b/components/wifi-manager/component.mk new file mode 100644 index 00000000..ce01ad6e --- /dev/null +++ b/components/wifi-manager/component.mk @@ -0,0 +1,11 @@ +# +# Component Makefile +# +# This Makefile should, at the very least, just include $(SDK_PATH)/Makefile. By default, +# this will take the sources in the src/ directory, compile them and link them into +# lib(subdirectory_name).a in the build directory. This behaviour is entirely configurable, +# please read the SDK documents if you need to do this. +# +COMPONENT_EMBED_FILES := style.css jquery.gz code.js index.html +CFLAGS += -D LOG_LOCAL_LEVEL=ESP_LOG_DEBUG +COMPONENT_ADD_INCLUDEDIRS := . diff --git a/components/wifi-manager/compress.bat b/components/wifi-manager/compress.bat new file mode 100644 index 00000000..bff6a512 --- /dev/null +++ b/components/wifi-manager/compress.bat @@ -0,0 +1,2 @@ +gzip index.html style.css jquery.js --best --keep --force +pause \ No newline at end of file diff --git a/components/wifi-manager/connect b/components/wifi-manager/connect new file mode 100644 index 00000000..8c7fe211 --- /dev/null +++ b/components/wifi-manager/connect @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/components/wifi-manager/dns_server.c b/components/wifi-manager/dns_server.c new file mode 100644 index 00000000..c336290d --- /dev/null +++ b/components/wifi-manager/dns_server.c @@ -0,0 +1,184 @@ +/* +Copyright (c) 2019 Tony Pottier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +@file dns_server.c +@author Tony Pottier +@brief Defines an extremely basic DNS server for captive portal functionality. +It's basically a DNS hijack that replies to the esp's address no matter which +request is sent to it. + +Contains the freeRTOS task for the DNS server that processes the requests. + +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +*/ + +#include "dns_server.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include + +#include "wifi_manager.h" + +static const char TAG[] = "dns_server"; +static TaskHandle_t task_dns_server = NULL; +int socket_fd; + +void dns_server_start() { + xTaskCreate(&dns_server, "dns_server", 3072, NULL, WIFI_MANAGER_TASK_PRIORITY-1, &task_dns_server); +} + +void dns_server_stop(){ + if(task_dns_server){ + vTaskDelete(task_dns_server); + close(socket_fd); + task_dns_server = NULL; + } + +} + + + +void dns_server(void *pvParameters) { + + + + struct sockaddr_in sa, ra; + + /* Set redirection DNS hijack to the access point IP */ + ip4_addr_t ip_resolved; + inet_pton(AF_INET, DEFAULT_AP_IP, &ip_resolved); + + + /* Create UDP socket */ + socket_fd = socket(AF_INET, SOCK_DGRAM, 0); + if (socket_fd < 0){ + ESP_LOGE(TAG, "Failed to create socket"); + exit(0); + } + memset(&sa, 0, sizeof(struct sockaddr_in)); + + /* Bind to port 53 (typical DNS Server port) */ + tcpip_adapter_ip_info_t ip; + tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_STA, &ip); + ra.sin_family = AF_INET; + ra.sin_addr.s_addr = ip.ip.addr; + ra.sin_port = htons(53); + if (bind(socket_fd, (struct sockaddr *)&ra, sizeof(struct sockaddr_in)) == -1) { + ESP_LOGE(TAG, "Failed to bind to 53/udp"); + close(socket_fd); + exit(1); + } + + struct sockaddr_in client; + socklen_t client_len; + client_len = sizeof(client); + int length; + uint8_t data[DNS_QUERY_MAX_SIZE]; /* dns query buffer */ + uint8_t response[DNS_ANSWER_MAX_SIZE]; /* dns response buffer */ + char ip_address[INET_ADDRSTRLEN]; /* buffer to store IPs as text. This is only used for debug and serves no other purpose */ + char *domain; /* This is only used for debug and serves no other purpose */ + int err; + + ESP_LOGI(TAG, "DNS Server listening on 53/udp"); + + /* Start loop to process DNS requests */ + for(;;) { + + memset(data, 0x00, sizeof(data)); /* reset buffer */ + length = recvfrom(socket_fd, data, sizeof(data), 0, (struct sockaddr *)&client, &client_len); /* read udp request */ + + /*if the query is bigger than the buffer size we simply ignore it. This case should only happen in case of multiple + * queries within the same DNS packet and is not supported by this simple DNS hijack. */ + if ( length > 0 && ((length + sizeof(dns_answer_t)-1) < DNS_ANSWER_MAX_SIZE) ) { + + data[length] = '\0'; /*in case there's a bogus domain name that isn't null terminated */ + + /* Generate header message */ + memcpy(response, data, sizeof(dns_header_t)); + dns_header_t *dns_header = (dns_header_t*)response; + dns_header->QR = 1; /*response bit */ + dns_header->OPCode = DNS_OPCODE_QUERY; /* no support for other type of response */ + dns_header->AA = 1; /*authoritative answer */ + dns_header->RCode = DNS_REPLY_CODE_NO_ERROR; /* no error */ + dns_header->TC = 0; /*no truncation */ + dns_header->RD = 0; /*no recursion */ + dns_header->ANCount = dns_header->QDCount; /* set answer count = question count -- duhh! */ + dns_header->NSCount = 0x0000; /* name server resource records = 0 */ + dns_header->ARCount = 0x0000; /* resource records = 0 */ + + + /* copy the rest of the query in the response */ + memcpy(response + sizeof(dns_header_t), data + sizeof(dns_header_t), length - sizeof(dns_header_t)); + + + /* extract domain name and request IP for debug */ + inet_ntop(AF_INET, &(client.sin_addr), ip_address, INET_ADDRSTRLEN); + domain = (char*) &data[sizeof(dns_header_t) + 1]; + for(char* c=domain; *c != '\0'; c++){ + if(*c < ' ' || *c > 'z') *c = '.'; /* technically we should test if the first two bits are 00 (e.g. if( (*c & 0xC0) == 0x00) *c = '.') but this makes the code a lot more readable */ + } + ESP_LOGI(TAG, "Replying to DNS request for %s from %s", domain, ip_address); + + + /* create DNS answer at the end of the query*/ + dns_answer_t *dns_answer = (dns_answer_t*)&response[length]; + dns_answer->NAME = __bswap_16(0xC00C); /* This is a pointer to the beginning of the question. As per DNS standard, first two bits must be set to 11 for some odd reason hence 0xC0 */ + dns_answer->TYPE = __bswap_16(DNS_ANSWER_TYPE_A); + dns_answer->CLASS = __bswap_16(DNS_ANSWER_CLASS_IN); + dns_answer->TTL = (uint32_t)0x00000000; /* no caching. Avoids DNS poisoning since this is a DNS hijack */ + dns_answer->RDLENGTH = __bswap_16(0x0004); /* 4 byte => size of an ipv4 address */ + dns_answer->RDATA = ip_resolved.addr; + + err = sendto(socket_fd, response, length+sizeof(dns_answer_t), 0, (struct sockaddr *)&client, client_len); + if (err < 0) { + ESP_LOGE(TAG, "UDP sendto failed: %d", err); + } + } + + taskYIELD(); /* allows the freeRTOS scheduler to take over if needed. DNS daemon should not be taxing on the system */ + + } + close(socket_fd); + + vTaskDelete ( NULL ); +} + + + + diff --git a/components/wifi-manager/dns_server.h b/components/wifi-manager/dns_server.h new file mode 100644 index 00000000..48075699 --- /dev/null +++ b/components/wifi-manager/dns_server.h @@ -0,0 +1,140 @@ +/* +Copyright (c) 2019 Tony Pottier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +@file dns_server.h +@author Tony Pottier +@brief Defines an extremly basic DNS server for captive portal functionality. + +Contains the freeRTOS task for the DNS server that processes the requests. + +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +@see http://www.zytrax.com/books/dns/ch15 +*/ + +#ifndef MAIN_DNS_SERVER_H_ +#define MAIN_DNS_SERVER_H_ +#include +#include + + +#ifdef __cplusplus +extern "C" { +#endif + + +/** 12 byte header, 64 byte domain name, 4 byte qtype/qclass. This NOT compliant with the RFC, but it's good enough for a captive portal + * if a DNS query is too big it just wont be processed. */ +#define DNS_QUERY_MAX_SIZE 80 + +/** Query + 2 byte ptr, 2 byte type, 2 byte class, 4 byte TTL, 2 byte len, 4 byte data */ +#define DNS_ANSWER_MAX_SIZE (DNS_QUERY_MAX_SIZE+16) + + +/** + * @brief RCODE values used in a DNS header message + */ +typedef enum dns_reply_code_t { + DNS_REPLY_CODE_NO_ERROR = 0, + DNS_REPLY_CODE_FORM_ERROR = 1, + DNS_REPLY_CODE_SERVER_FAILURE = 2, + DNS_REPLY_CODE_NON_EXISTANT_DOMAIN = 3, + DNS_REPLY_CODE_NOT_IMPLEMENTED = 4, + DNS_REPLY_CODE_REFUSED = 5, + DNS_REPLY_CODE_YXDOMAIN = 6, + DNS_REPLY_CODE_YXRRSET = 7, + DNS_REPLY_CODE_NXRRSET = 8 +}dns_reply_code_t; + + + +/** + * @brief OPCODE values used in a DNS header message + */ +typedef enum dns_opcode_code_t { + DNS_OPCODE_QUERY = 0, + DNS_OPCODE_IQUERY = 1, + DNS_OPCODE_STATUS = 2 +}dns_opcode_code_t; + + + +/** + * @brief Represents a 12 byte DNS header. + * __packed__ is needed to prevent potential unwanted memory alignments + */ +typedef struct __attribute__((__packed__)) dns_header_t{ + uint16_t ID; // identification number + uint8_t RD : 1; // recursion desired + uint8_t TC : 1; // truncated message + uint8_t AA : 1; // authoritive answer + uint8_t OPCode : 4; // message_type + uint8_t QR : 1; // query/response flag + uint8_t RCode : 4; // response code + uint8_t Z : 3; // its z! reserved + uint8_t RA : 1; // recursion available + uint16_t QDCount; // number of question entries + uint16_t ANCount; // number of answer entries + uint16_t NSCount; // number of authority entries + uint16_t ARCount; // number of resource entries +}dns_header_t; + + + +typedef enum dns_answer_type_t { + DNS_ANSWER_TYPE_A = 1, + DNS_ANSWER_TYPE_NS = 2, + DNS_ANSWER_TYPE_CNAME = 5, + DNS_ANSWER_TYPE_SOA = 6, + DNS_ANSWER_TYPE_WKS = 11, + DNS_ANSWER_TYPE_PTR = 12, + DNS_ANSWER_TYPE_MX = 15, + DNS_ANSWER_TYPE_SRV = 33, + DNS_ANSWER_TYPE_AAAA = 28 +}dns_answer_type_t; + +typedef enum dns_answer_class_t { + DNS_ANSWER_CLASS_IN = 1 +}dns_answer_class_t; + + + +typedef struct __attribute__((__packed__)) dns_answer_t{ + uint16_t NAME; /* for the sake of simplicity only 16 bit pointers are supported */ + uint16_t TYPE; /* Unsigned 16 bit value. The resource record types - determines the content of the RDATA field. */ + uint16_t CLASS; /* Class of response. */ + uint32_t TTL; /* The time in seconds that the record may be cached. A value of 0 indicates the record should not be cached. */ + uint16_t RDLENGTH; /* Unsigned 16-bit value that defines the length in bytes of the RDATA record. */ + uint32_t RDATA; /* For the sake of simplicity only ipv4 is supported, and as such it's a unsigned 32 bit */ +}dns_answer_t; + +void dns_server(void *pvParameters); +void dns_server_start(); +void dns_server_stop(); + + + +#ifdef __cplusplus +} +#endif + + +#endif /* MAIN_DNS_SERVER_H_ */ diff --git a/components/wifi-manager/http_server.c b/components/wifi-manager/http_server.c new file mode 100644 index 00000000..74cd7cdd --- /dev/null +++ b/components/wifi-manager/http_server.c @@ -0,0 +1,399 @@ +/* +Copyright (c) 2017-2019 Tony Pottier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +@file http_server.c +@author Tony Pottier +@brief Defines all functions necessary for the HTTP server to run. + +Contains the freeRTOS task for the HTTP listener and all necessary support +function to process requests, decode URLs, serve files, etc. etc. + +@note http_server task cannot run without the wifi_manager task! +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +*/ + +#include "http_server.h" +#include "cmd_system.h" + + +/* @brief tag used for ESP serial console messages */ +static const char TAG[] = "http_server"; +static const char json_start[] = "{ \"autoexec\": %u, \"list\": ["; +static const char json_end[] = "]}"; +static const char template[] = "{ \"%s\": \"%s\" }"; +static const char array_separator[]=","; + +/* @brief task handle for the http server */ +static TaskHandle_t task_http_server = NULL; + + +/** + * @brief embedded binary data. + * @see file "component.mk" + * @see https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html#embedding-binary-data + */ +extern const uint8_t style_css_start[] asm("_binary_style_css_start"); +extern const uint8_t style_css_end[] asm("_binary_style_css_end"); +extern const uint8_t jquery_gz_start[] asm("_binary_jquery_gz_start"); +extern const uint8_t jquery_gz_end[] asm("_binary_jquery_gz_end"); +extern const uint8_t code_js_start[] asm("_binary_code_js_start"); +extern const uint8_t code_js_end[] asm("_binary_code_js_end"); +extern const uint8_t index_html_start[] asm("_binary_index_html_start"); +extern const uint8_t index_html_end[] asm("_binary_index_html_end"); + + +/* const http headers stored in ROM */ +const static char http_html_hdr[] = "HTTP/1.1 200 OK\nContent-type: text/html\n\n"; +const static char http_css_hdr[] = "HTTP/1.1 200 OK\nContent-type: text/css\nCache-Control: public, max-age=31536000\n\n"; +const static char http_js_hdr[] = "HTTP/1.1 200 OK\nContent-type: text/javascript\n\n"; +const static char http_jquery_gz_hdr[] = "HTTP/1.1 200 OK\nContent-type: text/javascript\nAccept-Ranges: bytes\nContent-Length: 29995\nContent-Encoding: gzip\n\n"; +const static char http_400_hdr[] = "HTTP/1.1 400 Bad Request\nContent-Length: 0\n\n"; +const static char http_404_hdr[] = "HTTP/1.1 404 Not Found\nContent-Length: 0\n\n"; +const static char http_503_hdr[] = "HTTP/1.1 503 Service Unavailable\nContent-Length: 0\n\n"; +const static char http_ok_json_no_cache_hdr[] = "HTTP/1.1 200 OK\nContent-type: application/json\nCache-Control: no-store, no-cache, must-revalidate, max-age=0\nPragma: no-cache\n\n"; +const static char http_redirect_hdr_start[] = "HTTP/1.1 302 Found\nLocation: http://"; +const static char http_redirect_hdr_end[] = "/\n\n"; + + + +void http_server_start(){ + if(task_http_server == NULL){ + xTaskCreate(&http_server, "http_server", 1024*3, NULL, WIFI_MANAGER_TASK_PRIORITY-1, &task_http_server); + } +} + +void http_server(void *pvParameters) { + + struct netconn *conn, *newconn; + err_t err; + conn = netconn_new(NETCONN_TCP); + netconn_bind(conn, IP_ADDR_ANY, 80); + netconn_listen(conn); + ESP_LOGI(TAG, "HTTP Server listening on 80/tcp"); + do { + err = netconn_accept(conn, &newconn); + if (err == ERR_OK) { + http_server_netconn_serve(newconn); + netconn_delete(newconn); + } + else + { + ESP_LOGE(TAG,"Error accepting new connection. Terminating HTTP server"); + } + taskYIELD(); /* allows the freeRTOS scheduler to take over if needed. */ + } while(err == ERR_OK); + + netconn_close(conn); + netconn_delete(conn); + + vTaskDelete( NULL ); +} + + +char* http_server_get_header(char *request, char *header_name, int *len) { + *len = 0; + char *ret = NULL; + char *ptr = NULL; + + ptr = strstr(request, header_name); + if (ptr) { + ret = ptr + strlen(header_name); + ptr = ret; + while (*ptr != '\0' && *ptr != '\n' && *ptr != '\r') { + (*len)++; + ptr++; + } + return ret; + } + return NULL; +} + + +void http_server_netconn_serve(struct netconn *conn) { + + struct netbuf *inbuf; + char *buf = NULL; + u16_t buflen; + err_t err; + const char new_line[2] = "\n"; + + err = netconn_recv(conn, &inbuf); + if (err == ERR_OK) { + + netbuf_data(inbuf, (void**)&buf, &buflen); + + /* extract the first line of the request */ + char *save_ptr = buf; + char *line = strtok_r(save_ptr, new_line, &save_ptr); + ESP_LOGD(TAG,"Processing line %s",line); + + if(line) { + + /* captive portal functionality: redirect to access point IP for HOST that are not the access point IP OR the STA IP */ + int lenH = 0; + char *host = http_server_get_header(save_ptr, "Host: ", &lenH); + /* determine if Host is from the STA IP address */ + wifi_manager_lock_sta_ip_string(portMAX_DELAY); + bool access_from_sta_ip = lenH > 0?strstr(host, wifi_manager_get_sta_ip_string()):false; + wifi_manager_unlock_sta_ip_string(); + + if (lenH > 0 && !strstr(host, DEFAULT_AP_IP) && !access_from_sta_ip) { + ESP_LOGI(TAG,"Redirecting to default AP IP Address : %s", DEFAULT_AP_IP); + netconn_write(conn, http_redirect_hdr_start, sizeof(http_redirect_hdr_start) - 1, NETCONN_NOCOPY); + netconn_write(conn, DEFAULT_AP_IP, sizeof(DEFAULT_AP_IP) - 1, NETCONN_NOCOPY); + netconn_write(conn, http_redirect_hdr_end, sizeof(http_redirect_hdr_end) - 1, NETCONN_NOCOPY); + + } + else{ + /* default page */ + if(strstr(line, "GET / ")) { + netconn_write(conn, http_html_hdr, sizeof(http_html_hdr) - 1, NETCONN_NOCOPY); + netconn_write(conn, index_html_start, index_html_end - index_html_start, NETCONN_NOCOPY); + } + else if(strstr(line, "GET /jquery.js ")) { + netconn_write(conn, http_jquery_gz_hdr, sizeof(http_jquery_gz_hdr) - 1, NETCONN_NOCOPY); + netconn_write(conn, jquery_gz_start, jquery_gz_end - jquery_gz_start, NETCONN_NOCOPY); + } + else if(strstr(line, "GET /code.js ")) { + netconn_write(conn, http_js_hdr, sizeof(http_js_hdr) - 1, NETCONN_NOCOPY); + netconn_write(conn, code_js_start, code_js_end - code_js_start, NETCONN_NOCOPY); + } + else if(strstr(line, "GET /ap.json ")) { + /* if we can get the mutex, write the last version of the AP list */ + ESP_LOGI(TAG,"Processing ap.json request"); + if(wifi_manager_lock_json_buffer(( TickType_t ) 10)){ + netconn_write(conn, http_ok_json_no_cache_hdr, sizeof(http_ok_json_no_cache_hdr) - 1, NETCONN_NOCOPY); + char *buff = wifi_manager_get_ap_list_json(); + netconn_write(conn, buff, strlen(buff), NETCONN_NOCOPY); + wifi_manager_unlock_json_buffer(); + } + else{ + netconn_write(conn, http_503_hdr, sizeof(http_503_hdr) - 1, NETCONN_NOCOPY); + ESP_LOGE(TAG, "http_server_netconn_serve: GET /ap.json failed to obtain mutex"); + } + /* request a wifi scan */ + ESP_LOGI(TAG,"Starting wifi scan"); + wifi_manager_scan_async(); + } + else if(strstr(line, "GET /style.css ")) { + netconn_write(conn, http_css_hdr, sizeof(http_css_hdr) - 1, NETCONN_NOCOPY); + netconn_write(conn, style_css_start, style_css_end - style_css_start, NETCONN_NOCOPY); + } + else if(strstr(line, "GET /status.json ")){ + ESP_LOGI(TAG,"Serving status.json"); + if(wifi_manager_lock_json_buffer(( TickType_t ) 10)){ + char *buff = wifi_manager_get_ip_info_json(); + if(buff){ + netconn_write(conn, http_ok_json_no_cache_hdr, sizeof(http_ok_json_no_cache_hdr) - 1, NETCONN_NOCOPY); + netconn_write(conn, buff, strlen(buff), NETCONN_NOCOPY); + + wifi_manager_unlock_json_buffer(); + } + else{ + netconn_write(conn, http_503_hdr, sizeof(http_503_hdr) - 1, NETCONN_NOCOPY); + } + } + else{ + netconn_write(conn, http_503_hdr, sizeof(http_503_hdr) - 1, NETCONN_NOCOPY); + ESP_LOGE(TAG, "http_server_netconn_serve: GET /status failed to obtain mutex"); + } + } + else if(strstr(line, "GET /config.json ")){ + ESP_LOGI(TAG,"Serving config.json"); + char autoexec_name[21]={0}; + char * autoexec_value=NULL; + uint8_t autoexec_flag=0; + int buflen=MAX_COMMAND_LINE_SIZE+strlen(template)+1; + char * buff = malloc(buflen); + char *s = "\""; + char *r = "\\\""; + if(!buff) + { + ESP_LOGE(TAG,"Unable to allocate buffer for config.json!"); + netconn_write(conn, http_503_hdr, sizeof(http_503_hdr) - 1, NETCONN_NOCOPY); + } + else + { + int i=1; + size_t l = 0; + netconn_write(conn, http_ok_json_no_cache_hdr, sizeof(http_ok_json_no_cache_hdr) - 1, NETCONN_NOCOPY); + + autoexec_flag = wifi_manager_get_flag(); + snprintf(buff,buflen-1, json_start, autoexec_flag); + netconn_write(conn, buff, strlen(buff), NETCONN_NOCOPY); + do { + snprintf(autoexec_name,sizeof(autoexec_name)-1,"autoexec%u",i); + ESP_LOGD(TAG,"Getting command name %s", autoexec_name); + autoexec_value = wifi_manager_alloc_get_config(autoexec_name, &l); + if(autoexec_value!=NULL ){ + if(i>1) + { + netconn_write(conn, array_separator, strlen(array_separator), NETCONN_NOCOPY); + ESP_LOGD(TAG,"%s", array_separator); + } + ESP_LOGI(TAG,"found command %s = %s", autoexec_name, autoexec_value); + strreplace(autoexec_value, s, r); + snprintf(buff, buflen-1, template, autoexec_name, autoexec_value); + netconn_write(conn, buff, strlen(buff), NETCONN_NOCOPY); + ESP_LOGD(TAG,"%s", buff); + ESP_LOGD(TAG,"Freeing memory for command %s name", autoexec_name); + free(autoexec_value); + } + else { + ESP_LOGD(TAG,"No matching command found for name %s", autoexec_name); + break; + } + i++; + } while(1); + free(buff); + netconn_write(conn, json_end, strlen(json_end), NETCONN_NOCOPY); + ESP_LOGD(TAG,"%s", json_end); + } + } + else if(strstr(line, "POST /factory.json ")){ + guided_factory(); + } + else if(strstr(line, "POST /config.json ")){ + ESP_LOGI(TAG,"Serving POST config.json"); + + if(wifi_manager_lock_json_buffer(( TickType_t ) 10)){ + int i=1; + int lenS = 0, lenA=0; + char autoexec_name[22]={0}; + char autoexec_key[12]={0}; + char * autoexec_value=NULL; + char * autoexec_flag_s=NULL; + uint8_t autoexec_flag=0; + autoexec_flag_s = http_server_get_header(save_ptr, "X-Custom-autoexec: ", &lenA); + if(autoexec_flag_s!=NULL && lenA > 0) + { + autoexec_flag = atoi(autoexec_flag_s); + wifi_manager_save_autoexec_flag(autoexec_flag); + } + + do { + if(snprintf(autoexec_name,sizeof(autoexec_name)-1,"X-Custom-autoexec%u: ",i)<0) + { + ESP_LOGE(TAG,"Unable to process autoexec%u. Name length overflow.",i); + break; + } + if(snprintf(autoexec_key,sizeof(autoexec_key)-1,"autoexec%u",i++)<0) + { + ESP_LOGE(TAG,"Unable to process autoexec%u. Name length overflow.",i); + break; + } + ESP_LOGD(TAG,"Looking for command name %s.", autoexec_name); + autoexec_value = http_server_get_header(save_ptr, autoexec_name, &lenS); + + + if(autoexec_value ){ + if(lenS < MAX_COMMAND_LINE_SIZE ){ + ESP_LOGD(TAG, "http_server_netconn_serve: config.json/ call, with %s: %s, length %i", autoexec_key, autoexec_value, lenS); + wifi_manager_save_autoexec_config(autoexec_value,autoexec_key,lenS); + } + else + { + ESP_LOGE(TAG,"command line length is too long : %s = %s", autoexec_name, autoexec_value); + } + } + else { + ESP_LOGD(TAG,"No matching command found for name %s", autoexec_name); + break; + } + } while(1); + + netconn_write(conn, http_ok_json_no_cache_hdr, sizeof(http_ok_json_no_cache_hdr) - 1, NETCONN_NOCOPY); //200ok + + } + else{ + netconn_write(conn, http_503_hdr, sizeof(http_503_hdr) - 1, NETCONN_NOCOPY); + ESP_LOGE(TAG, "http_server_netconn_serve: GET /status failed to obtain mutex"); + } + } + + else if(strstr(line, "DELETE /connect.json ")) { + ESP_LOGI(TAG, "http_server_netconn_serve: DELETE /connect.json"); + /* request a disconnection from wifi and forget about it */ + wifi_manager_disconnect_async(); + netconn_write(conn, http_ok_json_no_cache_hdr, sizeof(http_ok_json_no_cache_hdr) - 1, NETCONN_NOCOPY); /* 200 ok */ + } + else if(strstr(line, "POST /connect.json ")) { + ESP_LOGI(TAG, "http_server_netconn_serve: POST /connect.json"); + bool found = false; + int lenS = 0, lenP = 0; + char *ssid = NULL, *password = NULL; + ssid = http_server_get_header(save_ptr, "X-Custom-ssid: ", &lenS); + password = http_server_get_header(save_ptr, "X-Custom-pwd: ", &lenP); + + if(ssid && lenS <= MAX_SSID_SIZE && password && lenP <= MAX_PASSWORD_SIZE){ + wifi_config_t* config = wifi_manager_get_wifi_sta_config(); + memset(config, 0x00, sizeof(wifi_config_t)); + memcpy(config->sta.ssid, ssid, lenS); + memcpy(config->sta.password, password, lenP); + ESP_LOGD(TAG, "http_server_netconn_serve: wifi_manager_connect_async() call, with ssid: %s, password: %s", ssid, password); + wifi_manager_connect_async(); + netconn_write(conn, http_ok_json_no_cache_hdr, sizeof(http_ok_json_no_cache_hdr) - 1, NETCONN_NOCOPY); //200ok + found = true; + } + + if(!found){ + /* bad request the authentification header is not complete/not the correct format */ + netconn_write(conn, http_400_hdr, sizeof(http_400_hdr) - 1, NETCONN_NOCOPY); + ESP_LOGE(TAG, "bad request the authentification header is not complete/not the correct format"); + } + + } + else{ + netconn_write(conn, http_400_hdr, sizeof(http_400_hdr) - 1, NETCONN_NOCOPY); + ESP_LOGE(TAG, "bad request"); + } + } + } + else{ + ESP_LOGE(TAG, "URL Not found. Sending 404."); + netconn_write(conn, http_404_hdr, sizeof(http_404_hdr) - 1, NETCONN_NOCOPY); + } + } + + /* free the buffer */ + netbuf_delete(inbuf); +} + +void strreplace(char *src, char *str, char *rep) +{ + char *p = strstr(src, str); + if (p) + { + int len = strlen(src)+strlen(rep)-strlen(str); + char r[len]; + memset(r, 0, len); + if ( p >= src ){ + strncpy(r, src, p-src); + r[p-src]='\0'; + strncat(r, rep, strlen(rep)); + strncat(r, p+strlen(str), p+strlen(str)-src+strlen(src)); + strcpy(src, r); + strreplace(p+strlen(rep), str, rep); + } + } +} + diff --git a/components/wifi-manager/http_server.h b/components/wifi-manager/http_server.h new file mode 100644 index 00000000..ec9afb97 --- /dev/null +++ b/components/wifi-manager/http_server.h @@ -0,0 +1,98 @@ +/* +Copyright (c) 2017-2019 Tony Pottier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +@file http_server.h +@author Tony Pottier +@brief Defines all functions necessary for the HTTP server to run. + +Contains the freeRTOS task for the HTTP listener and all necessary support +function to process requests, decode URLs, serve files, etc. etc. + +@note http_server task cannot run without the wifi_manager task! +@see https://idyl.io +@see https://github.com/tonyp7/esp32-wifi-manager +*/ + +#ifndef HTTP_SERVER_H_INCLUDED +#define HTTP_SERVER_H_INCLUDED +#include +#include +#include +#include +#include "wifi_manager.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/event_groups.h" +#include "esp_wifi.h" +#include "esp_event_loop.h" +#include "nvs_flash.h" +#include "esp_log.h" +#include "driver/gpio.h" +#include "mdns.h" +#include "lwip/api.h" +#include "lwip/err.h" +#include "lwip/netdb.h" +#include "lwip/opt.h" +#include "lwip/memp.h" +#include "lwip/ip.h" +#include "lwip/raw.h" +#include "lwip/udp.h" +#include "lwip/priv/api_msg.h" +#include "lwip/priv/tcp_priv.h" +#include "lwip/priv/tcpip_priv.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief RTOS task for the HTTP server. Do not start manually. + * @see void http_server_start() + */ +void http_server(void *pvParameters); + +/* @brief helper function that processes one HTTP request at a time */ +void http_server_netconn_serve(struct netconn *conn); + +/* @brief create the task for the http server */ +void http_server_start(); + +/** + * @brief gets a char* pointer to the first occurence of header_name withing the complete http request request. + * + * For optimization purposes, no local copy is made. memcpy can then be used in coordination with len to extract the + * data. + * + * @param request the full HTTP raw request. + * @param header_name the header that is being searched. + * @param len the size of the header value if found. + * @return pointer to the beginning of the header value. + */ +char* http_server_get_header(char *request, char *header_name, int *len); + +void strreplace(char *src, char *str, char *rep); + + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/components/wifi-manager/index.html b/components/wifi-manager/index.html new file mode 100644 index 00000000..e7cea9f3 --- /dev/null +++ b/components/wifi-manager/index.html @@ -0,0 +1,351 @@ + + + + + + + + + + esp32-wifi-manager + + + + + +
+
+
+
+

Startup command

+
+

+
+ +
+

+ +
+
+

Audio output

+ + + + +
+
+ +
+
+

Player name

+ +
+
+

Optional setting (e.g. for LMS IP address)

+ +
+ +
+ +
+ +

Command to run

+
+ +
+
+ +
+ +
+
+
+

Firmware upgrade

+
+ Upload Progress + + + + +
+
+ +
+
+

Wi-Fi

+
+
+

Connected to:

+
+
+
+
+

Manual connect

+
+
ADD (HIDDEN) SSID
+
+

or choose a network...

+
+
+
Powered by esp32-wifi-manager.
+
+
+
+

Enter Details

+
+

Manual Connection

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

Enter Password

+
+

Password for

+
+ +
+
+ + +
+
+
+
+

Please wait...

+
+

Connecting to

+
+
+
+

You may lose wifi access while the esp32 recalibrates its radio. Please wait until your device automatically reconnects. This can take up to 30s.

+
+
+

Success!

+
+
+

Connection failed

+

Please double-check wifi password if any and make sure the access point has good signal.

+
+
+
+ +
+
+
+
+
+

+
+

+
+
+ +
+
+

IP Address

+
+
IP Address:
+
Subnet Mask:
+
Default Gateway:
+
+
+ +
+
+
+
+

Are you sure you would like to disconnect from this wifi?

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

About this app...

+
+

+
+

esp32-wifi-manager, © 2017-2019, Tony Pottier
Licender under the MIT License.

+

+ This app would not be possible without the following libraries: +

+
    +
  • SpinKit, © 2015, Tobias Ahlin. Licensed under the MIT License.
  • +
  • jQuery, The jQuery Foundation. Licensed under the MIT License.
  • +
  • cJSON, © 2009-2017, Dave Gamble and cJSON contributors. Licensed under the MIT License.
  • +
+
+
+ +
+
+ + diff --git a/components/wifi-manager/jquery.gz b/components/wifi-manager/jquery.gz new file mode 100644 index 00000000..6d127872 Binary files /dev/null and b/components/wifi-manager/jquery.gz differ diff --git a/components/wifi-manager/jquery.js b/components/wifi-manager/jquery.js new file mode 100644 index 00000000..644d35e2 --- /dev/null +++ b/components/wifi-manager/jquery.js @@ -0,0 +1,4 @@ +/*! jQuery v3.2.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.2.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext;function B(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()}var C=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,D=/^.[^:#\[\.,]*$/;function E(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):D.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(E(this,a||[],!1))},not:function(a){return this.pushStack(E(this,a||[],!0))},is:function(a){return!!E(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var F,G=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,H=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||F,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:G.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),C.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};H.prototype=r.fn,F=r(d);var I=/^(?:parents|prev(?:Until|All))/,J={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function K(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return K(a,"nextSibling")},prev:function(a){return K(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return B(a,"iframe")?a.contentDocument:(B(a,"template")&&(a=a.content||a),r.merge([],a.childNodes))}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(J[a]||r.uniqueSort(e),I.test(a)&&e.reverse()),this.pushStack(e)}});var L=/[^\x20\t\r\n\f]+/g;function M(a){var b={};return r.each(a.match(L)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?M(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=e||a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function N(a){return a}function O(a){throw a}function P(a,b,c,d){var e;try{a&&r.isFunction(e=a.promise)?e.call(a).done(b).fail(c):a&&r.isFunction(e=a.then)?e.call(a,b,c):b.apply(void 0,[a].slice(d))}catch(a){c.apply(void 0,[a])}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==O&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:N,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:N)),c[2][3].add(g(0,a,r.isFunction(d)?d:O))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(P(a,g.done(h(c)).resolve,g.reject,!b),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)P(e[c],h(c),g.reject);return g.promise()}});var Q=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&Q.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var R=r.Deferred();r.fn.ready=function(a){return R.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||R.resolveWith(d,[r]))}}),r.ready.then=R.then;function S(){d.removeEventListener("DOMContentLoaded",S), +a.removeEventListener("load",S),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",S),a.addEventListener("load",S));var T=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)T(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){X.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=W.get(a,b),c&&(!d||Array.isArray(c)?d=W.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return W.get(a,c)||W.access(a,c,{empty:r.Callbacks("once memory").add(function(){W.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,la=/^$|\/(?:java|ecma)script/i,ma={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ma.optgroup=ma.option,ma.tbody=ma.tfoot=ma.colgroup=ma.caption=ma.thead,ma.th=ma.td;function na(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&B(a,b)?r.merge([a],c):c}function oa(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=na(l.appendChild(f),"script"),j&&oa(g),c){k=0;while(f=g[k++])la.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var ra=d.documentElement,sa=/^key/,ta=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ua=/^([^.]*)(?:\.(.+)|)/;function va(){return!0}function wa(){return!1}function xa(){try{return d.activeElement}catch(a){}}function ya(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ya(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=wa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(ra,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(L)||[""],j=b.length;while(j--)h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.hasData(a)&&W.get(a);if(q&&(i=q.events)){b=(b||"").match(L)||[""],j=b.length;while(j--)if(h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&W.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(W.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,Aa=/\s*$/g;function Ea(a,b){return B(a,"table")&&B(11!==b.nodeType?b:b.firstChild,"tr")?r(">tbody",a)[0]||a:a}function Fa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Ga(a){var b=Ca.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ha(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(W.hasData(a)&&(f=W.access(a),g=W.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&Ba.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ja(f,b,c,d)});if(m&&(e=qa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(na(e,"script"),Fa),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=na(h),f=na(a),d=0,e=f.length;d0&&oa(g,!i&&na(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(U(c)){if(b=c[W.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[W.expando]=void 0}c[X.expando]&&(c[X.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ka(this,a,!0)},remove:function(a){return Ka(this,a)},text:function(a){return T(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.appendChild(a)}})},prepend:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(na(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return T(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!Aa.test(a)&&!ma[(ka.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function _a(a,b,c,d,e){return new _a.prototype.init(a,b,c,d,e)}r.Tween=_a,_a.prototype={constructor:_a,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=_a.propHooks[this.prop];return a&&a.get?a.get(this):_a.propHooks._default.get(this)},run:function(a){var b,c=_a.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):_a.propHooks._default.set(this),this}},_a.prototype.init.prototype=_a.prototype,_a.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},_a.propHooks.scrollTop=_a.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=_a.prototype.init,r.fx.step={};var ab,bb,cb=/^(?:toggle|show|hide)$/,db=/queueHooks$/;function eb(){bb&&(d.hidden===!1&&a.requestAnimationFrame?a.requestAnimationFrame(eb):a.setTimeout(eb,r.fx.interval),r.fx.tick())}function fb(){return a.setTimeout(function(){ab=void 0}),ab=r.now()}function gb(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ca[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function hb(a,b,c){for(var d,e=(kb.tweeners[b]||[]).concat(kb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?lb:void 0)),void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b), +null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&B(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(L);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),lb={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=mb[b]||r.find.attr;mb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=mb[g],mb[g]=e,e=null!=c(a,b,d)?g:null,mb[g]=f),e}});var nb=/^(?:input|select|textarea|button)$/i,ob=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return T(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):nb.test(a.nodeName)||ob.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function pb(a){var b=a.match(L)||[];return b.join(" ")}function qb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,qb(this)))});if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,qb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,qb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(L)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=qb(this),b&&W.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":W.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+pb(qb(c))+" ").indexOf(b)>-1)return!0;return!1}});var rb=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":Array.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(rb,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:pb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(Array.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var sb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!sb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,sb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(W.get(h,"events")||{})[b.type]&&W.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&U(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!U(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=W.access(d,b);e||d.addEventListener(a,c,!0),W.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=W.access(d,b)-1;e?W.access(d,b,e):(d.removeEventListener(a,c,!0),W.remove(d,b))}}});var tb=a.location,ub=r.now(),vb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var wb=/\[\]$/,xb=/\r?\n/g,yb=/^(?:submit|button|image|reset|file)$/i,zb=/^(?:input|select|textarea|keygen)/i;function Ab(a,b,c,d){var e;if(Array.isArray(b))r.each(b,function(b,e){c||wb.test(a)?d(a,e):Ab(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)Ab(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(Array.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)Ab(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&zb.test(this.nodeName)&&!yb.test(a)&&(this.checked||!ja.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:Array.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(xb,"\r\n")}}):{name:b.name,value:c.replace(xb,"\r\n")}}).get()}});var Bb=/%20/g,Cb=/#.*$/,Db=/([?&])_=[^&]*/,Eb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Fb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Gb=/^(?:GET|HEAD)$/,Hb=/^\/\//,Ib={},Jb={},Kb="*/".concat("*"),Lb=d.createElement("a");Lb.href=tb.href;function Mb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(L)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Nb(a,b,c,d){var e={},f=a===Jb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Ob(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Pb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Qb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:tb.href,type:"GET",isLocal:Fb.test(tb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Kb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Ob(Ob(a,r.ajaxSettings),b):Ob(r.ajaxSettings,a)},ajaxPrefilter:Mb(Ib),ajaxTransport:Mb(Jb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Eb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||tb.href)+"").replace(Hb,tb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(L)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Lb.protocol+"//"+Lb.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Nb(Ib,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Gb.test(o.type),f=o.url.replace(Cb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(Bb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(vb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Db,"$1"),n=(vb.test(f)?"&":"?")+"_="+ub++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Kb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Nb(Jb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Pb(o,y,d)),v=Qb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Rb={0:200,1223:204},Sb=r.ajaxSettings.xhr();o.cors=!!Sb&&"withCredentials"in Sb,o.ajax=Sb=!!Sb,r.ajaxTransport(function(b){var c,d;if(o.cors||Sb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Rb[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r("