diff --git a/.cproject b/.cproject index 8637e407..bedc05f8 100644 --- a/.cproject +++ b/.cproject @@ -14,13 +14,13 @@ - + - + + + + + 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/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_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/bt_app_sink.c b/components/driver_bt/bt_app_sink.c index e9bb86ae..ef323f5a 100644 --- a/components/driver_bt/bt_app_sink.c +++ b/components/driver_bt/bt_app_sink.c @@ -21,7 +21,6 @@ #include "esp_gap_bt_api.h" #include "esp_a2dp_api.h" #include "esp_avrc_api.h" -#include "nvs.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" @@ -40,11 +39,9 @@ #define BT_RC_CT_TAG "RCCT" #ifndef CONFIG_BT_SINK_NAME -#define CONFIG_BT_SINK_NAME "default" +#define CONFIG_BT_SINK_NAME "unavailable" #endif -extern char current_namespace[]; - /* event for handler "bt_av_hdl_stack_up */ enum { BT_APP_EVT_STACK_UP = 0, @@ -355,7 +352,7 @@ static void bt_av_hdl_avrc_tg_evt(uint16_t event, void *p_param) } } -void bt_sink_init(bt_cmd_cb_t cmd_cb, bt_data_cb_t data_cb) +void bt_sink_init(void (*cmd_cb)(bt_sink_cmd_t cmd, ...), void (*data_cb)(const uint8_t *data, uint32_t len)) { esp_err_t err; @@ -412,21 +409,6 @@ void bt_sink_init(bt_cmd_cb_t cmd_cb, bt_data_cb_t data_cb) } -void bt_sink_deinit(void) -{ - /* this still does not work, can't figure out how to stop properly this BT stack */ - bt_app_task_shut_down(); - ESP_LOGI(BT_AV_TAG, "bt_app_task shutdown successfully"); - if (esp_bluedroid_disable() != ESP_OK) return; - ESP_LOGI(BT_AV_TAG, "esp_bluedroid_disable called successfully"); - if (esp_bluedroid_deinit() != ESP_OK) return; - ESP_LOGI(BT_AV_TAG, "esp_bluedroid_deinit called successfully"); - if (esp_bt_controller_disable() != ESP_OK) return; - ESP_LOGI(BT_AV_TAG, "esp_bt_controller_disable called successfully"); - if (esp_bt_controller_deinit() != ESP_OK) return; - ESP_LOGI(BT_AV_TAG, "bt stopped successfully"); -} - static void bt_app_gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t *param) { switch (event) { @@ -467,17 +449,9 @@ static void bt_av_hdl_stack_evt(uint16_t event, void *p_param) switch (event) { case BT_APP_EVT_STACK_UP: { /* set up device name */ - nvs_handle nvs; - char dev_name[32] = CONFIG_BT_SINK_NAME; - - if (nvs_open(current_namespace, NVS_READONLY, &nvs) == ESP_OK) { - size_t len = 31; - nvs_get_str(nvs, "bt_sink_name", dev_name, &len); - nvs_close(nvs); - } - - esp_bt_dev_set_device_name(dev_name); - + char *dev_name = CONFIG_BT_SINK_NAME; + esp_bt_dev_set_device_name(dev_name); + esp_bt_gap_register_callback(bt_app_gap_cb); /* initialize AVRCP controller */ diff --git a/components/driver_bt/bt_app_sink.h b/components/driver_bt/bt_app_sink.h index aa641bc8..9c84446e 100644 --- a/components/driver_bt/bt_app_sink.h +++ b/components/driver_bt/bt_app_sink.h @@ -13,23 +13,10 @@ typedef enum { BT_SINK_CONNECTED, BT_SINK_DISCONNECTED, BT_SINK_PLAY, BT_SINK_STOP, BT_SINK_PAUSE, BT_SINK_RATE, BT_SINK_VOLUME, } bt_sink_cmd_t; - -typedef void (*bt_cmd_cb_t)(bt_sink_cmd_t cmd, ...); -typedef void (*bt_data_cb_t)(const uint8_t *data, uint32_t len); /** * @brief init sink mode (need to be provided) */ -void bt_sink_init(bt_cmd_cb_t cmd_cb, bt_data_cb_t data_cb); - -/** - * @brief deinit sink mode (need to be provided) - */ -void bt_sink_deinit(void); - -/** - * @brief local command mode (stop, play, volume ...) - */ -void bt_sink_cmd(bt_sink_cmd_t event, ...); +void bt_sink_init(void (*cmd_cb)(bt_sink_cmd_t cmd, ...), void (*data_cb)(const uint8_t *data, uint32_t len)); #endif /* __BT_APP_SINK_H__*/ diff --git a/components/driver_bt/component.mk b/components/driver_bt/component.mk index e694d472..0c83faa9 100644 --- a/components/driver_bt/component.mk +++ b/components/driver_bt/component.mk @@ -7,6 +7,5 @@ # please read the SDK documents if you need to do this. # -CFLAGS += -I$(COMPONENT_PATH)/../tools - +CFLAGS += -I$(COMPONENT_PATH)/../tools #CFLAGS += -DLOG_LOCAL_LEVEL=ESP_LOG_DEBUG diff --git a/components/io/led.c b/components/io/led.c index 10f2b901..702db2cb 100644 --- a/components/io/led.c +++ b/components/io/led.c @@ -109,7 +109,6 @@ bool led_unconfig(int idx) { if (idx >= MAX_LED) return false; if (leds[idx].timer) xTimerDelete(leds[idx].timer, BLOCKTIME); - leds[idx].timer = NULL; return true; } 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/squeezelite/buffer.c b/components/squeezelite/buffer.c index 076fd990..ed71d811 100644 --- a/components/squeezelite/buffer.c +++ b/components/squeezelite/buffer.c @@ -64,11 +64,6 @@ void buf_flush(struct buffer *buf) { mutex_unlock(buf->mutex); } -void _buf_flush(struct buffer *buf) { - buf->readp = buf->buf; - buf->writep = buf->buf; -} - // adjust buffer to multiple of mod bytes so reading in multiple always wraps on frame boundary void buf_adjust(struct buffer *buf, size_t mod) { size_t size; @@ -83,7 +78,6 @@ void buf_adjust(struct buffer *buf, size_t mod) { // called with mutex locked to resize, does not retain contents, reverts to original size if fails void _buf_resize(struct buffer *buf, size_t size) { - if (size == buf->size) return; free(buf->buf); buf->buf = malloc(size); if (!buf->buf) { diff --git a/components/squeezelite/component.mk b/components/squeezelite/component.mk index 0d60996d..1d9abe13 100644 --- a/components/squeezelite/component.mk +++ b/components/squeezelite/component.mk @@ -13,8 +13,7 @@ CFLAGS += -O3 -DLINKALL -DLOOPBACK -DNO_FAAD -DRESAMPLE16 -DEMBEDDED -DTREMOR_ON -I$(COMPONENT_PATH)/../tools \ -I$(COMPONENT_PATH)/../codecs/inc/opus \ -I$(COMPONENT_PATH)/../codecs/inc/opusfile \ - -I$(COMPONENT_PATH)/../driver_bt \ - -I$(COMPONENT_PATH)/../raop + -I$(COMPONENT_PATH)/../driver_bt # -I$(COMPONENT_PATH)/../codecs/inc/faad2 diff --git a/components/squeezelite/decode.c b/components/squeezelite/decode.c index cc59f77a..333af5df 100644 --- a/components/squeezelite/decode.c +++ b/components/squeezelite/decode.c @@ -116,10 +116,6 @@ static void *decode_thread() { usleep(100000); } } - -#if EMBEDDED - deregister_external(); -#endif return 0; } @@ -204,7 +200,7 @@ void decode_init(log_level level, const char *include_codecs, const char *exclud sort_codecs((include_codecs ? order_codecs - include_codecs : i), register_mpg()); #if EMBEDDED - register_external(); + register_other(); #endif LOG_DEBUG("include codecs: %s exclude codecs: %s", include_codecs ? include_codecs : "", exclude_codecs); diff --git a/components/squeezelite/decode_bt.c b/components/squeezelite/decode_bt.c new file mode 100644 index 00000000..572b8860 --- /dev/null +++ b/components/squeezelite/decode_bt.c @@ -0,0 +1,141 @@ +/* + * Squeezelite for esp32 + * + * (c) Sebastien 2019 + * Philippe G. 2019, philippe_44@outlook.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "squeezelite.h" +#include "bt_app_sink.h" + +#define LOCK_O mutex_lock(outputbuf->mutex) +#define UNLOCK_O mutex_unlock(outputbuf->mutex) +#define LOCK_D mutex_lock(decode.mutex); +#define UNLOCK_D mutex_unlock(decode.mutex); + +extern struct outputstate output; +extern struct decodestate decode; +extern struct buffer *outputbuf; +// this is the only system-wide loglevel variable +extern log_level loglevel; + +/**************************************************************************************** + * BT sink data handler + */ +static void bt_sink_data_handler(const uint8_t *data, uint32_t len) +{ + size_t bytes; + + // would be better to lock decoder, but really, it does not matter + if (decode.state != DECODE_STOPPED) { + LOG_WARN("Cannot use BT sink while LMS is controlling player"); + return; + } + + // there will always be room at some point + while (len) { + LOCK_O; + + bytes = min(len, _buf_cont_write(outputbuf)); +#if BYTES_PER_FRAME == 4 + memcpy(outputbuf->writep, data, bytes); +#else + { + s16_t *iptr = (s16_t*) data; + ISAMPLE_T *optr = (ISAMPLE_T*) outputbuf->writep; + size_t n = bytes / BYTES_PER_FRAME * 2; + while (n--) *optr++ = *iptr++ << 16; + } +#endif + _buf_inc_writep(outputbuf, bytes); + len -= bytes; + data += bytes; + + UNLOCK_O; + + // allow i2s to empty the buffer if needed + if (len) usleep(50000); + } +} + +/**************************************************************************************** + * BT sink command handler + */ +static void bt_sink_cmd_handler(bt_sink_cmd_t cmd, ...) +{ + va_list args; + + LOCK_D; + if (decode.state != DECODE_STOPPED) { + LOG_WARN("Cannot use BT sink while LMS is controlling player"); + UNLOCK_D; + return; + } + + va_start(args, cmd); + + if (cmd != BT_SINK_VOLUME) LOCK_O; + + switch(cmd) { + case BT_SINK_CONNECTED: + output.state = OUTPUT_STOPPED; + LOG_INFO("BT sink started"); + break; + case BT_SINK_DISCONNECTED: + output.state = OUTPUT_OFF; + LOG_INFO("BT sink stopped"); + break; + case BT_SINK_PLAY: + output.state = OUTPUT_EXTERNAL; + LOG_INFO("BT sink playing"); + break; + case BT_SINK_PAUSE: + case BT_SINK_STOP: + output.state = OUTPUT_STOPPED; + LOG_INFO("BT sink stopped"); + break; + case BT_SINK_RATE: + output.current_sample_rate = va_arg(args, u32_t); + LOG_INFO("Setting BT sample rate %u", output.current_sample_rate); + break; + case BT_SINK_VOLUME: { + u16_t volume = (u16_t) va_arg(args, u32_t); + volume *= 65536 / 128; + set_volume(volume, volume); + break; + } + } + + if (cmd != BT_SINK_VOLUME) UNLOCK_O; + UNLOCK_D; + + va_end(args); +} + +/**************************************************************************************** + * We provide the generic codec register option + */ +void register_other(void) { +#ifdef CONFIG_BT_SINK + if (!strcasestr(output.device, "BT ")) { + bt_sink_init(bt_sink_cmd_handler, bt_sink_data_handler); + LOG_INFO("Initializing BT sink"); + } else { + LOG_WARN("Cannot be a BT sink and source"); + } +#endif +} diff --git a/components/squeezelite/embedded.h b/components/squeezelite/embedded.h index 8db425ab..f96c6c24 100644 --- a/components/squeezelite/embedded.h +++ b/components/squeezelite/embedded.h @@ -19,10 +19,10 @@ #define PTHREAD_STACK_MIN 256 #endif -#define STREAM_THREAD_STACK_SIZE 6 * 1024 -#define DECODE_THREAD_STACK_SIZE 16 * 1024 -#define OUTPUT_THREAD_STACK_SIZE 6 * 1024 -#define IR_THREAD_STACK_SIZE 6 * 1024 +#define STREAM_THREAD_STACK_SIZE 8 * 1024 +#define DECODE_THREAD_STACK_SIZE 20 * 1024 +#define OUTPUT_THREAD_STACK_SIZE 8 * 1024 +#define IR_THREAD_STACK_SIZE 8 * 1024 //#define BASE_CAP "Model=squeezelite,AccuratePlayPoints=0,HasDigitalOut=1,HasPolarityInversion=1,Firmware=" VERSION @@ -36,13 +36,9 @@ typedef unsigned long long u64_t; #define gettime_ms _gettime_ms_ #define mutex_create_p(m) mutex_create(m) -uint32_t _gettime_ms_(void); - -int pthread_create_name(pthread_t *thread, _CONST pthread_attr_t *attr, +uint32_t _gettime_ms_(void); +int pthread_create_name(pthread_t *thread, _CONST pthread_attr_t *attr, void *(*start_routine)( void * ), void *arg, char *name); - -// these are here as they can be #define to nothing -void register_external(void); -void deregister_external(void); +void register_other(void); #endif // EMBEDDED_H diff --git a/components/squeezelite/opus.c b/components/squeezelite/opus.c index f20ebe42..73cd86a0 100644 --- a/components/squeezelite/opus.c +++ b/components/squeezelite/opus.c @@ -139,7 +139,7 @@ static decode_state opus_decompress(void) { info = OP(u, head, u->of, -1); LOCK_O; - output.next_sample_rate = decode_newstream(48000, output.supported_rates); + output.next_sample_rate = 48000; IF_DSD( output.next_fmt = PCM; ) output.track_start = outputbuf->writep; if (output.fade_mode) _checkfade(true); diff --git a/components/squeezelite/output.c b/components/squeezelite/output.c index f41a032b..e79c5c6d 100644 --- a/components/squeezelite/output.c +++ b/components/squeezelite/output.c @@ -345,9 +345,8 @@ void output_init_common(log_level level, const char *device, unsigned output_buf unsigned i; loglevel = level; - + output_buf_size = output_buf_size - (output_buf_size % BYTES_PER_FRAME); - output.init_size = output_buf_size; LOG_DEBUG("outputbuf size: %u", output_buf_size); buf_init(outputbuf, output_buf_size); diff --git a/components/squeezelite/output_bt.c b/components/squeezelite/output_bt.c index 15101d76..d7080e21 100644 --- a/components/squeezelite/output_bt.c +++ b/components/squeezelite/output_bt.c @@ -19,7 +19,6 @@ * */ -#include "driver/gpio.h" #include "squeezelite.h" #include "perf_trace.h" @@ -39,12 +38,10 @@ extern u8_t *silencebuf; extern void hal_bluetooth_init(const char * options); extern void hal_bluetooth_stop(void); -extern u8_t config_spdif_gpio; static log_level loglevel; static bool running = false; -static uint8_t *btout; -static frames_t oframes; +uint8_t * btout; static int _write_frames(frames_t out_frames, bool silence, s32_t gainL, s32_t gainR, s32_t cross_gain_in, s32_t cross_gain_out, ISAMPLE_T **cross_ptr); @@ -68,11 +65,6 @@ static int _write_frames(frames_t out_frames, bool silence, s32_t gainL, s32_t g DECLARE_ALL_MIN_MAX; void output_init_bt(log_level level, char *device, unsigned output_buf_size, char *params, unsigned rates[], unsigned rate_delay, unsigned idle) { -#ifdef CONFIG_SQUEEZEAMP - gpio_pad_select_gpio(config_spdif_gpio); - gpio_set_direction(config_spdif_gpio, GPIO_MODE_OUTPUT); - gpio_set_level(config_spdif_gpio, 0); -#endif loglevel = level; running = true; output.write_cb = &_write_frames; @@ -102,12 +94,12 @@ static int _write_frames(frames_t out_frames, bool silence, s32_t gainL, s32_t g } #if BYTES_PER_FRAME == 4 - memcpy(btout + oframes * BYTES_PER_FRAME, outputbuf->readp, out_frames * BYTES_PER_FRAME); + memcpy(btout, outputbuf->readp, out_frames * BYTES_PER_FRAME); #else { frames_t count = out_frames; s32_t *_iptr = (s32_t*) outputbuf->readp; - s16_t *_optr = (s16_t*) (btout + oframes * BYTES_PER_FRAME); + s16_t *_optr = (s16_t*) bt_optr; while (count--) { *_optr++ = *_iptr++ >> 16; *_optr++ = *_iptr++ >> 16; @@ -118,7 +110,7 @@ static int _write_frames(frames_t out_frames, bool silence, s32_t gainL, s32_t g } else { u8_t *buf = silencebuf; - memcpy(btout + oframes * BYTES_PER_FRAME, buf, out_frames * BYTES_PER_FRAME); + memcpy(btout, buf, out_frames * BYTES_PER_FRAME); } return (int)out_frames; @@ -132,7 +124,6 @@ int32_t output_bt_data(uint8_t *data, int32_t len) { } btout = data; - oframes = 0; // This is how the BTC layer calculates the number of bytes to // for us to send. (BTC_SBC_DEC_PCM_DATA_LEN * sizeof(OI_INT16) - availPcmBytes @@ -152,7 +143,6 @@ int32_t output_bt_data(uint8_t *data, int32_t len) { if (wanted_len > 0) { SET_MIN_MAX(wanted_len, under); } - output.frames_in_process = len-wanted_len; UNLOCK; SET_MIN_MAX(TIME_MEASUREMENT_GET(start_timer),lock_out_time); diff --git a/components/squeezelite/output_i2s.c b/components/squeezelite/output_i2s.c index e4b8ed20..f8f2132b 100644 --- a/components/squeezelite/output_i2s.c +++ b/components/squeezelite/output_i2s.c @@ -41,7 +41,6 @@ sure that using rate_delay would fix that */ #include "squeezelite.h" -#include "esp_pthread.h" #include "driver/i2s.h" #include "driver/i2c.h" #include "driver/gpio.h" @@ -85,9 +84,9 @@ sure that using rate_delay would fix that typedef enum { DAC_ON = 0, DAC_OFF, DAC_POWERDOWN, DAC_VOLUME } dac_cmd_e; -// must have an integer ratio with FRAME_BLOCK (see spdif comment) +// must have an integer ratio with FRAME_BLOCK #define DMA_BUF_LEN 512 -#define DMA_BUF_COUNT 12 +#define DMA_BUF_COUNT 16 #define DECLARE_ALL_MIN_MAX \ DECLARE_MIN_MAX(o); \ @@ -116,9 +115,7 @@ static i2s_config_t i2s_config; static int bytes_per_frame; static thread_type thread, stats_thread; static u8_t *obuf; -static frames_t oframes; static bool spdif; -static size_t dma_buf_frames; DECLARE_ALL_MIN_MAX; @@ -154,14 +151,12 @@ static void spdif_convert(ISAMPLE_T *src, size_t frames, u32_t *dst, size_t *cou #define I2C_PORT 0 #define I2C_ADDR 0x4c #define VOLUME_GPIO 33 -#define JACK_GPIO 34 +#define JACK_GPIO 39 struct tas575x_cmd_s { u8_t reg; u8_t value; }; - -u8_t config_spdif_gpio = CONFIG_SPDIF_DO_IO; static const struct tas575x_cmd_s tas575x_init_sequence[] = { { 0x00, 0x00 }, // select page 0 @@ -197,9 +192,9 @@ void output_init_i2s(log_level level, char *device, unsigned output_buf_size, ch #ifdef TAS575x gpio_pad_select_gpio(JACK_GPIO); gpio_set_direction(JACK_GPIO, GPIO_MODE_INPUT); - + adc1_config_width(ADC_WIDTH_BIT_12); - adc1_config_channel_atten(ADC1_CHANNEL_7, ADC_ATTEN_DB_0); + adc1_config_channel_atten(ADC1_CHANNEL_0,ADC_ATTEN_DB_0); // init volume & mute gpio_pad_select_gpio(VOLUME_GPIO); @@ -271,37 +266,27 @@ void output_init_i2s(log_level level, char *device, unsigned output_buf_size, ch }; i2s_config.sample_rate = output.current_sample_rate * 2; i2s_config.bits_per_sample = 32; - // Normally counted in frames, but 16 sample are transformed into 32 bits in spdif - i2s_config.dma_buf_len = DMA_BUF_LEN / 2; - i2s_config.dma_buf_count = DMA_BUF_COUNT * 2; - /* - In DMA, we have room for (LEN * COUNT) frames of 32 bits samples that - we push at sample_rate * 2. Each of these peuso-frames is a single true - audio frame. So the real depth is true frames is (LEN * COUNT / 2) - */ - dma_buf_frames = DMA_BUF_COUNT * DMA_BUF_LEN / 2; } else { pin_config = (i2s_pin_config_t) { .bck_io_num = CONFIG_I2S_BCK_IO, .ws_io_num = CONFIG_I2S_WS_IO, .data_out_num = CONFIG_I2S_DO_IO, .data_in_num = -1 //Not used }; i2s_config.sample_rate = output.current_sample_rate; i2s_config.bits_per_sample = bytes_per_frame * 8 / 2; - // Counted in frames (but i2s allocates a buffer <= 4092 bytes) - i2s_config.dma_buf_len = DMA_BUF_LEN; - i2s_config.dma_buf_count = DMA_BUF_COUNT; - dma_buf_frames = DMA_BUF_COUNT * DMA_BUF_LEN; -#ifdef TAS575x +#ifdef TAS575x gpio_pad_select_gpio(CONFIG_SPDIF_DO_IO); gpio_set_direction(CONFIG_SPDIF_DO_IO, GPIO_MODE_OUTPUT); gpio_set_level(CONFIG_SPDIF_DO_IO, 0); #endif } - + i2s_config.mode = I2S_MODE_MASTER | I2S_MODE_TX; i2s_config.channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT; i2s_config.communication_format = I2S_COMM_FORMAT_I2S| I2S_COMM_FORMAT_I2S_MSB; // in case of overflow, do not replay old buffer i2s_config.tx_desc_auto_clear = true; + i2s_config.dma_buf_count = DMA_BUF_COUNT; + // Counted in frames (but i2s allocates a buffer <= 4092 bytes) + i2s_config.dma_buf_len = DMA_BUF_LEN; i2s_config.use_apll = true; i2s_config.intr_alloc_flags = ESP_INTR_FLAG_LEVEL1; //Interrupt level 1 @@ -316,21 +301,15 @@ void output_init_i2s(log_level level, char *device, unsigned output_buf_size, ch isI2SStarted=false; dac_cmd(DAC_OFF); + + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setstacksize(&attr, PTHREAD_STACK_MIN + OUTPUT_THREAD_STACK_SIZE); + pthread_create_name(&thread, &attr, output_thread_i2s, NULL, "output_i2s"); + pthread_attr_destroy(&attr); - esp_pthread_cfg_t cfg = esp_pthread_get_default_config(); - - cfg.thread_name= "output_i2s"; - cfg.inherit_cfg = false; - cfg.prio = CONFIG_ESP32_PTHREAD_TASK_PRIO_DEFAULT + 1; - cfg.stack_size = PTHREAD_STACK_MIN + OUTPUT_THREAD_STACK_SIZE; - esp_pthread_set_cfg(&cfg); - pthread_create(&thread, NULL, output_thread_i2s, NULL); - - cfg.thread_name= "output_i2s_sts"; - cfg.prio = CONFIG_ESP32_PTHREAD_TASK_PRIO_DEFAULT - 1; - cfg.stack_size = 2048; - esp_pthread_set_cfg(&cfg); - pthread_create(&stats_thread, NULL, output_thread_i2s_stats, NULL); + // leave stack size to default + pthread_create_name(&stats_thread, NULL, output_thread_i2s_stats, NULL, "output_i2s_sts"); } @@ -382,13 +361,13 @@ static int _i2s_write_frames(frames_t out_frames, bool silence, s32_t gainL, s32 _apply_gain(outputbuf, out_frames, gainL, gainR); } - memcpy(obuf + oframes * bytes_per_frame, outputbuf->readp, out_frames * bytes_per_frame); + memcpy(obuf, outputbuf->readp, out_frames * bytes_per_frame); #else optr = (s32_t*) outputbuf->readp; #endif } else { #if BYTES_PER_FRAME == 4 - memcpy(obuf + oframes * bytes_per_frame, silencebuf, out_frames * bytes_per_frame); + memcpy(obuf, silencebuf, out_frames * bytes_per_frame); #else optr = (s32_t*) silencebuf; #endif @@ -402,20 +381,19 @@ static int _i2s_write_frames(frames_t out_frames, bool silence, s32_t gainL, s32 dsd_invert((u32_t *) optr, out_frames); ) - _scale_and_pack_frames(obuf + oframes * bytes_per_frame, optr, out_frames, gainL, gainR, output.format); + _scale_and_pack_frames(obuf, optr, out_frames, gainL, gainR, output.format); #endif - oframes += out_frames; - return out_frames; } + /**************************************************************************************** * Main output thread */ static void *output_thread_i2s() { size_t count = 0, bytes; - frames_t iframes = FRAME_BLOCK; + frames_t iframes = FRAME_BLOCK, oframes; uint32_t timer_start = 0; int discard = 0; uint32_t fullness = gettime_ms(); @@ -426,14 +404,14 @@ static void *output_thread_i2s() { // spdif needs 16 bytes per frame : 32 bits/sample, 2 channels, BMC encoded if (spdif && (sbuf = malloc(FRAME_BLOCK * 16)) == NULL) { LOG_ERROR("Cannot allocate SPDIF buffer"); - } + } while (running) { TIME_MEASUREMENT_START(timer_start); LOCK; - + // manage led display if (state != output.state) { LOG_INFO("Output state is %d", output.state); @@ -457,15 +435,12 @@ static void *output_thread_i2s() { synced = false; } - oframes = 0; output.updated = gettime_ms(); output.frames_played_dmp = output.frames_played; - // try to estimate how much we have consumed from the DMA buffer (calculation is incorrect at the very beginning ...) - output.device_frames = dma_buf_frames - ((output.updated - fullness) * output.current_sample_rate) / 1000; - _output_frames( iframes ); - // oframes must be a global updated by the write callback - output.frames_in_process = oframes; - + // try to estimate how much we have consumed from the DMA buffer + output.device_frames = DMA_BUF_COUNT * DMA_BUF_LEN - ((output.updated - fullness) * output.current_sample_rate) / 1000; + oframes = _output_frames( iframes ); + SET_MIN_MAX_SIZED(oframes,rec,iframes); SET_MIN_MAX_SIZED(_buf_used(outputbuf),o,outputbuf->size); SET_MIN_MAX_SIZED(_buf_used(streambuf),s,streambuf->size); @@ -526,7 +501,7 @@ static void *output_thread_i2s() { if (bytes != oframes * bytes_per_frame) { LOG_WARN("I2S DMA Overflow! available bytes: %d, I2S wrote %d bytes", oframes * bytes_per_frame, bytes); } - + SET_MIN_MAX( TIME_MEASUREMENT_GET(timer_start),i2s_time); } @@ -542,7 +517,7 @@ static void *output_thread_i2s() { static void *output_thread_i2s_stats() { while (running) { #ifdef TAS575x - LOG_ERROR("Jack %d Voltage %.2fV", !gpio_get_level(JACK_GPIO), adc1_get_raw(ADC1_CHANNEL_7) / 4095. * (10+174)/10. * 1.1); + LOG_ERROR("Jack %d Voltage %.2fV", !gpio_get_level(JACK_GPIO), adc1_get_raw(ADC1_CHANNEL_0) / 4095. * (10+169)/10. * 1.1); #endif LOCK; output_state state = output.state; @@ -567,11 +542,6 @@ static void *output_thread_i2s_stats() { LOG_INFO(" ----------+----------+-----------+-----------+"); RESET_ALL_MIN_MAX; } - LOG_INFO("Heap internal:%zu (min:%zu) external:%zu (min:%zu)", - heap_caps_get_free_size(MALLOC_CAP_INTERNAL), - heap_caps_get_minimum_free_size(MALLOC_CAP_INTERNAL), - heap_caps_get_free_size(MALLOC_CAP_SPIRAM), - heap_caps_get_minimum_free_size(MALLOC_CAP_SPIRAM)); usleep(STATS_PERIOD_MS *1000); } return NULL; diff --git a/components/squeezelite/slimproto.c b/components/squeezelite/slimproto.c index 42fb73b0..e301d13e 100644 --- a/components/squeezelite/slimproto.c +++ b/components/squeezelite/slimproto.c @@ -371,8 +371,6 @@ static void process_strm(u8_t *pkt, int len) { sendSTAT("STMc", 0); sentSTMu = sentSTMo = sentSTMl = false; LOCK_O; - output.external = false; - _buf_resize(outputbuf, output.init_size); output.threshold = strm->output_threshold; output.next_replay_gain = unpackN(&strm->replay_gain); output.fade_mode = strm->transition_type - '0'; @@ -631,6 +629,7 @@ static void slimproto_run() { #endif last = now; + LOCK_S; status.stream_full = _buf_used(streambuf); status.stream_size = streambuf->size; @@ -689,7 +688,7 @@ static void slimproto_run() { status.current_sample_rate = output.current_sample_rate; status.updated = output.updated; status.device_frames = output.device_frames; - + if (output.track_started) { _sendSTMs = true; output.track_started = false; @@ -704,7 +703,7 @@ static void slimproto_run() { if (_start_output && (output.state == OUTPUT_STOPPED || output.state == OUTPUT_OFF)) { output.state = OUTPUT_BUFFER; } - if (!output.external && output.state == OUTPUT_RUNNING && !sentSTMu && status.output_full == 0 && status.stream_state <= DISCONNECT && + if (output.state == OUTPUT_RUNNING && !sentSTMu && status.output_full == 0 && status.stream_state <= DISCONNECT && _decode_state == DECODE_STOPPED) { _sendSTMu = true; @@ -722,7 +721,7 @@ static void slimproto_run() { output.state = OUTPUT_OFF; LOG_DEBUG("output timeout"); } - if (!output.external && output.state == OUTPUT_RUNNING && now - status.last > 1000) { + if (output.state == OUTPUT_RUNNING && now - status.last > 1000) { _sendSTMt = true; status.last = now; } diff --git a/components/squeezelite/squeezelite.h b/components/squeezelite/squeezelite.h index 8de70146..b51247a3 100644 --- a/components/squeezelite/squeezelite.h +++ b/components/squeezelite/squeezelite.h @@ -539,7 +539,6 @@ unsigned _buf_cont_write(struct buffer *buf); void _buf_inc_readp(struct buffer *buf, unsigned by); void _buf_inc_writep(struct buffer *buf, unsigned by); void buf_flush(struct buffer *buf); -void _buf_flush(struct buffer *buf); void buf_adjust(struct buffer *buf, size_t mod); void _buf_resize(struct buffer *buf, size_t size); void buf_init(struct buffer *buf, size_t size); @@ -635,7 +634,7 @@ bool resample_init(char *opt); // output.c output_alsa.c output_pa.c output_pack.c typedef enum { OUTPUT_OFF = -1, OUTPUT_STOPPED = 0, OUTPUT_BUFFER, OUTPUT_RUNNING, - OUTPUT_PAUSE_FRAMES, OUTPUT_SKIP_FRAMES, OUTPUT_START_AT } output_state; + OUTPUT_PAUSE_FRAMES, OUTPUT_SKIP_FRAMES, OUTPUT_START_AT, OUTPUT_EXTERNAL } output_state; #if DSD typedef enum { PCM, DOP, DSD_U8, DSD_U16_LE, DSD_U32_LE, DSD_U16_BE, DSD_U32_BE, DOP_S24_LE, DOP_S24_3LE } dsd_format; @@ -655,8 +654,6 @@ struct outputstate { output_state state; output_format format; const char *device; - bool external; - u32_t init_size; #if ALSA unsigned buffer; unsigned period; @@ -676,7 +673,6 @@ struct outputstate { unsigned default_sample_rate; bool error_opening; unsigned device_frames; - unsigned frames_in_process; u32_t updated; u32_t track_start_time; u32_t current_replay_gain; diff --git a/components/wifi-manager/CMakeLists.txt b/components/wifi-manager/CMakeLists.txt new file mode 100644 index 00000000..0b155304 --- /dev/null +++ b/components/wifi-manager/CMakeLists.txt @@ -0,0 +1,11 @@ +set(COMPONENT_ADD_INCLUDEDIRS .) + +set(COMPONENT_SRCS "dns_server.c" "http_server.c" "json.c" "wifi_manager.c") +set(REQUIRES esp_common) +set(COMPONENT_EMBED_FILES "style.css jquery.gz code.js index.html") + +set(REQUIRES_COMPONENTS freertos ) + +register_component() + + 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..158a33f8 --- /dev/null +++ b/components/wifi-manager/code.js @@ -0,0 +1,454 @@ +// 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 checkConfigInterval = null; + +var StatusIntervalActive = false; +var ConfigIntervalActive = false; +var RefreshAPIIntervalActive = false; + + +function stopCheckStatusInterval(){ + if(checkStatusInterval != null){ + clearTimeout(checkStatusInterval); + checkStatusInterval = null; + } + StatusIntervalActive = false; +} +function stopCheckConfigInterval(){ + if(checkConfigInterval != null){ + clearTimeout(checkConfigInterval); + checkConfigInterval = null; + } + ConfigIntervalActive=false; +} + +function stopRefreshAPInterval(){ + + if(refreshAPInterval != null){ + clearTimeout(refreshAPInterval); + refreshAPInterval = null; + } + RefreshAPIIntervalActive = false; +} + + +function startCheckStatusInterval(){ + StatusIntervalActive = true; + checkStatusInterval = setTimeout(checkStatus, 950); +} +function startCheckConfigInterval(){ + ConfigIntervalActive = true; + checkConfigInterval = setTimeout(checkConfig, 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() {}); + + }); + $("#update").on("click", function() { + + performUpdate(); + }); + $("#factory").on("click", function() { + + performFactory(); + }); + + $("#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() {}) + }); + + + + + + + + + //first time the page loads: attempt get the connection status and start the wifi scan + refreshAP(); + startCheckStatusInterval(); + startRefreshAPInterval(); + startCheckConfigInterval(); + + + + +}); + + +function performUpdate(){ + autoexec1 = $("#autoexec1").val(); + //reset connection +// +// $( "#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: '/config.json', + dataType: 'json', + method: 'POST', + cache: false, + headers: { 'X-Custom-autoexec1': autoexec1 }, + data: { 'timestamp': Date.now()} + }); + + +} + +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()} + }); + + +} + + +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 checkConfig(){ + var h = ""; + //{ "autoexec" : 0, "list" : [{ 'autoexec1' : 'squeezelite -o "I2S" -b 500:2000 -d all=info -M esp32' }]} + $.getJSON( "/config.json", function( data ) { + if(data.hasOwnProperty('autoexec')) { + h+= '
Autoexec: {0}
'.format(data["autoexec"]===1?"Active":"Inactive"); + } + if(data.hasOwnProperty('list')) { + data["list"].forEach(function(e, idx, array) { + for (const [key, value] of Object.entries(e)) { + h+= '
'.format(key,value); + } + } + + ); + h += "\n"; + $( "#command-list" ).html(h); + } + + }) + .fail(function() { + //don't do anything, the server might be down while esp32 recalibrates radio + }); + + RepeatCheckConfigInterval(); + +} 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..38fc5147 --- /dev/null +++ b/components/wifi-manager/http_server.c @@ -0,0 +1,367 @@ +/* +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; + char * autoexec_flag_s=NULL; + uint8_t autoexec_flag=0; + int buflen=MAX_COMMAND_LINE_SIZE+strlen(template)+1; + char * buff = malloc(buflen); + 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); + 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[21]={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 { + snprintf(autoexec_name,sizeof(autoexec_name)-1,"X-Custom-autoexec%u:",i++); + 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", autoexec_name, autoexec_value); + wifi_manager_save_autoexec_config(autoexec_value,autoexec_name,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); +} diff --git a/components/wifi-manager/http_server.h b/components/wifi-manager/http_server.h new file mode 100644 index 00000000..3b4c2419 --- /dev/null +++ b/components/wifi-manager/http_server.h @@ -0,0 +1,95 @@ +/* +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); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/components/wifi-manager/index.html b/components/wifi-manager/index.html new file mode 100644 index 00000000..7c976d6f --- /dev/null +++ b/components/wifi-manager/index.html @@ -0,0 +1,310 @@ + + + + + + + + + + esp32-wifi-manager + + + +
+
+
+
+

Wi-Fi

+
+
+

Connected to:

+
+
+
+
+

Manual connect

+
+
ADD (HIDDEN) SSID
+
+

or choose a network...

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

Startup command

+
+

Squeezelite

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

Application

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

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.
  • +
+
+
+ +
+
+ + \ No newline at end of file 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("