├── LICENSE ├── PCB ├── LEDLamp_pcb.png └── LEDLamp_schematics.png ├── README.md ├── SmartLEDLamp ├── App.cpp ├── App.h ├── DemoReelVisualizer.cpp ├── DemoReelVisualizer.h ├── FadeAndScrollVisualizer.cpp ├── FadeAndScrollVisualizer.h ├── FireVisualizer.cpp ├── FireVisualizer.h ├── LEDLamp.cpp ├── LEDLamp.h ├── LEDMatrix.cpp ├── LEDMatrix.h ├── Log.cpp ├── Log.h ├── NoiseWithPaletteVisualizer.cpp ├── NoiseWithPaletteVisualizer.h ├── Runnable.cpp ├── Runnable.h ├── RuntimeConfigurable.h ├── SmartLEDLamp.ino ├── TurnOffRunnable.cpp ├── TurnOffRunnable.h ├── TurnOnRunnable.cpp ├── TurnOnRunnable.h ├── VUMeterVisualizer.cpp ├── VUMeterVisualizer.h ├── Visualizer.cpp ├── Visualizer.h ├── VisualizerApp.cpp ├── VisualizerApp.h ├── data │ ├── config.html │ ├── control.html │ ├── jquery-3.1.1.min.js │ ├── rc.jpg │ └── style.css └── defines.h └── assets ├── lamp_bluesky.png ├── lamp_deepsea.png ├── lamp_fire.png ├── lamp_green.png ├── lamp_onfire.gif ├── lamp_purpledream.png ├── lamp_rainbow.png ├── lamp_red.png ├── lamp_tentacleblobs.png ├── lamp_white.png ├── lamp_whiteblack.png ├── makingof01.png ├── makingof02.png ├── makingof03.png ├── makingof04.png ├── makingof05.png ├── makingof06.png ├── makingof07.png ├── makingof08.png └── makingof09.png /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 ghmartin77 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /PCB/LEDLamp_pcb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghmartin77/SmartLEDLamp/47147e4ed04f02911dcf0874428b4aee80b621df/PCB/LEDLamp_pcb.png -------------------------------------------------------------------------------- /PCB/LEDLamp_schematics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ghmartin77/SmartLEDLamp/47147e4ed04f02911dcf0874428b4aee80b621df/PCB/LEDLamp_schematics.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SmartLEDLamp 2 | 3 | A smart, web-enabled floor lamp with nice visual effects based on IKEA Vidja powered by ESP8266. 4 | IR or http controlled. 5 | 6 | # Looks like this (some examples): 7 | 8 | ![White Lamp](assets/lamp_white.png) ![Red Lamp](assets/lamp_red.png) ![Green Lamp](assets/lamp_green.png) ![Rainbow Lamp](assets/lamp_rainbow.png) ![Fire Lamp](assets/lamp_fire.png) ![Blue Sky Lamp](assets/lamp_bluesky.png) ![Deep Sea Lamp](assets/lamp_deepsea.png) ![Purple Dream Lamp](assets/lamp_purpledream.png) ![Tentacle Blobs Lamp](assets/lamp_tentacleblobs.png) ![White Black Lamp](assets/lamp_whiteblack.png) 9 | 10 | Animation example (fire) 11 | 12 | ![On Fire Lamp](assets/lamp_onfire.gif) 13 | 14 | 15 | # Making of 16 | 17 | ![Step 1](assets/makingof01.png) 18 | 19 | ![Step 2](assets/makingof02.png) 20 | 21 | ![Step 3](assets/makingof03.png) 22 | 23 | ![Step 4](assets/makingof04.png) 24 | 25 | ![Step 5](assets/makingof05.png) 26 | 27 | ![Step 6](assets/makingof06.png) 28 | 29 | ![Step 7](assets/makingof07.png) 30 | 31 | ![Step 8](assets/makingof08.png) 32 | 33 | ![Step 9](assets/makingof09.png) 34 | 35 | ![PCB](PCB/LEDLamp_pcb.png) 36 | 37 | 38 | # Ingredients 39 | * IKEA Vidja 40 | * ESP8266 (Wemos D1 or a similar variant with 4MB flash memory) 41 | * 5m WS2801 (160 LEDs) 42 | * Power Supply 5V, 10A 43 | * Cable straps, resistors, capacitors, duct tape, few hours spare time... 44 | * (optional) IR Sensor (e.g. VS1838) plus Mini led controller 44 ([search aliexpress](https://de.aliexpress.com/wholesale?SearchText=remote+control+44)) 45 | 46 | # How to Build 47 | * Make sure to use a memory layout with 3M SPIFFS 48 | * The following libraries are needed: 49 | * From Arduino core for ESP8266: 50 | * ArduinoOTA 51 | * DNSServer 52 | * EEPROM 53 | * ESP8266mDNS 54 | * ESP8266WebServer 55 | * ESP8266WiFi 56 | * Hash 57 | * 3rd party: 58 | * ArduinoJson - https://github.com/bblanchon/ArduinoJson 59 | * FastLED - https://github.com/FastLED/FastLED 60 | * WebSockets - https://github.com/Links2004/arduinoWebSockets 61 | * WiFiManager - https://github.com/tzapu/WiFiManager 62 | * ArtnetWifi (optional, check for ARTNET_ENABLE in defines.h to activate) - https://github.com/rstephan/ArtnetWifi (to use http://www.solderlab.de/index.php/software/glediator for example) 63 | * IRremoteESP8266 (optional, only if you want to use an IR receiver; If so, check for IR_ENABLE in defines.h to activate) - https://github.com/markszabo/IRremoteESP8266 64 | * PubSubClient (optional, check for MQTT_ENABLE in defines.h to activate) - https://github.com/knolleary/pubsubclient 65 | 66 | * Compile & upload 67 | 68 | # First Start 69 | * After initial upload ESP8266 is running in AP mode offering a WiFi AP connection called `SmartLEDLampAP`. 70 | * Connect to that WiFi and open http://192.168.4.1 in your browser if it isn't shown automatically. 71 | * Configure WiFi by supplying WiFi name plus password, save, wait a minute and restart the ESP8266. 72 | * If connect data was right, ESP8266 is now accessible from your local network. IP is shown in the console as logging output: 73 | ``` 74 | Info: Starting Smart LED Lamp 0.2 75 | Info: Free Sketch Space: 622592 76 | Info: No config file found, using defaults 77 | Info: IP: 192.168.0.110 78 | Info: HTTP server started 79 | Info: WebSocket server started 80 | ``` 81 | * Now build and upload SPIFFS. In folder SmartLEDLamp run 82 | ``` 83 | mkspiffs -p 256 -b 8192 -s 3125248 -c data spiffs-image.bin 84 | ``` 85 | and 86 | ``` 87 | espota.py -i -s -f spiffs-image.bin 88 | ``` 89 | Don't worry if the latter comes back with an error after upload. 90 | * Alternatively you can use [ArduinoIDE Plugin for SPIFFS upload](https://github.com/esp8266/arduino-esp8266fs-plugin). 91 | * Restart ESP8266 92 | 93 | # Usage 94 | * More often than not LED stripes won't be plain white when R, G and B are fully powered, thus you may want to calibrate your stripes. Go to http://<IP>/config.html to apply and save your calibration (Tip: this is done most easily turning on the lamp with fully brightness and white color.) 95 | * You can access a web remote control page at http://<IP>/control.html to access a web control page showing the remote (please also refer to [data/rc.jpg](data/rc.jpg)) 96 | * Most buttons are self-explaining. Here's just the special stuff: 97 | * 3rd button in the first row allows to start and stop animations 98 | * JUMP3 - Rainbow animation - Try QUICK/SLOW and the blue up/down arrows 99 | * JUMP7 - Fire animation - Try QUICK/SLOW, different up/down arrows (fire parameters), DIY1-6/AUTO (choose color palette) 100 | * FLUSH - FastLED noise animation - Try QUICK/SLOW, blue up/down arrows (scale), red up/down arrows (choose color palette) 101 | * FADE3 - FastLED DemoReel animations - Try red up/down arrows 102 | * FADE7 - VU Meter - commented out in the code. You can run a microphone or line in on A0 of the ESP8266 to drive the VU Meter. You'll have to poke around with the code to make this work. Look for `readAnalogPeek` and the code calling it in `loop` in LEDLamp.cpp. 103 | * Remote logging is available via port 8888. You could use PuTTY to connect to your lamp's IP, port 8888 using connection type raw and you will receive logging output like key presses and so on. 104 | 105 | # Home Automation Integration 106 | The following web API is available to integrate the SmartLEDLamp into OpenHAB for example: 107 | ``` 108 | http:///action/?act=on - Turn the lamp on 109 | http:///action/?act=off - Turn the lamp off 110 | 111 | http:///action/?btn=1 - Simulate press button 1 112 | http:///action/?btn=17 - Simulate press button 17 113 | 114 | http:///action/?brightness=17 - set brightness to 17% 115 | http:///action/?brightness=100 - set brightness to 100% 116 | ``` 117 | Additionally you can use MQTT (see above on how to enable, configure MQTT_SERVER and MQTT_TOPIC in defines.h appropriately). 118 | Message of "0" sent to the topic turns the lamp off, a message of "1" turns it on. Furthermore same topic is reflecting the state if lamp is turned on or off via web interface or IR. 119 | -------------------------------------------------------------------------------- /SmartLEDLamp/App.cpp: -------------------------------------------------------------------------------- 1 | #include "App.h" 2 | 3 | App::App(uint8_t appId, LEDMatrix* pLEDMatrix) : 4 | id(appId), pMatrix(pLEDMatrix), name("Unknown") { 5 | 6 | } 7 | 8 | App::~App() { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /SmartLEDLamp/App.h: -------------------------------------------------------------------------------- 1 | #ifndef APP_H_ 2 | #define APP_H_ 3 | 4 | #include "LEDMatrix.h" 5 | #include "RuntimeConfigurable.h" 6 | 7 | class App: public virtual RuntimeConfigurable { 8 | public: 9 | App(uint8_t appId, LEDMatrix* pLEDMatrix); 10 | virtual ~App(); 11 | 12 | virtual void start() { 13 | } 14 | 15 | virtual void stop() { 16 | } 17 | 18 | virtual boolean onButtonPressed(uint8_t button) { 19 | return false; 20 | } 21 | 22 | virtual void run() = 0; 23 | virtual void update() = 0; 24 | 25 | const char* getName() const { 26 | return name; 27 | } 28 | 29 | const uint8_t getId() const { 30 | return id; 31 | } 32 | 33 | protected: 34 | const uint8_t id; 35 | const char* name; 36 | LEDMatrix *pMatrix; 37 | }; 38 | 39 | #endif /* APP_H_ */ 40 | -------------------------------------------------------------------------------- /SmartLEDLamp/DemoReelVisualizer.cpp: -------------------------------------------------------------------------------- 1 | #include "DemoReelVisualizer.h" 2 | #include 3 | #include "Log.h" 4 | 5 | DemoReelVisualizer::DemoReelVisualizer() { 6 | } 7 | 8 | DemoReelVisualizer::~DemoReelVisualizer() { 9 | } 10 | 11 | void DemoReelVisualizer::computeImage() { 12 | EVERY_N_MILLISECONDS(20) { 13 | hue++; 14 | } 15 | 16 | switch (currentPatternNumber) { 17 | case 0: 18 | confetti(); 19 | break; 20 | case 1: 21 | sinelon(); 22 | break; 23 | case 2: 24 | bpm(); 25 | break; 26 | default: 27 | juggle(); 28 | break; 29 | } 30 | 31 | for (int i = 0; i < LEDS_HEIGHT; ++i) 32 | for (int x = 0; x < LEDS_WIDTH; ++x) { 33 | imageData[((i) * LEDS_WIDTH + x) * 3 + 0] = leds[i].r; 34 | imageData[((i) * LEDS_WIDTH + x) * 3 + 1] = leds[i].g; 35 | imageData[((i) * LEDS_WIDTH + x) * 3 + 2] = leds[i].b; 36 | } 37 | } 38 | 39 | void DemoReelVisualizer::confetti() { 40 | fadeToBlackBy(leds, LEDS_HEIGHT, 10); 41 | int pos = random16(LEDS_HEIGHT); 42 | leds[pos] += CHSV(hue + random8(64), 200, 255); 43 | } 44 | 45 | void DemoReelVisualizer::rainbow() { 46 | fill_rainbow(leds, LEDS_HEIGHT, hue, 7); 47 | } 48 | 49 | void DemoReelVisualizer::sinelon() { 50 | fadeToBlackBy(leds, LEDS_HEIGHT, 20); 51 | int pos = beatsin16(16, 0, LEDS_HEIGHT); 52 | CHSV chsv = CHSV(hue, 255, 192); 53 | 54 | if (lastPos != -1) { 55 | for (int i = min(lastPos, pos); i < max(lastPos,pos); ++i) 56 | { 57 | leds[i] += chsv; 58 | } 59 | } 60 | 61 | lastPos = pos; 62 | } 63 | 64 | void DemoReelVisualizer::bpm() { 65 | // colored stripes pulsing at a defined Beats-Per-Minute (BPM) 66 | uint8_t BeatsPerMinute = 62; 67 | CRGBPalette16 palette = PartyColors_p; 68 | uint8_t beat = beatsin8(BeatsPerMinute, 64, 255); 69 | for (int i = 0; i < LEDS_HEIGHT; i++) { 70 | leds[i] = ColorFromPalette(palette, hue + (i * 2), 71 | beat - hue + (i * 10)); 72 | } 73 | } 74 | 75 | void DemoReelVisualizer::juggle() { 76 | fadeToBlackBy(leds, LEDS_HEIGHT, 20); 77 | byte dothue = 0; 78 | for (int i = 0; i < 3; i++) { 79 | leds[beatsin16(i + 8, 0, LEDS_HEIGHT)] |= CHSV(dothue, 200, 255); 80 | dothue += 32; 81 | } 82 | } 83 | 84 | void DemoReelVisualizer::start() { 85 | randomSeed(micros()); 86 | } 87 | 88 | void DemoReelVisualizer::stop() { 89 | } 90 | 91 | boolean DemoReelVisualizer::onButtonPressed(uint8_t button) { 92 | boolean handled = false; 93 | 94 | switch (button) { 95 | case BTN_RED_UP: 96 | handled = true; 97 | currentPatternNumber += 1; 98 | if (currentPatternNumber > 5) 99 | currentPatternNumber = 5; 100 | else 101 | delay(200); 102 | break; 103 | case BTN_RED_DOWN: 104 | handled = true; 105 | currentPatternNumber -= 1; 106 | if (currentPatternNumber < 0) 107 | currentPatternNumber = 0; 108 | else 109 | delay(200); 110 | break; 111 | } 112 | 113 | return handled; 114 | } 115 | 116 | void DemoReelVisualizer::readRuntimeConfiguration(int &address) { 117 | Logger.debug("DemoReelVisualizer::readRuntimeConfiguration"); 118 | Visualizer::readRuntimeConfiguration(address); 119 | EEPROM.get(address, currentPatternNumber); 120 | address += sizeof(currentPatternNumber); 121 | } 122 | 123 | void DemoReelVisualizer::writeRuntimeConfiguration(int &address) { 124 | Logger.debug("DemoReelVisualizer::writeRuntimeConfiguration"); 125 | Visualizer::writeRuntimeConfiguration(address); 126 | EEPROM.write(address, currentPatternNumber); 127 | address += sizeof(currentPatternNumber); 128 | } 129 | 130 | -------------------------------------------------------------------------------- /SmartLEDLamp/DemoReelVisualizer.h: -------------------------------------------------------------------------------- 1 | #ifndef DEMOREELVISUALIZER_H_ 2 | #define DEMOREELVISUALIZER_H_ 3 | 4 | #define FASTLED_INTERNAL 5 | 6 | #include "Visualizer.h" 7 | #include "FastLED.h" 8 | #include "defines.h" 9 | 10 | class DemoReelVisualizer: public Visualizer { 11 | public: 12 | DemoReelVisualizer(); 13 | virtual ~DemoReelVisualizer(); 14 | 15 | virtual void start(); 16 | virtual void stop(); 17 | 18 | virtual boolean onButtonPressed(uint8_t button); 19 | 20 | virtual void readRuntimeConfiguration(int &address); 21 | virtual void writeRuntimeConfiguration(int &address); 22 | 23 | protected: 24 | virtual void computeImage(); 25 | 26 | private: 27 | int8_t currentPatternNumber = 0; 28 | uint8_t hue = 0; 29 | int lastPos = -1; 30 | 31 | CRGB leds[LEDS_HEIGHT]; 32 | 33 | void rainbow(); 34 | void confetti(); 35 | void sinelon(); 36 | void bpm(); 37 | void juggle(); 38 | }; 39 | 40 | #endif /* DEMOREELVISUALIZER_H_ */ 41 | -------------------------------------------------------------------------------- /SmartLEDLamp/FadeAndScrollVisualizer.cpp: -------------------------------------------------------------------------------- 1 | #include "FadeAndScrollVisualizer.h" 2 | #include "defines.h" 3 | #include 4 | #include "Log.h" 5 | 6 | FadeAndScrollVisualizer::FadeAndScrollVisualizer(uint8_t _fsSpeed, 7 | uint8_t _fsZoom) : 8 | fsSpeed(_fsSpeed), fsZoom(_fsZoom), pFsHeight(NULL), pFsLut(NULL), fsIndex( 9 | 0), fsHeightOffset(0) { 10 | } 11 | 12 | FadeAndScrollVisualizer::~FadeAndScrollVisualizer() { 13 | } 14 | 15 | void FadeAndScrollVisualizer::computeImage() { 16 | for (int x = 0; x < LEDS_WIDTH; x++) { 17 | for (int y = 0; y < LEDS_HEIGHT; y++) { 18 | fsIndex = y * LEDS_WIDTH + x; 19 | pFsHeight[fsIndex] += fsSpeed; 20 | if (pFsHeight[fsIndex] >= 1536) { 21 | pFsHeight[fsIndex] = pFsHeight[fsIndex] - 1536; 22 | } 23 | imageData[fsIndex * 3 + 0] = pFsLut[pFsHeight[fsIndex] * 3 + 0]; 24 | imageData[fsIndex * 3 + 1] = pFsLut[pFsHeight[fsIndex] * 3 + 1]; 25 | imageData[fsIndex * 3 + 2] = pFsLut[pFsHeight[fsIndex] * 3 + 2]; 26 | } 27 | } 28 | } 29 | 30 | void FadeAndScrollVisualizer::updateFsHeight() { 31 | for (int x = 0; x < LEDS_WIDTH; x++) { 32 | for (int y = 0; y < LEDS_HEIGHT; y++) { 33 | fsIndex = y * LEDS_WIDTH + x; 34 | pFsHeight[fsIndex] = (fsHeightOffset + (y * fsZoom)) % 1536; 35 | } 36 | } 37 | } 38 | 39 | void FadeAndScrollVisualizer::start() { 40 | fsIndex = 0; 41 | 42 | pFsLut = (uint8_t*) malloc(sizeof(uint8_t) * 3 * 1536); 43 | pFsHeight = (int16_t*) malloc(sizeof(int16_t) * LEDS_WIDTH * LEDS_HEIGHT); 44 | 45 | for (int i = 0; i < 256; ++i) { 46 | pFsLut[(i + 0) * 3 + 0] = 255; 47 | pFsLut[(i + 0) * 3 + 1] = i; 48 | pFsLut[(i + 0) * 3 + 2] = 0; 49 | 50 | pFsLut[(i + 256) * 3 + 0] = 255 - i; 51 | pFsLut[(i + 256) * 3 + 1] = 255; 52 | pFsLut[(i + 256) * 3 + 2] = 0; 53 | 54 | pFsLut[(i + 512) * 3 + 0] = 0; 55 | pFsLut[(i + 512) * 3 + 1] = 255; 56 | pFsLut[(i + 512) * 3 + 2] = i; 57 | 58 | pFsLut[(i + 768) * 3 + 0] = 0; 59 | pFsLut[(i + 768) * 3 + 1] = 255 - i; 60 | pFsLut[(i + 768) * 3 + 2] = 255; 61 | 62 | pFsLut[(i + 1024) * 3 + 0] = i; 63 | pFsLut[(i + 1024) * 3 + 1] = 0; 64 | pFsLut[(i + 1024) * 3 + 2] = 255; 65 | 66 | pFsLut[(i + 1280) * 3 + 0] = 255; 67 | pFsLut[(i + 1280) * 3 + 1] = 0; 68 | pFsLut[(i + 1280) * 3 + 2] = 255 - i; 69 | } 70 | 71 | updateFsHeight(); 72 | computeImage(); 73 | } 74 | 75 | void FadeAndScrollVisualizer::stop() { 76 | if (pFsLut) 77 | free(pFsLut); 78 | if (pFsHeight) 79 | free(pFsHeight); 80 | } 81 | 82 | boolean FadeAndScrollVisualizer::onButtonPressed(uint8_t button) { 83 | boolean handled = false; 84 | 85 | switch (button) { 86 | case BTN_QUICK: 87 | handled = true; 88 | fsSpeed += 1; 89 | if (fsSpeed >= 40) 90 | fsSpeed = 40; 91 | break; 92 | case BTN_SLOW: 93 | handled = true; 94 | fsSpeed -= 1; 95 | if (fsSpeed <= 1) 96 | fsSpeed = 1; 97 | break; 98 | 99 | case BTN_BLUE_UP: 100 | handled = true; 101 | fsZoom -= 5; 102 | if (fsZoom <= 1) 103 | fsZoom = 1; 104 | else 105 | updateFsHeight(); 106 | break; 107 | case BTN_BLUE_DOWN: 108 | handled = true; 109 | fsZoom += 5; 110 | if (fsZoom >= 250) 111 | fsZoom = 250; 112 | else 113 | updateFsHeight(); 114 | break; 115 | } 116 | 117 | return handled; 118 | } 119 | 120 | void FadeAndScrollVisualizer::readRuntimeConfiguration(int &address) { 121 | Logger.debug("FadeAndScrollVisualizer::readRuntimeConfiguration"); 122 | Visualizer::readRuntimeConfiguration(address); 123 | EEPROM.get(address, fsSpeed); 124 | address += sizeof(fsSpeed); 125 | EEPROM.get(address, fsZoom); 126 | address += sizeof(fsZoom); 127 | EEPROM.get(address, fsHeightOffset); 128 | address += sizeof(fsHeightOffset); 129 | 130 | // adjust offset to cope for modification in computeImage() 131 | fsHeightOffset -= fsSpeed; 132 | if (fsHeightOffset < 0) { 133 | fsHeightOffset += 1536; 134 | } 135 | 136 | } 137 | 138 | void FadeAndScrollVisualizer::writeRuntimeConfiguration(int &address) { 139 | Logger.debug("FadeAndScrollVisualizer::writeRuntimeConfiguration"); 140 | 141 | fsHeightOffset = 0; 142 | 143 | if (pFsHeight) { 144 | fsHeightOffset = pFsHeight[0]; 145 | } 146 | 147 | Visualizer::writeRuntimeConfiguration(address); 148 | EEPROM.put(address, fsSpeed); 149 | address += sizeof(fsSpeed); 150 | EEPROM.put(address, fsZoom); 151 | address += sizeof(fsZoom); 152 | EEPROM.put(address, fsHeightOffset); 153 | address += sizeof(fsHeightOffset); 154 | } 155 | 156 | -------------------------------------------------------------------------------- /SmartLEDLamp/FadeAndScrollVisualizer.h: -------------------------------------------------------------------------------- 1 | #ifndef FADEANDSCROLLVISUALIZER_H_ 2 | #define FADEANDSCROLLVISUALIZER_H_ 3 | 4 | #include "Visualizer.h" 5 | 6 | class FadeAndScrollVisualizer: public Visualizer { 7 | public: 8 | FadeAndScrollVisualizer(uint8_t _fsSpeed, uint8_t _fsZoom); 9 | virtual ~FadeAndScrollVisualizer(); 10 | 11 | virtual void start(); 12 | virtual void stop(); 13 | 14 | virtual boolean onButtonPressed(uint8_t button); 15 | 16 | virtual void readRuntimeConfiguration(int &address); 17 | virtual void writeRuntimeConfiguration(int &address); 18 | 19 | protected: 20 | virtual void computeImage(); 21 | 22 | private: 23 | int8_t fsSpeed; 24 | int16_t fsZoom; 25 | int16_t fsHeightOffset; 26 | uint16_t fsIndex; 27 | int16_t* pFsHeight; 28 | uint8_t* pFsLut; 29 | 30 | void updateFsHeight(); 31 | }; 32 | 33 | #endif /* FADEANDSCROLLVISUALIZER_H_ */ 34 | -------------------------------------------------------------------------------- /SmartLEDLamp/FireVisualizer.cpp: -------------------------------------------------------------------------------- 1 | #include "FireVisualizer.h" 2 | #include 3 | #include "Log.h" 4 | 5 | FireVisualizer::FireVisualizer() : 6 | cooling(95), sparking(200), paletteNo(0), paletteMax(240) { 7 | } 8 | 9 | FireVisualizer::~FireVisualizer() { 10 | } 11 | 12 | void FireVisualizer::computeImage() { 13 | random16_add_entropy(random(-1)); 14 | 15 | switch (rendererId) { 16 | case 0: 17 | computeImage3(); 18 | break; 19 | case 1: 20 | computeImage1(); 21 | break; 22 | case 2: 23 | computeImage2(); 24 | break; 25 | } 26 | } 27 | 28 | void FireVisualizer::computeImage1() { 29 | int8_t leds = LEDS_HEIGHT * 2; 30 | 31 | for (int x = 0; x < LEDS_WIDTH; x++) { 32 | 33 | // Step 1. Cool down every cell a little 34 | for (int i = 0; i < leds; i++) { 35 | heat[x][i] = qsub8(heat[x][i], 36 | random8(0, ((cooling * 10.0f) / leds) + 2)); 37 | } 38 | 39 | // Step 2. Heat from each cell drifts 'up' and diffuses a little 40 | for (int k = leds - 1; k >= 2; k--) { 41 | heat[x][k] = (heat[x][k - 1] + heat[x][k - 2] + heat[x][k - 2]) / 3; 42 | } 43 | 44 | // Step 3. Randomly ignite new 'sparks' of heat near the bottom 45 | if (random8() < sparking) { 46 | int y = random8(7); 47 | heat[x][y] = qadd8(heat[x][y], random8(160, 255)); 48 | } 49 | 50 | // Step 4. Map from heat cells to LED colors 51 | for (int j = 0; j < leds; j++) { 52 | // Scale the heat value from 0-255 down to 0-240 53 | // for best results with color palettes. 54 | byte colorindex = scale8(heat[x][j], paletteMax); 55 | CRGB color = ColorFromPalette(palette[paletteNo], colorindex); 56 | int pixelnumber; 57 | pixelnumber = (leds - 1) - j; 58 | int pixelIdx = ((pixelnumber) * LEDS_WIDTH + x) * 3; 59 | vImageData[pixelIdx + 0] = color.r; 60 | vImageData[pixelIdx + 1] = color.g; 61 | vImageData[pixelIdx + 2] = color.b; 62 | } 63 | 64 | for (int j = 0; j < LEDS_HEIGHT; j++) { 65 | int16_t rgbIndex = (j * LEDS_WIDTH + x) * 3; 66 | int16_t r, g, b; 67 | r = vImageData[rgbIndex * 2 + 0]; 68 | g = vImageData[rgbIndex * 2 + 1]; 69 | b = vImageData[rgbIndex * 2 + 2]; 70 | 71 | if (j > 0) { 72 | r += vImageData[rgbIndex * 2 + 0 - 12]; 73 | g += vImageData[rgbIndex * 2 + 1 - 12]; 74 | b += vImageData[rgbIndex * 2 + 2 - 12]; 75 | } 76 | 77 | r += vImageData[rgbIndex * 2 + 0 + 12]; 78 | g += vImageData[rgbIndex * 2 + 1 + 12]; 79 | b += vImageData[rgbIndex * 2 + 2 + 12]; 80 | 81 | if (j > 0) { 82 | r = r / 3; 83 | g = g / 3; 84 | b = b / 3; 85 | } else { 86 | r = r / 2; 87 | g = g / 2; 88 | b = b / 2; 89 | } 90 | 91 | if (r > 255) 92 | r = 255; 93 | if (g > 255) 94 | g = 255; 95 | if (b > 255) 96 | b = 255; 97 | 98 | imageData[rgbIndex + 0] = r; 99 | imageData[rgbIndex + 1] = g; 100 | imageData[rgbIndex + 2] = b; 101 | } 102 | } 103 | } 104 | 105 | void FireVisualizer::computeImage2() { 106 | int8_t leds = LEDS_HEIGHT * 2; 107 | 108 | for (int x = 0; x < LEDS_WIDTH; x++) { 109 | 110 | // Step 1. Cool down every cell a little 111 | for (int i = 0; i < leds; i++) { 112 | heat[x][i] = qsub8(heat[x][i], 113 | random8(0, ((cooling * 10) / leds) + 2)); 114 | } 115 | 116 | // Step 2. Heat from each cell drifts 'up' and diffuses a little 117 | for (int k = leds - 1; k >= 2; k--) { 118 | heat[x][k] = (heat[x][k - 1] + heat[x][k - 2] + heat[x][k - 2]) / 3; 119 | } 120 | 121 | // Step 3. Randomly ignite new 'sparks' of heat near the bottom 122 | if (random8() < sparking) { 123 | int y = random8(7); 124 | heat[x][y] = qadd8(heat[x][y], random8(160, 255)); 125 | } 126 | 127 | // Step 4. Map from heat cells to LED colors 128 | for (int j = 0; j < leds; j++) { 129 | // Scale the heat value from 0-255 down to 0-240 130 | // for best results with color palettes. 131 | byte colorindex = scale8(heat[x][j], paletteMax); 132 | CRGB color = ColorFromPalette(palette[paletteNo], colorindex); 133 | int pixelnumber; 134 | pixelnumber = (leds - 1) - j; 135 | int pixelIdx = ((pixelnumber) * LEDS_WIDTH + x) * 3; 136 | vImageData[pixelIdx + 0] = color.r; 137 | vImageData[pixelIdx + 1] = color.g; 138 | vImageData[pixelIdx + 2] = color.b; 139 | } 140 | 141 | for (int j = 0; j < LEDS_HEIGHT; j++) { 142 | int16_t rgbIndex = (j * LEDS_WIDTH + x) * 3; 143 | int16_t r, g, b; 144 | r = vImageData[rgbIndex * 2 + 0]; 145 | g = vImageData[rgbIndex * 2 + 1]; 146 | b = vImageData[rgbIndex * 2 + 2]; 147 | 148 | if (j > 0) { 149 | r += vImageData[rgbIndex * 2 + 0 - 12] / 2; 150 | g += vImageData[rgbIndex * 2 + 1 - 12] / 2; 151 | b += vImageData[rgbIndex * 2 + 2 - 12] / 2; 152 | } 153 | 154 | r += vImageData[rgbIndex * 2 + 0 + 12] / 2; 155 | g += vImageData[rgbIndex * 2 + 1 + 12] / 2; 156 | b += vImageData[rgbIndex * 2 + 2 + 12] / 2; 157 | 158 | if (r > 255) 159 | r = 255; 160 | if (g > 255) 161 | g = 255; 162 | if (b > 255) 163 | b = 255; 164 | 165 | imageData[rgbIndex + 0] = r; 166 | imageData[rgbIndex + 1] = g; 167 | imageData[rgbIndex + 2] = b; 168 | } 169 | } 170 | } 171 | 172 | void FireVisualizer::computeImage3() { 173 | int8_t leds = LEDS_HEIGHT; 174 | 175 | for (int x = 0; x < LEDS_WIDTH; x++) { 176 | 177 | // Step 1. Cool down every cell a little 178 | for (int i = 0; i < leds; i++) { 179 | heat[x][i] = qsub8(heat[x][i], 180 | random8(0, ((cooling * 10) / leds) + 2)); 181 | } 182 | 183 | // Step 2. Heat from each cell drifts 'up' and diffuses a little 184 | for (int k = leds - 1; k >= 2; k--) { 185 | heat[x][k] = (heat[x][k - 1] + heat[x][k - 2] + heat[x][k - 2]) / 3; 186 | } 187 | 188 | // Step 3. Randomly ignite new 'sparks' of heat near the bottom 189 | if (random8() < sparking) { 190 | int y = random8(7); 191 | heat[x][y] = qadd8(heat[x][y], random8(160, 255)); 192 | } 193 | 194 | // Step 4. Map from heat cells to LED colors 195 | for (int j = 0; j < leds; j++) { 196 | // Scale the heat value from 0-255 down to 0-240 197 | // for best results with color palettes. 198 | byte colorindex = scale8(heat[x][j], paletteMax); 199 | CRGB color = ColorFromPalette(palette[paletteNo], colorindex); 200 | int pixelnumber; 201 | pixelnumber = (leds - 1) - j; 202 | int pixelIdx = ((pixelnumber) * LEDS_WIDTH + x) * 3; 203 | imageData[pixelIdx + 0] = color.r; 204 | imageData[pixelIdx + 1] = color.g; 205 | imageData[pixelIdx + 2] = color.b; 206 | } 207 | } 208 | } 209 | 210 | void FireVisualizer::start() { 211 | randomSeed(micros()); 212 | } 213 | 214 | void FireVisualizer::stop() { 215 | } 216 | 217 | boolean FireVisualizer::onButtonPressed(uint8_t button) { 218 | boolean handled = false; 219 | 220 | switch (button) { 221 | case BTN_QUICK: 222 | handled = true; 223 | if (rendererId > 0) 224 | rendererId--; 225 | delay(200); 226 | break; 227 | case BTN_SLOW: 228 | handled = true; 229 | if (rendererId < 2) 230 | rendererId++; 231 | delay(200); 232 | break; 233 | 234 | case BTN_RED_UP: 235 | cooling -= 5; 236 | if (cooling <= 0) 237 | cooling = 0; 238 | handled = true; 239 | break; 240 | case BTN_RED_DOWN: 241 | cooling += 5; 242 | if (cooling >= 1000) 243 | cooling = 1000; 244 | handled = true; 245 | break; 246 | 247 | case BTN_GREEN_UP: 248 | sparking += 5; 249 | if (sparking >= 250) 250 | sparking = 250; 251 | handled = true; 252 | break; 253 | case BTN_GREEN_DOWN: 254 | sparking -= 5; 255 | if (sparking <= 0) 256 | sparking = 0; 257 | handled = true; 258 | break; 259 | 260 | case BTN_BLUE_UP: 261 | paletteMax += 5; 262 | if (paletteMax > 240) 263 | paletteMax = 240; 264 | handled = true; 265 | break; 266 | case BTN_BLUE_DOWN: 267 | paletteMax -= 5; 268 | if (paletteMax < 50) 269 | paletteMax = 50; 270 | handled = true; 271 | break; 272 | 273 | case BTN_DIY1: 274 | paletteNo = 0; 275 | handled = true; 276 | break; 277 | case BTN_DIY2: 278 | paletteNo = 1; 279 | handled = true; 280 | break; 281 | case BTN_DIY3: 282 | paletteNo = 2; 283 | handled = true; 284 | break; 285 | case BTN_AUTO: 286 | paletteNo = 3; 287 | handled = true; 288 | break; 289 | case BTN_DIY4: 290 | paletteNo = 4; 291 | handled = true; 292 | break; 293 | case BTN_DIY5: 294 | paletteNo = 5; 295 | handled = true; 296 | break; 297 | case BTN_DIY6: 298 | paletteNo = 6; 299 | handled = true; 300 | break; 301 | } 302 | 303 | return handled; 304 | } 305 | 306 | void FireVisualizer::readRuntimeConfiguration(int &address) { 307 | Logger.debug("FireVisualizer::readRuntimeConfiguration"); 308 | Visualizer::readRuntimeConfiguration(address); 309 | EEPROM.get(address, rendererId); 310 | address += sizeof(rendererId); 311 | EEPROM.get(address, cooling); 312 | address += sizeof(cooling); 313 | EEPROM.get(address, sparking); 314 | address += sizeof(sparking); 315 | EEPROM.get(address, paletteNo); 316 | address += sizeof(paletteNo); 317 | EEPROM.get(address, paletteMax); 318 | address += sizeof(paletteMax); 319 | } 320 | 321 | void FireVisualizer::writeRuntimeConfiguration(int &address) { 322 | Logger.debug("FireVisualizer::writeRuntimeConfiguration"); 323 | Visualizer::writeRuntimeConfiguration(address); 324 | EEPROM.put(address, rendererId); 325 | address += sizeof(rendererId); 326 | EEPROM.put(address, cooling); 327 | address += sizeof(cooling); 328 | EEPROM.put(address, sparking); 329 | address += sizeof(sparking); 330 | EEPROM.put(address, paletteNo); 331 | address += sizeof(paletteNo); 332 | EEPROM.put(address, paletteMax); 333 | address += sizeof(paletteMax); 334 | } 335 | 336 | const TProgmemRGBPalette16 HeatColorsBlue_p FL_PROGMEM = { 0x000000, 0x000033, 337 | 0x000066, 0x000099, 0x0000cc, 0x0000ff, 0x0033FF, 0x0066FF, 0x0099FF, 338 | 0x00CCFF, 0x00FFFF, 0x33FFFF, 0x66FFFF, 0x99FFFF, 0xccFFFF, 0xFFFFFF }; 339 | 340 | const TProgmemRGBPalette16 HeatColorsGreen_p FL_PROGMEM = { 0x000000, 0x003300, 341 | 0x006600, 0x009900, 0x00CC00, 0x00FF00, 0x00FF33, 0x00FF66, 0x00FF99, 342 | 0x00FFCC, 0x00FFFF, 0x33FFFF, 0x66FFFF, 0x99FFFF, 0xccFFFF, 0xFFFFFF }; 343 | -------------------------------------------------------------------------------- /SmartLEDLamp/FireVisualizer.h: -------------------------------------------------------------------------------- 1 | #ifndef FIREVISUALIZER_H_ 2 | #define FIREVISUALIZER_H_ 3 | 4 | #define FASTLED_INTERNAL 5 | #include "FastLED.h" 6 | #include "Visualizer.h" 7 | #include "defines.h" 8 | 9 | extern const TProgmemRGBPalette16 HeatColorsBlue_p FL_PROGMEM; 10 | extern const TProgmemRGBPalette16 HeatColorsGreen_p FL_PROGMEM; 11 | 12 | class FireVisualizer: public Visualizer { 13 | public: 14 | FireVisualizer(); 15 | virtual ~FireVisualizer(); 16 | 17 | virtual void start(); 18 | virtual void stop(); 19 | 20 | virtual boolean onButtonPressed(uint8_t button); 21 | 22 | virtual void readRuntimeConfiguration(int &address); 23 | virtual void writeRuntimeConfiguration(int &address); 24 | 25 | protected: 26 | virtual void computeImage(); 27 | void computeImage1(); 28 | void computeImage2(); 29 | void computeImage3(); 30 | 31 | private: 32 | uint8_t rendererId = 0; 33 | uint8_t paletteNo; 34 | int16_t paletteMax; 35 | CRGBPalette16 palette[7] = { CRGBPalette16(HeatColors_p), CRGBPalette16( 36 | HeatColorsGreen_p), CRGBPalette16(HeatColorsBlue_p), CRGBPalette16( 37 | PartyColors_p), CRGBPalette16(CloudColors_p), CRGBPalette16( 38 | LavaColors_p), CRGBPalette16(ForestColors_p) }; 39 | byte heat[LEDS_WIDTH][LEDS_HEIGHT * 2]; 40 | 41 | int16_t cooling; 42 | int16_t sparking; 43 | 44 | uint8_t vImageData[LEDS_WIDTH * LEDS_HEIGHT * 2 * 3]; 45 | 46 | }; 47 | 48 | #endif /* FIREVISUALIZER_H_ */ 49 | -------------------------------------------------------------------------------- /SmartLEDLamp/LEDLamp.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include "defines.h" 13 | #ifdef IR_ENABLE 14 | #include 15 | #include 16 | #include 17 | #endif // IR_ENABLE 18 | #include 19 | #include 20 | #include 21 | #include 22 | 23 | #include "Log.h" 24 | #include "LEDMatrix.h" 25 | #include "App.h" 26 | #include "VisualizerApp.h" 27 | #include "FadeAndScrollVisualizer.h" 28 | #include "FireVisualizer.h" 29 | #include "DemoReelVisualizer.h" 30 | #include "VUMeterVisualizer.h" 31 | #include "NoiseWithPaletteVisualizer.h" 32 | #include "Runnable.h" 33 | #include "TurnOnRunnable.h" 34 | #include "TurnOffRunnable.h" 35 | 36 | #ifdef MQTT_ENABLE 37 | #include 38 | 39 | WiFiClient mqttWiFiClient; 40 | PubSubClient mqttClient(mqttWiFiClient); 41 | #endif // MQTT_ENABLE 42 | 43 | #ifdef ARTNET_ENABLE 44 | #include 45 | 46 | ArtnetWifi artnet; 47 | #endif // ARTNET_ENABLE 48 | 49 | ESP8266WebServer server(80); 50 | 51 | WebSocketsServer webSocket = WebSocketsServer(81); 52 | uint8_t socketNumber = 0; 53 | unsigned long messageNumber; 54 | 55 | LEDMatrix matrix(LEDS_WIDTH, LEDS_HEIGHT); 56 | 57 | TurnOnRunnable turnOnRunnable(&matrix); 58 | TurnOffRunnable turnOffRunnable(&matrix); 59 | 60 | #ifdef IR_ENABLE 61 | IRrecv irrecv(PIN_RECV_IR); 62 | 63 | long irCodes[] = { 0xff3ac5, 0xffba45, 0xff827d, 0xff02fd, 0xff1ae5, 0xff9a65, 64 | 0xffa25d, 0xff22dd, 0xff2ad5, 0xffaa55, 0xff926d, 0xff12ed, 0xff0af5, 65 | 0xff8a75, 0xffb24d, 0xff32cd, 0xff38c7, 0xffb847, 0xff7887, 0xfff807, 66 | 0xff18e7, 0xff9867, 0xff58a7, 0xffd827, 0xff28d7, 0xffa857, 0xff6897, 67 | 0xffe817, 0xff08f7, 0xff8877, 0xff48b7, 0xffc837, 0xff30cf, 0xffb04f, 68 | 0xff708f, 0xfff00f, 0xff10ef, 0xff906f, 0xff50af, 0xffd02f, 0xff20df, 69 | 0xffa05f, 0xff609f, 0xffe01f }; 70 | #endif 71 | 72 | boolean isOn = false; 73 | boolean isPlaying = true; 74 | 75 | App* pCurrentApp = NULL; 76 | 77 | std::vector allApps; 78 | 79 | Runnable* pCurrentRunnable = NULL; 80 | 81 | int sensorPin = A0; 82 | long sensorValue = 0; 83 | 84 | uint8_t lastButton = 0; 85 | uint8_t r = 255, g = 255, b = 255; 86 | float brightness = 1.0f; 87 | float targetBrightness = 1.0f; 88 | 89 | String lampHostname; 90 | float calibRed = 1.0f; 91 | float calibGreen = 1.0f; 92 | float calibBlue = 1.0f; 93 | 94 | float val = 0.0f; 95 | float maxVal = 0.0f; 96 | float minVal = 2.0f; 97 | float curVal = 0.0f; 98 | 99 | unsigned long lastButtonPressMillis = 0; 100 | unsigned long lastSensorBurstReadMillis = 0; 101 | unsigned long lastBrightnessAdjustMillis = 0; 102 | unsigned long lastMatrixRefreshMillis = 0; 103 | unsigned long flushRuntimeConfigurationMillis = 0; 104 | 105 | void onButton(uint8_t btn); 106 | void update(); 107 | void switchApp(App* pApp); 108 | void writeConfiguration(); 109 | 110 | void readAndApplyRuntimeConfiguration(); 111 | void writeRuntimeConfiguration(); 112 | void flushRuntimeConfiguration(); 113 | 114 | App* getAppById(uint8_t appId) { 115 | std::vector::iterator it = allApps.begin(); 116 | 117 | while (it != allApps.end()) { 118 | if (appId == (*it)->getId()) { 119 | return (*it); 120 | } 121 | it++; 122 | } 123 | 124 | return NULL; 125 | } 126 | 127 | #ifdef MQTT_ENABLE 128 | void mqttCallback(char* topic, byte* payload, unsigned int length) { 129 | Logger.info("Message arrived [%s]", topic); 130 | 131 | Logger.info("Lamp state: %s, msg: %d", isOn ? "ON" : "OFF", payload[0]); 132 | 133 | if (payload[0] == '0' && isOn) { 134 | Logger.debug("Turning lamp OFF"); 135 | turnOffRunnable.init(); 136 | pCurrentRunnable = &turnOffRunnable; 137 | } else if (payload[0] == '1' && !isOn) { 138 | Logger.debug("Turning lamp ON"); 139 | turnOnRunnable.init(); 140 | pCurrentRunnable = &turnOnRunnable; 141 | } 142 | update(); 143 | } 144 | 145 | void mqttReconnect() { 146 | // Loop until we're reconnected 147 | while (!mqttClient.connected()) { 148 | Logger.info("Attempting MQTT connection..."); 149 | if (mqttClient.connect(lampHostname.c_str())) { 150 | Logger.info("Connected to MQTT server"); 151 | mqttClient.subscribe(MQTT_TOPIC); 152 | } else { 153 | Logger.info("failed, rc=%d. Trying again in a few seconds", 154 | mqttClient.state()); 155 | delay(5000); 156 | } 157 | } 158 | } 159 | #endif // MQTT_ENABLE 160 | 161 | #ifdef ARTNET_ENABLE 162 | void onDmxFrame(uint16_t universe, uint16_t length, uint8_t sequence, 163 | uint8_t* data) { 164 | 165 | int pixel = 0; 166 | uint16_t x = 0, y = 0; 167 | for (int i = 0; i < length; i += 3) { 168 | matrix.setPixel(x, y, data[i], data[i + 1], data[i + 2]); 169 | if (++x >= LEDS_WIDTH) { 170 | ++y; 171 | x = 0; 172 | } 173 | } 174 | 175 | matrix.update(); 176 | } 177 | #endif // ARTNET_ENABLE 178 | 179 | void connectToWiFi() { 180 | WiFi.hostname(lampHostname); 181 | WiFi.mode(WIFI_STA); 182 | WiFi.begin(); 183 | WiFiManager wifiManager; 184 | wifiManager.setTimeout(60 * 2); 185 | char apName[32]; 186 | sprintf(apName, "SmartLEDLamp-%06x", ESP.getChipId()); 187 | if (!wifiManager.autoConnect(apName)) { 188 | delay(1000); 189 | Logger.info("Failed to connect, resetting..."); 190 | ESP.reset(); 191 | delay(3000); 192 | } 193 | 194 | if (WiFi.isConnected()) { 195 | Logger.info("IP: %s", WiFi.localIP().toString().c_str()); 196 | } 197 | } 198 | 199 | void handleInfo() { 200 | String ret = F("Version: "); 201 | ret += VERSION; 202 | ret += "\nUptime (ms): "; 203 | ret += millis(); 204 | 205 | server.send(200, "text/plain", ret.c_str()); 206 | } 207 | 208 | 209 | void handleAction() { 210 | String act = server.arg("act"); 211 | if (act.length() != 0) { 212 | if (act == "off" && isOn) { 213 | turnOffRunnable.init(); 214 | pCurrentRunnable = &turnOffRunnable; 215 | } else if (act == "on" && !isOn) { 216 | turnOnRunnable.init(); 217 | pCurrentRunnable = &turnOnRunnable; 218 | } else if (act == "restart") { 219 | server.send(200, "text/plain", "Restarting..."); 220 | ESP.restart(); 221 | delay(3000); 222 | } else if (act == "resetconfig") { 223 | for (int i = 0; i < EEPROM_SIZE; ++i) { 224 | EEPROM.write(i, 0); 225 | EEPROM.commit(); 226 | } 227 | server.send(200, "text/plain", "Config reset. Restarting..."); 228 | ESP.restart(); 229 | delay(3000); 230 | } 231 | update(); 232 | } 233 | 234 | String btn = server.arg("btn"); 235 | if (btn.length() != 0) { 236 | long btnNo = btn.toInt(); 237 | 238 | if (btnNo >= 0) { 239 | onButton((uint8_t) btnNo); 240 | } 241 | } 242 | 243 | String brightness = server.arg("brightness"); 244 | if (brightness.length() != 0) { 245 | long bghtness = brightness.toInt(); 246 | 247 | if (bghtness >= 0 && bghtness <= 100) { 248 | if (bghtness == 0) 249 | bghtness = 1; 250 | targetBrightness = (float) (bghtness / 100.0); 251 | } 252 | } 253 | 254 | server.send(200, "text/plain", "OK"); 255 | } 256 | 257 | void webSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, 258 | size_t lenght) { 259 | socketNumber = num; 260 | 261 | switch (type) { 262 | case WStype_DISCONNECTED: 263 | break; 264 | case WStype_CONNECTED: 265 | webSocket.sendTXT(num, "Connected"); 266 | break; 267 | case WStype_TEXT: 268 | if (payload[0] == '!') { 269 | Logger.debug("Received RPC '%s'", &payload[1]); 270 | 271 | char* token = strtok((char*) &payload[1], " "); 272 | 273 | if (strcmp("setCalibration", token) == 0) { 274 | token = strtok(NULL, " "); 275 | double red = strtod(token, NULL); 276 | token = strtok(NULL, " "); 277 | double green = strtod(token, NULL); 278 | token = strtok(NULL, " "); 279 | double blue = strtod(token, NULL); 280 | 281 | calibRed = red; 282 | calibGreen = green; 283 | calibBlue = blue; 284 | 285 | Logger.debug("Calibrating to %f, %f, %f", red, green, blue); 286 | matrix.setCalibration(calibRed, calibGreen, calibBlue); 287 | } else if (strcmp("setHostname", token) == 0) { 288 | token = strtok(NULL, " "); 289 | lampHostname = token; 290 | if (lampHostname.length() > 31) 291 | lampHostname = lampHostname.substring(0, 31); 292 | lampHostname.trim(); 293 | 294 | Logger.debug("Setting hostname to %s", lampHostname.c_str()); 295 | } else if (strcmp("saveConfiguration", token) == 0) { 296 | writeConfiguration(); 297 | Logger.debug("Configuration saved"); 298 | } else if (strcmp("getConfiguration", token) == 0) { 299 | String ret = "getId()); 360 | } 361 | 362 | if (pCurrentApp) { 363 | pCurrentApp->stop(); 364 | } 365 | if (pApp) { 366 | pApp->start(); 367 | } 368 | 369 | pCurrentApp = pApp; 370 | 371 | if (pCurrentApp) { 372 | update(); 373 | } 374 | } 375 | 376 | void readConfiguration() { 377 | File configFile = SPIFFS.open("/config.json", "r"); 378 | if (!configFile) { 379 | Logger.info("No config file found, using defaults"); 380 | return; 381 | } 382 | 383 | size_t size = configFile.size(); 384 | if (size > 1024) { 385 | Logger.error("Config file size is too large"); 386 | return; 387 | } 388 | 389 | StaticJsonDocument<200> jsonDoc; 390 | DeserializationError err = deserializeJson(jsonDoc, configFile); 391 | if (err) { 392 | Logger.error("Failed to parse config file"); 393 | return; 394 | } 395 | 396 | char defaultHostname[15]; 397 | sprintf(defaultHostname, "LEDLamp-%06x", ESP.getChipId()); 398 | 399 | lampHostname = jsonDoc["hostname"] | defaultHostname; 400 | 401 | Logger.info("Hostname is %s", lampHostname.c_str()); 402 | 403 | calibRed = jsonDoc["calibration"]["red"]; 404 | calibGreen = jsonDoc["calibration"]["green"]; 405 | calibBlue = jsonDoc["calibration"]["blue"]; 406 | 407 | configFile.close(); 408 | } 409 | 410 | void writeConfiguration() { 411 | File configFile = SPIFFS.open("/config.json", "w"); 412 | 413 | StaticJsonDocument<200> jsonDoc; 414 | if (lampHostname) 415 | jsonDoc["hostname"] = lampHostname; 416 | 417 | JsonObject calibration = jsonDoc.createNestedObject("calibration"); 418 | calibration["red"] = calibRed; 419 | calibration["green"] = calibGreen; 420 | calibration["blue"] = calibBlue; 421 | 422 | serializeJsonPretty(jsonDoc, configFile); 423 | 424 | configFile.close(); 425 | } 426 | 427 | void setup() { 428 | // Turn off blue status LED 429 | pinMode(BUILTIN_LED, OUTPUT); 430 | digitalWrite(BUILTIN_LED, HIGH); 431 | 432 | delay(300); 433 | 434 | Logger.begin(); 435 | Serial.println("\n\n\n\n"); 436 | Logger.info("Starting Smart LED Lamp %s", VERSION); 437 | Logger.info("Free Sketch Space: %i", ESP.getFreeSketchSpace()); 438 | 439 | SPIFFS.begin(); 440 | 441 | matrix.init(); 442 | matrix.clear(); 443 | 444 | readConfiguration(); 445 | 446 | matrix.setCalibration(calibRed, calibGreen, calibBlue); 447 | 448 | connectToWiFi(); 449 | setupOTA(); 450 | 451 | matrix.clear(); 452 | 453 | startWebServer(); 454 | 455 | #ifdef ARTNET_ENABLE 456 | artnet.setArtDmxCallback(onDmxFrame); 457 | artnet.begin(); 458 | #endif // ARTNET_ENABLE 459 | 460 | matrix.clear(); 461 | 462 | uint8_t id = 1; 463 | 464 | VisualizerApp *pVisApp = new VisualizerApp(id++, &matrix); 465 | pVisApp->setVisualizer(0, new FadeAndScrollVisualizer(1, 20)); 466 | allApps.push_back(pVisApp); 467 | 468 | pVisApp = new VisualizerApp(id++, &matrix); 469 | pVisApp->setVisualizer(0, new FireVisualizer()); 470 | allApps.push_back(pVisApp); 471 | 472 | pVisApp = new VisualizerApp(id++, &matrix); 473 | pVisApp->setVisualizer(0, new DemoReelVisualizer()); 474 | allApps.push_back(pVisApp); 475 | 476 | pVisApp = new VisualizerApp(id++, &matrix); 477 | pVisApp->setVisualizer(0, new VUMeterVisualizer(&curVal)); 478 | allApps.push_back(pVisApp); 479 | 480 | pVisApp = new VisualizerApp(id++, &matrix); 481 | pVisApp->setVisualizer(0, new NoiseWithPaletteVisualizer()); 482 | allApps.push_back(pVisApp); 483 | 484 | #ifdef IR_ENABLE 485 | irrecv.enableIRIn(); // Start the receiver 486 | #endif 487 | 488 | #ifdef MQTT_ENABLE 489 | mqttClient.setServer(MQTT_SERVER, 1883); 490 | mqttClient.setCallback(mqttCallback); 491 | #endif // MQTT_ENABLE 492 | 493 | update(); 494 | 495 | pinMode(sensorPin, INPUT); 496 | 497 | EEPROM.begin(EEPROM_SIZE); 498 | readAndApplyRuntimeConfiguration(); 499 | } 500 | 501 | void readAndApplyRuntimeConfiguration() { 502 | Logger.debug("Reading Runtime Configuration"); 503 | 504 | int addr = 0; 505 | uint8_t lastApp = EEPROM.read(addr); 506 | 507 | // check for EEPROM validity marker 508 | if (EEPROM.read(++addr) == MAGIC_MARKER) { 509 | isPlaying = (EEPROM.read(++addr) != 0); 510 | 511 | matrix.setBrightness(EEPROM.get(++addr, brightness)); 512 | targetBrightness = brightness; 513 | addr += sizeof(brightness); 514 | 515 | EEPROM.get(addr, r); 516 | addr += sizeof(r); 517 | EEPROM.get(addr, g); 518 | addr += sizeof(g); 519 | EEPROM.get(addr, b); 520 | addr += sizeof(b); 521 | } else { 522 | Logger.debug("Couldn't find MAGIC_MARKER, skipped configuring"); 523 | } 524 | 525 | addr = 16; 526 | 527 | std::vector::iterator it = allApps.begin(); 528 | while (it != allApps.end()) { 529 | Logger.debug("Reading Runtime Configuration for app#%i (%s)", (*it)->getId(), (*it)->getName()); 530 | 531 | if (EEPROM.read(addr) == MAGIC_MARKER) { 532 | addr += 1; 533 | (*it)->readRuntimeConfiguration(addr); 534 | } else { 535 | Logger.debug("Couldn't find MAGIC_MARKER, skipped configuring"); 536 | } 537 | it++; 538 | } 539 | 540 | switchApp(getAppById(lastApp)); 541 | } 542 | 543 | void writeRuntimeConfiguration() { 544 | Logger.debug("Writing Runtime Configuration"); 545 | 546 | int addr = 0; 547 | uint8_t curApp = 0; 548 | 549 | if (pCurrentApp) { 550 | curApp = pCurrentApp->getId(); 551 | } 552 | 553 | EEPROM.write(addr, curApp); 554 | 555 | EEPROM.write(++addr, MAGIC_MARKER); 556 | EEPROM.write(++addr, isPlaying ? 1 : 0); 557 | 558 | EEPROM.put(++addr, brightness); 559 | addr += sizeof(brightness); 560 | 561 | EEPROM.write(addr, r); 562 | addr += sizeof(r); 563 | EEPROM.write(addr, g); 564 | addr += sizeof(g); 565 | EEPROM.write(addr, b); 566 | addr += sizeof(b); 567 | 568 | addr = 16; 569 | 570 | std::vector::iterator it = allApps.begin(); 571 | while (it != allApps.end()) { 572 | EEPROM.write(addr, MAGIC_MARKER); 573 | addr += 1; 574 | (*it)->writeRuntimeConfiguration(addr); 575 | 576 | it++; 577 | } 578 | } 579 | 580 | void flushRuntimeConfiguration() { 581 | Logger.debug("Flushing Runtime Configuration"); 582 | EEPROM.commit(); 583 | } 584 | 585 | void writeAndFlushRuntimeConfigurationDelayed(long flushInSecs) { 586 | writeRuntimeConfiguration(); 587 | flushRuntimeConfigurationMillis = millis() + flushInSecs * 1000; 588 | } 589 | 590 | void update() { 591 | if (isOn) { 592 | if (pCurrentApp) { 593 | pCurrentApp->update(); 594 | } else { 595 | matrix.fill(r, g, b); 596 | } 597 | } else { 598 | matrix.clear(); 599 | } 600 | matrix.update(); 601 | } 602 | 603 | void onButton(uint8_t btn) { 604 | Logger.debug("Button '%i' pressed", btn); 605 | 606 | if (btn == BTN_POWER) { 607 | if (!isOn) { 608 | turnOnRunnable.init(); 609 | pCurrentRunnable = &turnOnRunnable; 610 | } else { 611 | turnOffRunnable.init(); 612 | pCurrentRunnable = &turnOffRunnable; 613 | 614 | writeAndFlushRuntimeConfigurationDelayed(0); 615 | } 616 | delay(200); 617 | update(); 618 | } 619 | 620 | if (!isOn) { 621 | return; 622 | } 623 | 624 | if (pCurrentApp) { 625 | if (pCurrentApp->onButtonPressed(btn)) { 626 | writeAndFlushRuntimeConfigurationDelayed(5); 627 | return; 628 | } 629 | } 630 | 631 | boolean runtimeConfigurationChanged = false; 632 | 633 | switch (btn) { 634 | case BTN_BRIGHTER: 635 | brightness += 0.05f; 636 | if (brightness > 1.0f) 637 | brightness = 1.0f; 638 | matrix.setBrightness(brightness); 639 | targetBrightness = brightness; 640 | matrix.update(); 641 | runtimeConfigurationChanged = true; 642 | break; 643 | case BTN_DARKER: 644 | brightness -= 0.05f; 645 | if (brightness < 0.05f) 646 | brightness = 0.05f; 647 | matrix.setBrightness(brightness); 648 | targetBrightness = brightness; 649 | matrix.update(); 650 | runtimeConfigurationChanged = true; 651 | break; 652 | case BTN_PAUSE: 653 | isPlaying = !isPlaying; 654 | runtimeConfigurationChanged = true; 655 | delay(200); 656 | break; 657 | 658 | case BTN_R: 659 | switchApp(NULL); 660 | g = b = 0; 661 | r = 0xff; 662 | update(); 663 | runtimeConfigurationChanged = true; 664 | break; 665 | case BTN_G: 666 | switchApp(NULL); 667 | r = b = 0; 668 | g = 0xff; 669 | update(); 670 | runtimeConfigurationChanged = true; 671 | break; 672 | case BTN_B: 673 | switchApp(NULL); 674 | r = g = 0; 675 | b = 0xff; 676 | update(); 677 | runtimeConfigurationChanged = true; 678 | break; 679 | case BTN_W: 680 | switchApp(NULL); 681 | r = g = b = 0xff; 682 | update(); 683 | runtimeConfigurationChanged = true; 684 | break; 685 | 686 | case 9: 687 | switchApp(NULL); 688 | r = 0xf5; 689 | g = 0x28; 690 | b = 0x0a; 691 | update(); 692 | runtimeConfigurationChanged = true; 693 | break; 694 | case 10: 695 | switchApp(NULL); 696 | r = 0x00; 697 | g = 0xff; 698 | b = 0x14; 699 | update(); 700 | runtimeConfigurationChanged = true; 701 | break; 702 | case 11: 703 | switchApp(NULL); 704 | r = 0x19; 705 | g = 0x19; 706 | b = 0xff; 707 | update(); 708 | runtimeConfigurationChanged = true; 709 | break; 710 | case 12: 711 | switchApp(NULL); 712 | r = 0xf5; 713 | g = 0x69; 714 | b = 0x1e; 715 | update(); 716 | runtimeConfigurationChanged = true; 717 | break; 718 | 719 | case 13: 720 | switchApp(NULL); 721 | r = 0xff; 722 | g = 0x28; 723 | b = 0x0f; 724 | update(); 725 | runtimeConfigurationChanged = true; 726 | break; 727 | case 14: 728 | switchApp(NULL); 729 | r = 0x00; 730 | g = 0x7d; 731 | b = 0x69; 732 | update(); 733 | runtimeConfigurationChanged = true; 734 | break; 735 | case 15: 736 | switchApp(NULL); 737 | r = 0x00; 738 | g = 0x00; 739 | b = 0x14; 740 | update(); 741 | runtimeConfigurationChanged = true; 742 | break; 743 | case 16: 744 | switchApp(NULL); 745 | r = 0xf5; 746 | g = 0x69; 747 | b = 0x1e; 748 | update(); 749 | runtimeConfigurationChanged = true; 750 | break; 751 | 752 | case 17: 753 | switchApp(NULL); 754 | r = 0xf5; 755 | g = 0x28; 756 | b = 0x0a; 757 | update(); 758 | runtimeConfigurationChanged = true; 759 | break; 760 | case 18: 761 | switchApp(NULL); 762 | r = 0x00; 763 | g = 0x46; 764 | b = 0x37; 765 | update(); 766 | runtimeConfigurationChanged = true; 767 | break; 768 | case 19: 769 | switchApp(NULL); 770 | r = 0xb4; 771 | g = 0x00; 772 | b = 0x69; 773 | update(); 774 | runtimeConfigurationChanged = true; 775 | break; 776 | case 20: 777 | switchApp(NULL); 778 | r = 0x4b; 779 | g = 0x55; 780 | b = 0xff; 781 | update(); 782 | runtimeConfigurationChanged = true; 783 | break; 784 | 785 | case 21: 786 | switchApp(NULL); 787 | r = 0xfa; 788 | g = 0xec; 789 | b = 0x00; 790 | update(); 791 | runtimeConfigurationChanged = true; 792 | break; 793 | case 22: 794 | switchApp(NULL); 795 | r = 0x00; 796 | g = 0x23; 797 | b = 0x0a; 798 | update(); 799 | runtimeConfigurationChanged = true; 800 | break; 801 | case 23: 802 | switchApp(NULL); 803 | r = 0xe6; 804 | g = 0x00; 805 | b = 0x37; 806 | update(); 807 | runtimeConfigurationChanged = true; 808 | break; 809 | case 24: 810 | switchApp(NULL); 811 | r = 0x4b; 812 | g = 0x55; 813 | b = 0xff; 814 | update(); 815 | runtimeConfigurationChanged = true; 816 | break; 817 | 818 | case BTN_RED_UP: 819 | r += 5; 820 | if (r > 255) 821 | r = 255; 822 | update(); 823 | runtimeConfigurationChanged = true; 824 | break; 825 | case BTN_RED_DOWN: 826 | r -= 5; 827 | if (r < 0) 828 | r = 0; 829 | update(); 830 | runtimeConfigurationChanged = true; 831 | break; 832 | case BTN_GREEN_UP: 833 | g += 5; 834 | if (g > 255) 835 | g = 255; 836 | update(); 837 | runtimeConfigurationChanged = true; 838 | break; 839 | case BTN_GREEN_DOWN: 840 | g -= 5; 841 | if (g < 0) 842 | g = 0; 843 | update(); 844 | runtimeConfigurationChanged = true; 845 | break; 846 | case BTN_BLUE_UP: 847 | b += 5; 848 | if (b > 255) 849 | b = 255; 850 | update(); 851 | runtimeConfigurationChanged = true; 852 | break; 853 | case BTN_BLUE_DOWN: 854 | b -= 5; 855 | if (b < 0) 856 | b = 0; 857 | update(); 858 | runtimeConfigurationChanged = true; 859 | break; 860 | 861 | case BTN_FLASH: 862 | switchApp(getAppById(5)); 863 | runtimeConfigurationChanged = true; 864 | break; 865 | 866 | case BTN_JUMP3: 867 | switchApp(getAppById(1)); 868 | runtimeConfigurationChanged = true; 869 | break; 870 | case BTN_JUMP7: 871 | switchApp(getAppById(2)); 872 | runtimeConfigurationChanged = true; 873 | break; 874 | case BTN_FADE3: 875 | switchApp(getAppById(3)); 876 | runtimeConfigurationChanged = true; 877 | break; 878 | case BTN_FADE7: 879 | switchApp(getAppById(4)); 880 | runtimeConfigurationChanged = true; 881 | break; 882 | } 883 | 884 | if (runtimeConfigurationChanged) { 885 | writeAndFlushRuntimeConfigurationDelayed(5); 886 | } 887 | } 888 | 889 | void readAnalogPeek() { 890 | maxVal = -10.0f; 891 | minVal = 10.0f; 892 | // String message = "# " + String(messageNumber) + " "; 893 | for (int i = 0; i < 300; ++i) { 894 | sensorValue = analogRead(sensorPin); 895 | // if (i & 1) { 896 | // message = message + String(sensorValue) + ";"; 897 | // } 898 | float newVal = sensorValue / 1024.0f; 899 | if (newVal > maxVal) 900 | maxVal = newVal; 901 | 902 | if (newVal < minVal) 903 | minVal = newVal; 904 | 905 | if (maxVal - minVal > curVal) { 906 | curVal = maxVal - minVal; 907 | } 908 | } 909 | // message[message.length() - 1] = '\0'; 910 | // webSocket.sendTXT(socketNumber, message); 911 | lastSensorBurstReadMillis = millis(); 912 | } 913 | 914 | void loop() { 915 | unsigned long currentMillis = millis(); 916 | 917 | // if (currentMillis - lastSensorBurstRead > 6 /*5*/) { 918 | // readAnalogPeek(); 919 | // } 920 | 921 | ArduinoOTA.handle(); 922 | server.handleClient(); 923 | webSocket.loop(); 924 | Logger.loop(); 925 | 926 | if (brightness != targetBrightness 927 | && currentMillis - lastBrightnessAdjustMillis > 25) { 928 | lastBrightnessAdjustMillis = currentMillis; 929 | 930 | int brightnessDiff = 100 * targetBrightness - 100 * brightness; 931 | 932 | if (abs(brightnessDiff) <= 1) { 933 | brightness = targetBrightness; 934 | } 935 | 936 | if (brightness > targetBrightness) { 937 | brightness -= 0.01; 938 | } else if (brightness < targetBrightness) { 939 | brightness += 0.01; 940 | } 941 | Logger.debug("Setting brightness to %f (%f)", brightness, 942 | targetBrightness); 943 | matrix.setBrightness(brightness); 944 | 945 | if ((!pCurrentRunnable && !pCurrentApp) || !isPlaying) { 946 | update(); 947 | } 948 | } 949 | 950 | if (pCurrentRunnable) { 951 | pCurrentRunnable->run(); 952 | update(); 953 | } 954 | 955 | if (isOn && isPlaying) { 956 | if (pCurrentApp) { 957 | pCurrentApp->run(); 958 | } 959 | #ifdef ARTNET_ENABLE 960 | else { 961 | artnet.read(); 962 | } 963 | #endif // ARTNET_ENABLE 964 | } else if (currentMillis - lastMatrixRefreshMillis > 5000) { 965 | lastMatrixRefreshMillis = currentMillis; 966 | update(); 967 | } 968 | 969 | #ifdef IR_ENABLE 970 | decode_results results; 971 | 972 | if (currentMillis - lastButtonPressMillis > 200) { 973 | lastButton = 0; 974 | } 975 | 976 | if (irrecv.decode(&results)) { 977 | if (results.value == 0xffffffff && lastButton != BTN_POWER) { 978 | lastButtonPressMillis = currentMillis; 979 | } else if ((results.value & 0xff000000) == 0) { 980 | for (int i = 0; i < sizeof(irCodes) / sizeof(irCodes[0]); ++i) { 981 | if (irCodes[i] == results.value) { 982 | lastButton = i + 1; 983 | lastButtonPressMillis = currentMillis; 984 | break; 985 | } 986 | } 987 | } 988 | if (lastButton) 989 | onButton(lastButton); 990 | irrecv.resume(); // Receive the next value 991 | } 992 | #endif 993 | 994 | #ifdef MQTT_ENABLE 995 | if (!mqttClient.connected()) { 996 | mqttReconnect(); 997 | } 998 | mqttClient.loop(); 999 | #endif 1000 | 1001 | if (flushRuntimeConfigurationMillis != 0 && currentMillis - flushRuntimeConfigurationMillis >= 0) { 1002 | flushRuntimeConfigurationMillis = 0; 1003 | 1004 | flushRuntimeConfiguration(); 1005 | } 1006 | 1007 | yield(); 1008 | } 1009 | -------------------------------------------------------------------------------- /SmartLEDLamp/LEDLamp.h: -------------------------------------------------------------------------------- 1 | #ifndef LEDLAMP_H_ 2 | #define LEDLAMP_H_ 3 | 4 | #include "defines.h" 5 | #include "Runnable.h" 6 | 7 | extern boolean isOn; 8 | extern Runnable* pCurrentRunnable; 9 | 10 | #ifdef MQTT_ENABLE 11 | #include 12 | extern PubSubClient mqttClient; 13 | #endif // MQTT_ENABLE 14 | 15 | 16 | #endif /* LEDLAMP_H_ */ 17 | -------------------------------------------------------------------------------- /SmartLEDLamp/LEDMatrix.cpp: -------------------------------------------------------------------------------- 1 | #include "LEDMatrix.h" 2 | #include "defines.h" 3 | 4 | LEDMatrix::LEDMatrix(uint8_t aWidth, uint8_t aHeight) : 5 | width(aWidth), height(aHeight), noPixels(aWidth * aHeight), pLEDs( 6 | (CRGB*) malloc(aWidth * aHeight * sizeof(CRGB))), pPixels( 7 | (CRGB*) malloc(aWidth * aHeight * sizeof(CRGB))) { 8 | 9 | } 10 | 11 | LEDMatrix::~LEDMatrix() { 12 | if (pPixels) 13 | free(pPixels); 14 | if (pLEDs) 15 | free(pLEDs); 16 | } 17 | 18 | void LEDMatrix::init() { 19 | FastLED.addLeds(pLEDs, 20 | width * height); 21 | } 22 | 23 | void LEDMatrix::clear() { 24 | fill(0, 0, 0); 25 | } 26 | 27 | void LEDMatrix::fill(uint8_t r, uint8_t g, uint8_t b) { 28 | for (uint16_t i = 0; i < noPixels; ++i) { 29 | pPixels[i].r = r; 30 | pPixels[i].g = g; 31 | pPixels[i].b = b; 32 | } 33 | update(); 34 | } 35 | 36 | void LEDMatrix::setPixel(int16_t x, int16_t y, uint8_t r, uint8_t g, 37 | uint8_t b) { 38 | if (x >= width || y >= height || x < 0 || y < 0) 39 | return; 40 | 41 | uint16_t idx = x + y * width; 42 | pPixels[idx].r = r; 43 | pPixels[idx].g = g; 44 | pPixels[idx].b = b; 45 | } 46 | 47 | void LEDMatrix::setCalibration(float r, float g, float b) { 48 | calibrationRed = r; 49 | calibrationGreen = g; 50 | calibrationBlue = b; 51 | 52 | const CRGB colorCalibration(255 * r, 255 * g, 255 * b); 53 | FastLED.setCorrection(colorCalibration); 54 | } 55 | 56 | void LEDMatrix::setBrightness(float percentage) { 57 | brightness = percentage; 58 | 59 | FastLED.setBrightness(255 * percentage); 60 | } 61 | 62 | void LEDMatrix::update() { 63 | uint16_t idxSrc = 0; 64 | 65 | uint8_t curOffset = yOffset; 66 | 67 | for (uint8_t y = 0; y < height; ++y) { 68 | for (uint8_t x = 0; x < width; ++x) { 69 | CRGB& led = pLEDs[xy2idx(x, y)]; 70 | 71 | int16_t readY = y - yOffset; 72 | if (readY < 0) { 73 | led = 0; 74 | } else { 75 | led = pPixels[x + readY * width]; 76 | } 77 | } 78 | } 79 | 80 | FastLED.show(); 81 | } 82 | 83 | -------------------------------------------------------------------------------- /SmartLEDLamp/LEDMatrix.h: -------------------------------------------------------------------------------- 1 | #ifndef LEDMATRIX_H_ 2 | #define LEDMATRIX_H_ 3 | 4 | #define FASTLED_INTERNAL 5 | #include "FastLED.h" 6 | 7 | class LEDMatrix { 8 | public: 9 | LEDMatrix(uint8_t aWidth, uint8_t aHeight); 10 | virtual ~LEDMatrix(); 11 | 12 | void init(); 13 | 14 | void setPixel(int16_t x, int16_t y, uint8_t r, uint8_t g, uint8_t b); 15 | 16 | void clear(); 17 | void fill(uint8_t r, uint8_t g, uint8_t b); 18 | void update(); 19 | 20 | void flip(); 21 | 22 | void setCalibration(float r, float g, float b); 23 | void setBrightness(float percentage); 24 | 25 | inline uint8_t getWidth() { 26 | return width; 27 | } 28 | inline uint8_t getHeight() { 29 | return height; 30 | } 31 | 32 | inline void setYOffset(uint8_t offset) { 33 | if (offset > height) { 34 | offset = height; 35 | } 36 | yOffset = offset; 37 | } 38 | 39 | inline uint8_t getYOffset() { 40 | return yOffset; 41 | } 42 | 43 | inline uint16_t xy2idx(uint8_t x, uint8_t y) { 44 | uint16_t idx; 45 | 46 | if (serpentineLayout && !(x & 1)) { 47 | idx = (x * height) + (height - 1) - y; 48 | } else { 49 | idx = (x * height) + y; 50 | } 51 | 52 | return idx; 53 | } 54 | 55 | private: 56 | CRGB* pLEDs; 57 | 58 | CRGB* pPixels; 59 | uint8_t width; 60 | uint8_t height; 61 | uint8_t yOffset = 0; 62 | uint8_t noPixels; 63 | 64 | float calibrationRed = 1.0f; 65 | float calibrationGreen = 1.0f; 66 | float calibrationBlue = 1.0f; 67 | float brightness = 1.0f; 68 | 69 | bool serpentineLayout = true; 70 | }; 71 | 72 | #endif /* LEDMATRIX_H_ */ 73 | -------------------------------------------------------------------------------- /SmartLEDLamp/Log.cpp: -------------------------------------------------------------------------------- 1 | #include "Log.h" 2 | #include 3 | 4 | Log::Log() { 5 | } 6 | 7 | Log::~Log() { 8 | if (_pServer) 9 | delete _pServer; 10 | if (_pServerClient) 11 | delete _pServerClient; 12 | } 13 | 14 | void Log::begin() { 15 | begin(LOG_LEVEL_DEBUG, 115200L, true); 16 | } 17 | 18 | void Log::begin(int level, long baud, boolean logServer) { 19 | _level = constrain(level, LOG_LEVEL_NONE, LOG_LEVEL_VERBOSE); 20 | _baud = baud; 21 | _logServer = logServer; 22 | Serial.begin(_baud); 23 | } 24 | 25 | void Log::setLogLevel(int level) { 26 | _level = constrain(level, LOG_LEVEL_NONE, LOGLEVEL); 27 | if (level > LOGLEVEL) { 28 | error("Loglevel %d not possible", level); // requested level 29 | error("Selected level %d instead", _level); // selected/applied level 30 | } 31 | } 32 | 33 | void Log::error(char* msg, ...) { 34 | if (LOG_LEVEL_ERROR <= _level) { 35 | bufferString.remove(0); 36 | bufferString += "Error: "; 37 | va_list args; 38 | va_start(args, msg); 39 | print(msg, args, bufferString); 40 | println(bufferString); 41 | } 42 | } 43 | 44 | void Log::info(char* msg, ...) { 45 | #if LOGLEVEL > 1 46 | if (LOG_LEVEL_INFO <= _level) { 47 | bufferString.remove(0); 48 | bufferString += "Info: "; 49 | va_list args; 50 | va_start(args, msg); 51 | print(msg, args, bufferString); 52 | println(bufferString); 53 | } 54 | #endif 55 | } 56 | 57 | void Log::debug(char* msg, ...) { 58 | #if LOGLEVEL > 2 59 | if (LOG_LEVEL_DEBUG <= _level) { 60 | bufferString.remove(0); 61 | bufferString += "Debug: "; 62 | va_list args; 63 | va_start(args, msg); 64 | print(msg, args, bufferString); 65 | println(bufferString); 66 | } 67 | #endif 68 | } 69 | 70 | void Log::verbose(char* msg, ...) { 71 | #if LOGLEVEL > 3 72 | if (LOG_LEVEL_VERBOSE <= _level) 73 | { 74 | bufferString.remove(0); 75 | bufferString += "Verbose: "; 76 | va_list args; 77 | va_start(args, msg); 78 | print(msg, args, bufferString); 79 | println(bufferString); 80 | } 81 | #endif 82 | } 83 | 84 | void Log::print(const char *format, va_list args, String& dst) { 85 | boolean sendClient = _logServer && _pServerClient 86 | && _pServerClient->connected(); 87 | 88 | for (; *format != 0; ++format) { 89 | if (*format == '%') { 90 | ++format; 91 | if (*format == '\0') 92 | break; 93 | else if (*format == '%') { 94 | dst.concat(*format); 95 | continue; 96 | } else if (*format == 's') { 97 | register char *s = (char *) va_arg(args, int); 98 | dst.concat(s); 99 | continue; 100 | } else if (*format == 'd' || *format == 'i' || *format == 'c') { 101 | dst.concat(va_arg(args, int)); 102 | continue; 103 | } else if (*format == 'f') { 104 | dst.concat(va_arg(args, double)); 105 | continue; 106 | } else if (*format == 'l') { 107 | dst.concat(va_arg(args, long)); 108 | continue; 109 | } else if (*format == 't') { 110 | if (va_arg( 111 | args, int) == 1) { 112 | dst.concat('T'); 113 | } else { 114 | dst.concat('F'); 115 | } 116 | continue; 117 | } else if (*format == 'T') { 118 | if (va_arg( 119 | args, int) == 1) { 120 | dst.concat("true"); 121 | } else { 122 | dst.concat("false"); 123 | } 124 | continue; 125 | } 126 | } 127 | dst.concat(*format); 128 | } 129 | } 130 | 131 | void Log::loop() { 132 | if (!_logServer) 133 | return; 134 | 135 | if (_logServer && !_pServer) { 136 | _pServer = new WiFiServer(8888); 137 | _pServerClient = new WiFiClient(); 138 | _pServer->begin(); 139 | _pServer->setNoDelay(true); 140 | } 141 | 142 | if (_pServer->hasClient()) { 143 | 144 | if (!_pServerClient || !_pServerClient->connected()) { 145 | if (_pServerClient) 146 | _pServerClient->stop(); 147 | *_pServerClient = _pServer->available(); 148 | } 149 | WiFiClient serverClient = _pServer->available(); 150 | serverClient.stop(); 151 | } 152 | } 153 | 154 | Log Logger; 155 | -------------------------------------------------------------------------------- /SmartLEDLamp/Log.h: -------------------------------------------------------------------------------- 1 | #ifndef LOG_H_ 2 | #define LOG_H_ 3 | 4 | #define LOG_LEVEL_NONE 0 5 | #define LOG_LEVEL_ERROR 1 6 | #define LOG_LEVEL_INFO 2 7 | #define LOG_LEVEL_DEBUG 3 8 | #define LOG_LEVEL_VERBOSE 4 9 | 10 | #ifndef LOGLEVEL 11 | //#define LOGLEVEL LOG_LEVEL_NONE 12 | //#define LOGLEVEL LOG_LEVEL_ERROR 13 | //#define LOGLEVEL LOG_LEVEL_INFO 14 | #define LOGLEVEL LOG_LEVEL_DEBUG 15 | //#define LOGLEVEL LOG_LEVEL_VERBOSE 16 | #endif 17 | 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | class Log { 25 | public: 26 | Log(); 27 | virtual ~Log(); 28 | 29 | void begin(); 30 | void begin(int level, long baud, boolean logServer); 31 | 32 | void setLogLevel(int level); 33 | 34 | void error(char* msg, ...); 35 | void info(char* msg, ...); 36 | void debug(char* msg, ...); 37 | void verbose(char* msg, ...); 38 | 39 | void loop(); 40 | 41 | protected: 42 | inline void println(String& msg) { 43 | boolean sendClient = _logServer && _pServerClient 44 | && _pServerClient->connected(); 45 | 46 | Serial.println(msg); 47 | if (sendClient) 48 | _pServerClient->println(msg); 49 | } 50 | 51 | private: 52 | int _level = LOG_LEVEL_DEBUG; 53 | long _baud = 115200L; 54 | boolean _logServer = false; 55 | WiFiServer *_pServer = NULL; 56 | WiFiClient *_pServerClient = NULL; 57 | 58 | String bufferString; 59 | 60 | #if LOGLEVEL > 0 61 | void print(const char *format, va_list args, String& dst); 62 | #endif 63 | }; 64 | 65 | extern Log Logger; 66 | 67 | #endif /* LOG_H_ */ 68 | -------------------------------------------------------------------------------- /SmartLEDLamp/NoiseWithPaletteVisualizer.cpp: -------------------------------------------------------------------------------- 1 | #include "NoiseWithPaletteVisualizer.h" 2 | #include 3 | #include "Log.h" 4 | 5 | NoiseWithPaletteVisualizer::NoiseWithPaletteVisualizer() : 6 | currentPalette(PartyColors_p) { 7 | } 8 | 9 | NoiseWithPaletteVisualizer::~NoiseWithPaletteVisualizer() { 10 | } 11 | 12 | void NoiseWithPaletteVisualizer::computeImage() { 13 | // generate noise data 14 | fillnoise8(); 15 | 16 | // convert the noise data to colors in the LED array 17 | // using the current palette 18 | mapNoiseToLEDsUsingPalette(); 19 | } 20 | 21 | void NoiseWithPaletteVisualizer::fillnoise8() { 22 | // If we're running at a low "speed", some 8-bit artifacts become visible 23 | // from frame-to-frame. In order to reduce this, we can do some fast data-smoothing. 24 | // The amount of data smoothing we're doing depends on "speed". 25 | uint8_t dataSmoothing = 0; 26 | if (speed < 50) { 27 | dataSmoothing = 200 - (speed * 4); 28 | } 29 | 30 | for (int i = 0; i < LEDS_HEIGHT; i++) { 31 | int ioffset = scale * i; 32 | for (int j = 0; j < LEDS_HEIGHT; j++) { 33 | int joffset = scale * j; 34 | 35 | uint8_t data = inoise8(x + ioffset, y + joffset, z); 36 | 37 | // The range of the inoise8 function is roughly 16-238. 38 | // These two operations expand those values out to roughly 0..255 39 | // You can comment them out if you want the raw noise data. 40 | data = qsub8(data, 16); 41 | data = qadd8(data, scale8(data, 39)); 42 | 43 | if (dataSmoothing) { 44 | uint8_t olddata = noise[i][j]; 45 | uint8_t newdata = scale8(olddata, dataSmoothing) 46 | + scale8(data, 256 - dataSmoothing); 47 | data = newdata; 48 | } 49 | 50 | noise[i][j] = data; 51 | } 52 | } 53 | 54 | z += speed; 55 | 56 | // apply slow drift to X and Y, just for visual variation. 57 | x += speed / 8; 58 | y -= speed / 16; 59 | } 60 | 61 | void NoiseWithPaletteVisualizer::mapNoiseToLEDsUsingPalette() { 62 | for (int i = 0; i < 40; i++) { 63 | for (int j = 0; j < 4; j++) { 64 | // We use the value at the (i,j) coordinate in the noise 65 | // array for our brightness, and the flipped value from (j,i) 66 | // for our pixel's index into the color palette. 67 | 68 | uint8_t index = noise[j][i]; 69 | uint8_t bri = noise[i][j]; 70 | 71 | // if this palette is a 'loop', add a slowly-changing base value 72 | if (colorLoop) { 73 | index += ihue; 74 | } 75 | 76 | // brighten up, as the color palette itself often contains the 77 | // light/dark dynamic range desired 78 | if (bri > 127) { 79 | bri = 255; 80 | } else { 81 | bri = dim8_raw(bri * 2); 82 | } 83 | 84 | CRGB crgb = ColorFromPalette(currentPalette, index, bri); 85 | imageData[((i) * LEDS_WIDTH + j) * 3 + 0] = crgb.r; 86 | imageData[((i) * LEDS_WIDTH + j) * 3 + 1] = crgb.g; 87 | imageData[((i) * LEDS_WIDTH + j) * 3 + 2] = crgb.b; 88 | } 89 | } 90 | 91 | ihue += 1; 92 | } 93 | 94 | // There are several different palettes of colors demonstrated here. 95 | // 96 | // FastLED provides several 'preset' palettes: RainbowColors_p, RainbowStripeColors_p, 97 | // OceanColors_p, CloudColors_p, LavaColors_p, ForestColors_p, and PartyColors_p. 98 | // 99 | // Additionally, you can manually define your own color palettes, or you can write 100 | // code that creates color palettes on the fly. 101 | // 1 = 5 sec per palette 102 | // 2 = 10 sec per palette 103 | // etc 104 | #define HOLD_PALETTES_X_TIMES_AS_LONG 1 105 | 106 | void NoiseWithPaletteVisualizer::applyProfile() { 107 | if (profile == 1) { 108 | currentPalette = RainbowColors_p; 109 | colorLoop = 1; 110 | } else if (profile == 2) { 111 | SetupPurpleAndGreenPalette(); 112 | colorLoop = 1; 113 | } else if (profile == 3) { 114 | SetupBlackAndWhiteStripedPalette(); 115 | colorLoop = 1; 116 | } else if (profile == 4) { 117 | currentPalette = ForestColors_p; 118 | colorLoop = 0; 119 | } else if (profile == 5) { 120 | currentPalette = CloudColors_p; 121 | colorLoop = 0; 122 | } else if (profile == 6) { 123 | currentPalette = LavaColors_p; 124 | colorLoop = 0; 125 | } else if (profile == 7) { 126 | currentPalette = OceanColors_p; 127 | colorLoop = 0; 128 | } else if (profile == 8) { 129 | currentPalette = PartyColors_p; 130 | colorLoop = 1; 131 | } else if (profile == 9) { 132 | currentPalette = RainbowStripeColors_p; 133 | colorLoop = 1; 134 | } 135 | } 136 | 137 | // This function generates a random palette that's a gradient between four different colors. The first is a dim hue, the second is 138 | // a bright hue, the third is a bright pastel, and the last is 139 | // another bright hue. This gives some visual bright/dark variation 140 | // which is more interesting than just a gradient of different hues. 141 | void NoiseWithPaletteVisualizer::SetupRandomPalette() { 142 | currentPalette = CRGBPalette16(CHSV(random8(), 255, 32), 143 | CHSV(random8(), 255, 255), CHSV(random8(), 128, 255), 144 | CHSV(random8(), 255, 255)); 145 | } 146 | 147 | // This function sets up a palette of black and white stripes, 148 | // using code. Since the palette is effectively an array of 149 | // sixteen CRGB colors, the various fill_* functions can be used 150 | // to set them up. 151 | void NoiseWithPaletteVisualizer::SetupBlackAndWhiteStripedPalette() { 152 | // 'black out' all 16 palette entries... 153 | fill_solid(currentPalette, 16, CRGB::Black); 154 | // and set every fourth one to white. 155 | currentPalette[0] = CRGB::White; 156 | currentPalette[4] = CRGB::White; 157 | currentPalette[8] = CRGB::White; 158 | currentPalette[12] = CRGB::White; 159 | 160 | } 161 | 162 | // This function sets up a palette of purple and green stripes. 163 | void NoiseWithPaletteVisualizer::SetupPurpleAndGreenPalette() { 164 | CRGB purple = CHSV(HUE_PURPLE, 255, 255); 165 | CRGB green = CHSV(HUE_GREEN, 255, 255); 166 | CRGB black = CRGB::Black; 167 | 168 | currentPalette = CRGBPalette16(green, green, black, black, purple, purple, 169 | black, black, green, green, black, black, purple, purple, black, 170 | black); 171 | } 172 | 173 | void NoiseWithPaletteVisualizer::start() { 174 | x = random16(); 175 | y = random16(); 176 | z = random16(); 177 | } 178 | 179 | void NoiseWithPaletteVisualizer::stop() { 180 | } 181 | 182 | boolean NoiseWithPaletteVisualizer::onButtonPressed(uint8_t button) { 183 | boolean handled = false; 184 | 185 | switch (button) { 186 | case BTN_QUICK: 187 | handled = true; 188 | if (speed < 60) 189 | speed++; 190 | break; 191 | case BTN_SLOW: 192 | handled = true; 193 | if (speed > 0) 194 | speed--; 195 | break; 196 | 197 | case BTN_BLUE_DOWN: 198 | handled = true; 199 | scale -= 3; 200 | if (scale <= 4) 201 | scale = 4; 202 | break; 203 | case BTN_BLUE_UP: 204 | handled = true; 205 | scale += 3; 206 | if (scale >= 100) 207 | scale = 100; 208 | break; 209 | 210 | case BTN_RED_DOWN: 211 | handled = true; 212 | if (profile > 1) { 213 | profile--; 214 | applyProfile(); 215 | delay(200); 216 | } 217 | break; 218 | case BTN_RED_UP: 219 | handled = true; 220 | if (profile < 9) { 221 | profile++; 222 | applyProfile(); 223 | delay(200); 224 | } 225 | break; 226 | } 227 | 228 | return handled; 229 | } 230 | 231 | void NoiseWithPaletteVisualizer::readRuntimeConfiguration(int &address) { 232 | Logger.debug("NoiseWithPaletteVisualizer::readRuntimeConfiguration"); 233 | Visualizer::readRuntimeConfiguration(address); 234 | EEPROM.get(address, speed); 235 | address += sizeof(speed); 236 | EEPROM.get(address, scale); 237 | address += sizeof(scale); 238 | EEPROM.get(address, profile); 239 | address += sizeof(profile); 240 | } 241 | 242 | void NoiseWithPaletteVisualizer::writeRuntimeConfiguration(int &address) { 243 | Logger.debug("NoiseWithPaletteVisualizer::writeRuntimeConfiguration"); 244 | Visualizer::writeRuntimeConfiguration(address); 245 | EEPROM.put(address, speed); 246 | address += sizeof(speed); 247 | EEPROM.put(address, scale); 248 | address += sizeof(scale); 249 | EEPROM.put(address, profile); 250 | address += sizeof(profile); 251 | } 252 | -------------------------------------------------------------------------------- /SmartLEDLamp/NoiseWithPaletteVisualizer.h: -------------------------------------------------------------------------------- 1 | #ifndef NOISEWITHPALETTEVISUALIZER_H_ 2 | #define NOISEWITHPALETTEVISUALIZER_H_ 3 | 4 | #define FASTLED_INTERNAL 5 | 6 | #include "Visualizer.h" 7 | #include "FastLED.h" 8 | #include "defines.h" 9 | 10 | class NoiseWithPaletteVisualizer: public Visualizer { 11 | public: 12 | NoiseWithPaletteVisualizer(); 13 | virtual ~NoiseWithPaletteVisualizer(); 14 | 15 | virtual void start(); 16 | virtual void stop(); 17 | 18 | virtual boolean onButtonPressed(uint8_t button); 19 | 20 | virtual void readRuntimeConfiguration(int &address); 21 | virtual void writeRuntimeConfiguration(int &address); 22 | 23 | protected: 24 | virtual void computeImage(); 25 | 26 | void fillnoise8(); 27 | void mapNoiseToLEDsUsingPalette(); 28 | void applyProfile(); 29 | void SetupRandomPalette(); 30 | void SetupBlackAndWhiteStripedPalette(); 31 | void SetupPurpleAndGreenPalette(); 32 | 33 | private: 34 | uint16_t x = 0; 35 | uint16_t y = 0; 36 | uint16_t z = 0; 37 | 38 | uint8_t profile = 1; 39 | 40 | uint8_t noise[LEDS_HEIGHT][LEDS_HEIGHT]; 41 | uint16_t speed = 1; 42 | uint16_t scale = 15; 43 | CRGBPalette16 currentPalette; 44 | uint8_t colorLoop = 1; 45 | uint8_t ihue = 0; 46 | }; 47 | 48 | #endif /* NOISEWITHPALETTEVISUALIZER_H_ */ 49 | -------------------------------------------------------------------------------- /SmartLEDLamp/Runnable.cpp: -------------------------------------------------------------------------------- 1 | #include "Runnable.h" 2 | 3 | Runnable::Runnable(LEDMatrix* pLEDMatrix) : 4 | pMatrix(pLEDMatrix) { 5 | 6 | } 7 | 8 | Runnable::~Runnable() { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /SmartLEDLamp/Runnable.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNABLE_H_ 2 | #define RUNNABLE_H_ 3 | 4 | #include "LEDMatrix.h" 5 | 6 | class Runnable { 7 | public: 8 | Runnable(LEDMatrix* pLEDMatrix); 9 | virtual ~Runnable(); 10 | 11 | virtual void run() = 0; 12 | 13 | protected: 14 | LEDMatrix *pMatrix; 15 | unsigned long lastMillis = 0; 16 | }; 17 | 18 | #endif /* RUNNABLE_H_ */ 19 | -------------------------------------------------------------------------------- /SmartLEDLamp/RuntimeConfigurable.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNTIME_CONFIGURABLE_H_ 2 | #define RUNTIME_CONFIGURABLE_H_ 3 | 4 | class RuntimeConfigurable { 5 | public: 6 | virtual ~RuntimeConfigurable() {}; 7 | virtual void readRuntimeConfiguration(int &address) = 0; 8 | virtual void writeRuntimeConfiguration(int &address) = 0; 9 | }; 10 | 11 | #endif /* RUNTIME_CONFIGURABLE_H_ */ 12 | -------------------------------------------------------------------------------- /SmartLEDLamp/SmartLEDLamp.ino: -------------------------------------------------------------------------------- 1 | /* 2 | The purpose of this file is to make SmartLEDLamp a valid Arduino sketch. 3 | It is intentionally left blank. 4 | See the other files for the project source code. 5 | */ 6 | -------------------------------------------------------------------------------- /SmartLEDLamp/TurnOffRunnable.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * TurnOffRunnable.cpp 3 | */ 4 | 5 | #include "defines.h" 6 | #include "TurnOffRunnable.h" 7 | #include "LEDLamp.h" 8 | #include 9 | 10 | TurnOffRunnable::TurnOffRunnable(LEDMatrix* pLEDMatrix) : 11 | Runnable(pLEDMatrix) { 12 | } 13 | 14 | TurnOffRunnable::~TurnOffRunnable() { 15 | } 16 | 17 | void TurnOffRunnable::init() { 18 | currentOffset = pMatrix->getYOffset(); 19 | } 20 | 21 | void TurnOffRunnable::run() { 22 | unsigned long currentMillis = millis(); 23 | 24 | if (currentMillis - lastMillis < 10) { 25 | return; 26 | } 27 | 28 | lastMillis = currentMillis; 29 | 30 | if (currentOffset < pMatrix->getHeight()) { 31 | currentOffset = currentOffset + 2; 32 | pMatrix->setYOffset(currentOffset); 33 | } else { 34 | pMatrix->setYOffset(pMatrix->getHeight()); 35 | pCurrentRunnable = NULL; 36 | isOn = false; 37 | 38 | #ifdef MQTT_ENABLE 39 | mqttClient.publish(MQTT_TOPIC, "0", true); 40 | #endif // MQTT_ENABLE 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /SmartLEDLamp/TurnOffRunnable.h: -------------------------------------------------------------------------------- 1 | /* 2 | * TurnOffRunnable.h 3 | */ 4 | 5 | #ifndef TURNOFFRUNNABLE_H_ 6 | #define TURNOFFRUNNABLE_H_ 7 | 8 | #include "Runnable.h" 9 | #include "LEDMatrix.h" 10 | 11 | class TurnOffRunnable: public Runnable { 12 | public: 13 | TurnOffRunnable(LEDMatrix* pLEDMatrix); 14 | virtual ~TurnOffRunnable(); 15 | 16 | void init(); 17 | virtual void run(); 18 | 19 | protected: 20 | uint16_t currentOffset = 0; 21 | }; 22 | 23 | #endif /* TURNOFFRUNNABLE_H_ */ 24 | -------------------------------------------------------------------------------- /SmartLEDLamp/TurnOnRunnable.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * TurnOnRunnable.cpp 3 | */ 4 | 5 | #include "TurnOnRunnable.h" 6 | #include "LEDLamp.h" 7 | #include 8 | 9 | TurnOnRunnable::TurnOnRunnable(LEDMatrix* pLEDMatrix) : 10 | Runnable(pLEDMatrix) { 11 | } 12 | 13 | TurnOnRunnable::~TurnOnRunnable() { 14 | } 15 | 16 | void TurnOnRunnable::init() { 17 | currentOffset = pMatrix->getYOffset(); 18 | if (currentOffset == 0) { 19 | currentOffset = pMatrix->getHeight(); 20 | } 21 | pMatrix->setYOffset(currentOffset); 22 | } 23 | 24 | void TurnOnRunnable::run() { 25 | if (!isOn) { 26 | isOn = true; 27 | #ifdef MQTT_ENABLE 28 | mqttClient.publish(MQTT_TOPIC, "1", true); 29 | #endif // MQTT_ENABLE 30 | } 31 | 32 | unsigned long currentMillis = millis(); 33 | 34 | if (currentMillis - lastMillis < 10) { 35 | return; 36 | } 37 | 38 | lastMillis = currentMillis; 39 | 40 | if (currentOffset > 0) { 41 | currentOffset = currentOffset - 2; 42 | pMatrix->setYOffset(currentOffset); 43 | } else { 44 | pMatrix->setYOffset(0); 45 | pCurrentRunnable = NULL; 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /SmartLEDLamp/TurnOnRunnable.h: -------------------------------------------------------------------------------- 1 | /* 2 | * TurnOnRunnable.h 3 | */ 4 | 5 | #ifndef TURNONRUNNABLE_H_ 6 | #define TURNONRUNNABLE_H_ 7 | 8 | #include "Runnable.h" 9 | #include "LEDMatrix.h" 10 | 11 | class TurnOnRunnable: public Runnable { 12 | public: 13 | TurnOnRunnable(LEDMatrix* pLEDMatrix); 14 | virtual ~TurnOnRunnable(); 15 | 16 | void init(); 17 | virtual void run(); 18 | 19 | protected: 20 | uint16_t currentOffset = 0; 21 | }; 22 | 23 | #endif /* TURNONRUNNABLE_H_ */ 24 | -------------------------------------------------------------------------------- /SmartLEDLamp/VUMeterVisualizer.cpp: -------------------------------------------------------------------------------- 1 | #include "VUMeterVisualizer.h" 2 | 3 | VUMeterVisualizer::VUMeterVisualizer(float *pAmp) : 4 | pAmplitude(pAmp), cooling(95), sparking(200), paletteNo(6), paletteMax( 5 | 240) { 6 | } 7 | 8 | VUMeterVisualizer::~VUMeterVisualizer() { 9 | } 10 | 11 | void VUMeterVisualizer::computeImage() { 12 | int leds = LEDS_HEIGHT; 13 | 14 | int amp = *pAmplitude * 200; 15 | 16 | if (amp > LEDS_HEIGHT) 17 | amp = LEDS_HEIGHT; 18 | 19 | static char* pC = 20 | "###################################################################################"; 21 | 22 | pC[amp] = 0; 23 | Serial.println(pC); 24 | pC[amp] = '#'; 25 | 26 | // Serial.println(amp); 27 | 28 | for (int j = 0; j < leds; j++) { 29 | byte colorindex = 255 / leds * j; 30 | CRGB color = ColorFromPalette(palette[paletteNo], colorindex); 31 | int pixelnumber; 32 | for (int i = 0; i < 4; ++i) { 33 | pixelnumber = (leds - 1) - j; 34 | int pixelIdx = ((pixelnumber) * LEDS_WIDTH + i) * 3; 35 | 36 | if (j < amp) { 37 | imageData[pixelIdx + 0] = color.r; 38 | imageData[pixelIdx + 1] = color.g; 39 | imageData[pixelIdx + 2] = color.b; 40 | } else { 41 | imageData[pixelIdx + 0] = imageData[pixelIdx + 1] = 42 | imageData[pixelIdx + 2] = 0; 43 | } 44 | } 45 | } 46 | 47 | *pAmplitude *= 0.85f; 48 | // *pAmplitude = 0.0f; 49 | } 50 | 51 | void VUMeterVisualizer::start() { 52 | } 53 | 54 | void VUMeterVisualizer::stop() { 55 | } 56 | 57 | boolean VUMeterVisualizer::onButtonPressed(uint8_t button) { 58 | boolean handled = false; 59 | 60 | switch (button) { 61 | case 28: 62 | handled = true; 63 | if (rendererId > 0) 64 | rendererId--; 65 | delay(200); 66 | break; 67 | case 32: 68 | handled = true; 69 | if (rendererId < 2) 70 | rendererId++; 71 | delay(200); 72 | break; 73 | 74 | case 25: 75 | cooling -= 5; 76 | if (cooling <= 0) 77 | cooling = 0; 78 | handled = true; 79 | break; 80 | case 29: 81 | cooling += 5; 82 | if (cooling >= 1000) 83 | cooling = 1000; 84 | handled = true; 85 | break; 86 | case 26: 87 | sparking += 5; 88 | if (sparking >= 250) 89 | sparking = 250; 90 | handled = true; 91 | break; 92 | case 30: 93 | sparking -= 5; 94 | if (sparking <= 0) 95 | sparking = 0; 96 | handled = true; 97 | break; 98 | 99 | case 27: 100 | paletteMax += 5; 101 | if (paletteMax > 240) 102 | paletteMax = 240; 103 | handled = true; 104 | break; 105 | case 31: 106 | paletteMax -= 5; 107 | if (paletteMax < 50) 108 | paletteMax = 50; 109 | handled = true; 110 | break; 111 | 112 | case 33: 113 | paletteNo = 0; 114 | handled = true; 115 | break; 116 | case 34: 117 | paletteNo = 1; 118 | handled = true; 119 | break; 120 | case 35: 121 | paletteNo = 2; 122 | handled = true; 123 | break; 124 | case 36: 125 | paletteNo = 3; 126 | handled = true; 127 | break; 128 | case 37: 129 | paletteNo = 4; 130 | handled = true; 131 | break; 132 | case 38: 133 | paletteNo = 5; 134 | handled = true; 135 | break; 136 | } 137 | 138 | return handled; 139 | } 140 | -------------------------------------------------------------------------------- /SmartLEDLamp/VUMeterVisualizer.h: -------------------------------------------------------------------------------- 1 | #ifndef VUMETERVISUALIZER_H_ 2 | #define VUMETERVISUALIZER_H_ 3 | 4 | #define FASTLED_INTERNAL 5 | 6 | #include "Visualizer.h" 7 | #include "FastLED.h" 8 | #include "defines.h" 9 | 10 | class VUMeterVisualizer: public Visualizer { 11 | public: 12 | VUMeterVisualizer(float* pAmp); 13 | virtual ~VUMeterVisualizer(); 14 | 15 | virtual void start(); 16 | virtual void stop(); 17 | 18 | virtual boolean onButtonPressed(uint8_t button); 19 | 20 | protected: 21 | virtual void computeImage(); 22 | void computeImage1(); 23 | void computeImage2(); 24 | void computeImage3(); 25 | 26 | private: 27 | int8_t rendererId = 0; 28 | int8_t paletteNo; 29 | int16_t paletteMax; 30 | const TProgmemRGBPalette16 VUMeter_p FL_PROGMEM = { CRGB::Green, 31 | CRGB::Green, CRGB::Green, CRGB::Green, 32 | 33 | CRGB::Green, CRGB::Green, CRGB::Green, CRGB::Green, 34 | 35 | CRGB::Green, CRGB::Yellow, CRGB::Yellow, CRGB::Yellow, 36 | 37 | CRGB::Yellow, CRGB::Red, CRGB::Red, CRGB::Red }; 38 | CRGBPalette16 palette[6] = { CRGBPalette16(HeatColors_p), CRGBPalette16( 39 | PartyColors_p), CRGBPalette16(CloudColors_p), CRGBPalette16( 40 | LavaColors_p), CRGBPalette16(VUMeter_p), CRGBPalette16( 41 | OceanColors_p) }; 42 | byte heat[LEDS_WIDTH][LEDS_HEIGHT * 2]; 43 | 44 | int16_t cooling; 45 | int16_t sparking; 46 | 47 | uint8_t vImageData[LEDS_WIDTH * LEDS_HEIGHT * 2 * 3]; 48 | 49 | float* pAmplitude; 50 | 51 | }; 52 | 53 | #endif /* VUMETERVISUALIZER_H_ */ 54 | -------------------------------------------------------------------------------- /SmartLEDLamp/Visualizer.cpp: -------------------------------------------------------------------------------- 1 | #include "Visualizer.h" 2 | #include 3 | #include "Log.h" 4 | 5 | Visualizer::Visualizer() : 6 | speed(0), speedCounter(0) { 7 | memset(imageData, 0, sizeof(imageData) / sizeof(imageData[0])); 8 | } 9 | 10 | Visualizer::~Visualizer() { 11 | } 12 | 13 | uint8_t* Visualizer::renderNextImage() { 14 | if (speedCounter == 0) { 15 | computeImage(); 16 | } 17 | 18 | if (speedCounter++ >= speed) { 19 | speedCounter = 0; 20 | } 21 | 22 | return imageData; 23 | } 24 | 25 | void Visualizer::readRuntimeConfiguration(int &address) { 26 | Logger.debug("Visualizer::readRuntimeConfiguration"); 27 | EEPROM.get(address, speed); 28 | address += sizeof(speed); 29 | } 30 | 31 | void Visualizer::writeRuntimeConfiguration(int &address) { 32 | Logger.debug("Visualizer::writeRuntimeConfiguration"); 33 | EEPROM.write(address, speed); 34 | address += sizeof(speed); 35 | } 36 | -------------------------------------------------------------------------------- /SmartLEDLamp/Visualizer.h: -------------------------------------------------------------------------------- 1 | #ifndef VISUALIZER_H_ 2 | #define VISUALIZER_H_ 3 | 4 | #include 5 | #include "defines.h" 6 | #include "RuntimeConfigurable.h" 7 | 8 | class Visualizer: public virtual RuntimeConfigurable { 9 | public: 10 | Visualizer(); 11 | virtual ~Visualizer(); 12 | 13 | virtual void start() { 14 | } 15 | 16 | virtual void stop() { 17 | } 18 | 19 | uint8_t* renderNextImage(); 20 | 21 | uint8_t* getImage() { 22 | return imageData; 23 | } 24 | 25 | virtual boolean onButtonPressed(uint8_t button) { 26 | return false; 27 | } 28 | 29 | virtual void readRuntimeConfiguration(int &address); 30 | virtual void writeRuntimeConfiguration(int &address); 31 | 32 | protected: 33 | int8_t speed; 34 | int8_t speedCounter; 35 | 36 | uint8_t imageData[LEDS_WIDTH * LEDS_HEIGHT * 3]; 37 | 38 | virtual void computeImage() = 0; 39 | }; 40 | 41 | #endif /* VISUALIZER_H_ */ 42 | -------------------------------------------------------------------------------- /SmartLEDLamp/VisualizerApp.cpp: -------------------------------------------------------------------------------- 1 | #include "VisualizerApp.h" 2 | #include 3 | #include "defines.h" 4 | 5 | VisualizerApp::VisualizerApp(uint8_t appId, LEDMatrix *pLEDMatrix) : 6 | App(appId, pLEDMatrix), lastMillis(0) { 7 | name = "Visualizer"; 8 | } 9 | 10 | VisualizerApp::~VisualizerApp() { 11 | } 12 | 13 | void VisualizerApp::setVisualizer(uint8_t idx, Visualizer *pVisualizer) { 14 | visualizers[idx] = pVisualizer; 15 | } 16 | 17 | void VisualizerApp::start() { 18 | for (int i = 0; i < NO_VISUALIZERS; ++i) { 19 | if (visualizers[i]) { 20 | visualizers[i]->start(); 21 | } 22 | } 23 | } 24 | 25 | void VisualizerApp::stop() { 26 | for (int i = 0; i < NO_VISUALIZERS; ++i) { 27 | if (visualizers[i]) { 28 | visualizers[i]->stop(); 29 | } 30 | } 31 | } 32 | 33 | void VisualizerApp::readRuntimeConfiguration(int &address) { 34 | for (int i = 0; i < NO_VISUALIZERS; ++i) { 35 | if (visualizers[i]) { 36 | visualizers[i]->readRuntimeConfiguration(address); 37 | } 38 | } 39 | } 40 | 41 | void VisualizerApp::writeRuntimeConfiguration(int &address) { 42 | for (int i = 0; i < NO_VISUALIZERS; ++i) { 43 | if (visualizers[i]) { 44 | visualizers[i]->writeRuntimeConfiguration(address); 45 | } 46 | } 47 | } 48 | 49 | boolean VisualizerApp::onButtonPressed(uint8_t button) { 50 | boolean handled = false; 51 | 52 | for (int i = 0; i < NO_VISUALIZERS; ++i) { 53 | if (visualizers[i]) { 54 | handled |= visualizers[i]->onButtonPressed(button); 55 | } 56 | } 57 | 58 | return handled; 59 | } 60 | 61 | void VisualizerApp::update() { 62 | if (!pImageData[0] && visualizers[0]) { 63 | pImageData[0] = visualizers[0]->getImage(); 64 | } 65 | 66 | for (int i = 0; i < LEDS_WIDTH * LEDS_HEIGHT; ++i) { 67 | uint16_t r = 0, g = 0, b = 0; 68 | for (int j = 0; j < NO_VISUALIZERS; ++j) { 69 | if (pImageData[j]) { 70 | r += pImageData[j][i * 3]; 71 | if (r > 255) 72 | r = 255; 73 | 74 | g += pImageData[j][i * 3 + 1]; 75 | if (g > 255) 76 | g = 255; 77 | 78 | b += pImageData[j][i * 3 + 2]; 79 | if (b > 255) 80 | b = 255; 81 | } 82 | } 83 | pMatrix->setPixel(i % LEDS_WIDTH, i / LEDS_WIDTH, (uint8_t) (r), 84 | (uint8_t) (g), (uint8_t) (b)); 85 | } 86 | pMatrix->update(); 87 | } 88 | 89 | void VisualizerApp::run() { 90 | unsigned long currentMillis = millis(); 91 | 92 | // run with 25 fps 93 | if (currentMillis - lastMillis < 40) { 94 | return; 95 | } 96 | 97 | lastMillis = currentMillis; 98 | 99 | for (int i = 0; i < NO_VISUALIZERS; ++i) { 100 | if (visualizers[i]) { 101 | pImageData[i] = visualizers[i]->renderNextImage(); 102 | } else { 103 | pImageData[i] = NULL; 104 | } 105 | } 106 | 107 | update(); 108 | } 109 | -------------------------------------------------------------------------------- /SmartLEDLamp/VisualizerApp.h: -------------------------------------------------------------------------------- 1 | #ifndef VISUALIZERAPP_H_ 2 | #define VISUALIZERAPP_H_ 3 | 4 | #include "App.h" 5 | #include "Visualizer.h" 6 | 7 | #define NO_VISUALIZERS 2 8 | 9 | class VisualizerApp: public App { 10 | public: 11 | VisualizerApp(uint8_t appId, LEDMatrix* pLEDMatrix); 12 | virtual ~VisualizerApp(); 13 | 14 | virtual void start(); 15 | virtual void run(); 16 | virtual void update(); 17 | virtual void stop(); 18 | 19 | virtual boolean onButtonPressed(uint8_t button); 20 | 21 | virtual void readRuntimeConfiguration(int &address); 22 | virtual void writeRuntimeConfiguration(int &address); 23 | 24 | void setVisualizer(uint8_t idx, Visualizer* pVisualizer); 25 | 26 | private: 27 | Visualizer* visualizers[NO_VISUALIZERS] = { NULL, NULL }; 28 | unsigned long lastMillis; 29 | 30 | uint8_t* pImageData[NO_VISUALIZERS] = { NULL, NULL }; 31 | }; 32 | 33 | #endif /* VISUALIZERAPP_H_ */ 34 | -------------------------------------------------------------------------------- /SmartLEDLamp/data/config.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SmartLEDLamp 6 | 7 | 8 | 9 | 10 |

Smart LED Lamp - Configuration

11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
General
Hostname
 
Calibration
Red
Green
Blue
35 |
36 | 37 | 38 | 39 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /SmartLEDLamp/data/control.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /SmartLEDLamp/data/jquery-3.1.1.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery v3.1.1 | (c) jQuery Foundation | jquery.org/license */ 2 | !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.1.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,B=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,C=/^.[^:#\[\.,]*$/;function D(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}):C.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(D(this,a||[],!1))},not:function(a){return this.pushStack(D(this,a||[],!0))},is:function(a){return!!D(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var E,F=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,G=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||E,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:F.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)),B.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)};G.prototype=r.fn,E=r(d);var H=/^(?:parents|prev(?:Until|All))/,I={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 J(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 J(a,"nextSibling")},prev:function(a){return J(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 a.contentDocument||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&&(I[a]||r.uniqueSort(e),H.test(a)&&e.reverse()),this.pushStack(e)}});var K=/[^\x20\t\r\n\f]+/g;function L(a){var b={};return r.each(a.match(K)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?L(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(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 M(a){return a}function N(a){throw a}function O(a,b,c){var d;try{a&&r.isFunction(d=a.promise)?d.call(a).done(b).fail(c):a&&r.isFunction(d=a.then)?d.call(a,b,c):b.call(void 0,a)}catch(a){c.call(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!==N&&(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:M,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:M)),c[2][3].add(g(0,a,r.isFunction(d)?d:N))}).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&&(O(a,g.done(h(c)).resolve,g.reject),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)O(e[c],h(c),g.reject);return g.promise()}});var P=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&P.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var Q=r.Deferred();r.fn.ready=function(a){return Q.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,holdReady:function(a){a?r.readyWait++:r.ready(!0)},ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||Q.resolveWith(d,[r]))}}),r.ready.then=Q.then;function R(){d.removeEventListener("DOMContentLoaded",R), 3 | a.removeEventListener("load",R),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",R),a.addEventListener("load",R));var S=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)S(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(){W.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=V.get(a,b),c&&(!d||r.isArray(c)?d=V.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 V.get(a,c)||V.access(a,c,{empty:r.Callbacks("once memory").add(function(){V.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,ka=/^$|\/(?:java|ecma)script/i,la={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};la.optgroup=la.option,la.tbody=la.tfoot=la.colgroup=la.caption=la.thead,la.th=la.td;function ma(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&r.nodeName(a,b)?r.merge([a],c):c}function na(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=ma(l.appendChild(f),"script"),j&&na(g),c){k=0;while(f=g[k++])ka.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 qa=d.documentElement,ra=/^key/,sa=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ta=/^([^.]*)(?:\.(.+)|)/;function ua(){return!0}function va(){return!1}function wa(){try{return d.activeElement}catch(a){}}function xa(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)xa(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=va;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=V.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(qa,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(K)||[""],j=b.length;while(j--)h=ta.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=V.hasData(a)&&V.get(a);if(q&&(i=q.events)){b=(b||"").match(K)||[""],j=b.length;while(j--)if(h=ta.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)&&V.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=(V.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,za=/\s*$/g;function Da(a,b){return r.nodeName(a,"table")&&r.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a:a}function Ea(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Fa(a){var b=Ba.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ga(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(V.hasData(a)&&(f=V.access(a),g=V.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&&Aa.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ia(f,b,c,d)});if(m&&(e=pa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(ma(e,"script"),Ea),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=ma(h),f=ma(a),d=0,e=f.length;d0&&na(g,!i&&ma(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(T(c)){if(b=c[V.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[V.expando]=void 0}c[W.expando]&&(c[W.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ja(this,a,!0)},remove:function(a){return Ja(this,a)},text:function(a){return S(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 Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.appendChild(a)}})},prepend:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ia(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(ma(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 S(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&&!za.test(a)&&!la[(ja.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function Ya(a,b,c,d,e){return new Ya.prototype.init(a,b,c,d,e)}r.Tween=Ya,Ya.prototype={constructor:Ya,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=Ya.propHooks[this.prop];return a&&a.get?a.get(this):Ya.propHooks._default.get(this)},run:function(a){var b,c=Ya.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):Ya.propHooks._default.set(this),this}},Ya.prototype.init.prototype=Ya.prototype,Ya.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)}}},Ya.propHooks.scrollTop=Ya.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=Ya.prototype.init,r.fx.step={};var Za,$a,_a=/^(?:toggle|show|hide)$/,ab=/queueHooks$/;function bb(){$a&&(a.requestAnimationFrame(bb),r.fx.tick())}function cb(){return a.setTimeout(function(){Za=void 0}),Za=r.now()}function db(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ba[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function eb(a,b,c){for(var d,e=(hb.tweeners[b]||[]).concat(hb.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)?ib:void 0)), 4 | 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&&r.nodeName(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(K);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),ib={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=jb[b]||r.find.attr;jb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=jb[g],jb[g]=e,e=null!=c(a,b,d)?g:null,jb[g]=f),e}});var kb=/^(?:input|select|textarea|button)$/i,lb=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return S(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):kb.test(a.nodeName)||lb.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 mb(a){var b=a.match(K)||[];return b.join(" ")}function nb(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,nb(this)))});if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=mb(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,nb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=mb(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,nb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(K)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=nb(this),b&&V.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":V.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+mb(nb(c))+" ").indexOf(b)>-1)return!0;return!1}});var ob=/\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+="":r.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(ob,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:mb(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(r.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 pb=/^(?: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&&!pb.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,pb.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=(V.get(h,"events")||{})[b.type]&&V.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&T(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||!T(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=V.access(d,b);e||d.addEventListener(a,c,!0),V.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=V.access(d,b)-1;e?V.access(d,b,e):(d.removeEventListener(a,c,!0),V.remove(d,b))}}});var qb=a.location,rb=r.now(),sb=/\?/;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 tb=/\[\]$/,ub=/\r?\n/g,vb=/^(?:submit|button|image|reset|file)$/i,wb=/^(?:input|select|textarea|keygen)/i;function xb(a,b,c,d){var e;if(r.isArray(b))r.each(b,function(b,e){c||tb.test(a)?d(a,e):xb(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)xb(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(r.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)xb(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")&&wb.test(this.nodeName)&&!vb.test(a)&&(this.checked||!ia.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:r.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(ub,"\r\n")}}):{name:b.name,value:c.replace(ub,"\r\n")}}).get()}});var yb=/%20/g,zb=/#.*$/,Ab=/([?&])_=[^&]*/,Bb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Cb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Db=/^(?:GET|HEAD)$/,Eb=/^\/\//,Fb={},Gb={},Hb="*/".concat("*"),Ib=d.createElement("a");Ib.href=qb.href;function Jb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(K)||[];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 Kb(a,b,c,d){var e={},f=a===Gb;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 Lb(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 Mb(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 Nb(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:qb.href,type:"GET",isLocal:Cb.test(qb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Hb,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?Lb(Lb(a,r.ajaxSettings),b):Lb(r.ajaxSettings,a)},ajaxPrefilter:Jb(Fb),ajaxTransport:Jb(Gb),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=Bb.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||qb.href)+"").replace(Eb,qb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(K)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Ib.protocol+"//"+Ib.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)),Kb(Fb,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=!Db.test(o.type),f=o.url.replace(zb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(yb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(sb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Ab,"$1"),n=(sb.test(f)?"&":"?")+"_="+rb++ +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]?", "+Hb+"; 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=Kb(Gb,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=Mb(o,y,d)),v=Nb(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 Ob={0:200,1223:204},Pb=r.ajaxSettings.xhr();o.cors=!!Pb&&"withCredentials"in Pb,o.ajax=Pb=!!Pb,r.ajaxTransport(function(b){var c,d;if(o.cors||Pb&&!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(Ob[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("