├── .gitignore ├── .sublimelinterrc ├── .travis.yml ├── .vscode ├── arduino.json └── c_cpp_properties.json ├── Changelog.md ├── EFUpdate.cpp ├── EFUpdate.h ├── ESPixelStick.h ├── ESPixelStick.ino ├── EffectEngine.cpp ├── EffectEngine.h ├── Mode.h ├── PixelDriver.cpp ├── PixelDriver.h ├── README.md ├── SerialDriver.cpp ├── SerialDriver.h ├── buttons.cpp ├── buttons.h ├── data ├── config.json ├── config.json.h801 └── config.json.orig ├── dist ├── ESPSFlashTool.jar ├── README.md ├── bin │ ├── linux32 │ │ ├── esptool │ │ └── mkspiffs │ ├── linux64 │ │ ├── esptool │ │ └── mkspiffs │ ├── osx │ │ ├── esptool │ │ └── mkspiffs │ └── win │ │ ├── esptool.exe │ │ └── mkspiffs.exe ├── firmware │ ├── README.md │ └── firmware.json └── lib │ ├── gson-2.7.jar │ └── jSerialComm-1.3.11.jar ├── gamma.cpp ├── gamma.h ├── gpio.cpp ├── gpio.h ├── gulpfile.js ├── html ├── README.md ├── css │ └── bootstrap.css ├── garage.html ├── index.html ├── js │ ├── bootstrap.js │ ├── jqColorPicker.js │ └── jquery-3.1.1.js ├── script.js └── style.css ├── package.json ├── pwm.cpp ├── pwm.h ├── rgbhsv.cpp ├── rgbhsv.h ├── travis ├── firmware.json └── travis.md ├── udpraw.cpp ├── udpraw.h └── wshandler.h /.gitignore: -------------------------------------------------------------------------------- 1 | dist/*.html 2 | node_modules 3 | *.gz 4 | *~ 5 | *.swp 6 | data/config.json 7 | .build 8 | package-lock.json 9 | -------------------------------------------------------------------------------- /.sublimelinterrc: -------------------------------------------------------------------------------- 1 | { 2 | "linters": { 3 | "cpplint": { 4 | "args": ["--extensions=ino,c,cpp,h"] 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: generic 2 | env: 3 | global: 4 | - IDE_VERSION=1.8.1 5 | - ESP_HOME=$HOME/ide/hardware/esp8266com 6 | - LIB_HOME=$HOME/Arduino/libraries 7 | - ESPS_HOME=$HOME/Arduino/ESPixelStick 8 | - BUILD=$HOME/build 9 | - DIST=$ESPS_HOME/dist 10 | - STAMP=$(date +'%Y%m%d%H%M%S') 11 | - DIST_FILE=ESPixelStick_TRAVIS-$STAMP.zip 12 | before_install: 13 | - wget http://downloads.arduino.cc/arduino-$IDE_VERSION-linux64.tar.xz 14 | - tar xf arduino-$IDE_VERSION-linux64.tar.xz 15 | - mv arduino-$IDE_VERSION $HOME/ide 16 | - export PATH="$HOME/ide:$PATH" 17 | - mkdir -p $LIB_HOME 18 | - mkdir -p $ESP_HOME 19 | - mkdir -p $BUILD 20 | - git clone --recurse-submodules -j8 https://github.com/esp8266/Arduino.git $ESP_HOME/esp8266 21 | - cd $ESP_HOME/esp8266/tools 22 | - python get.py 23 | - git clone https://github.com/bblanchon/ArduinoJson $LIB_HOME/ArduinoJson 24 | - git clone https://github.com/forkineye/ESPAsyncE131 $LIB_HOME/ESPAsyncE131 25 | - git clone https://github.com/me-no-dev/ESPAsyncTCP $LIB_HOME/ESPAsyncTCP 26 | - git clone https://github.com/me-no-dev/ESPAsyncUDP $LIB_HOME/ESPAsyncUDP 27 | - git clone https://github.com/me-no-dev/ESPAsyncWebServer $LIB_HOME/ESPAsyncWebServer 28 | - git clone https://github.com/marvinroger/async-mqtt-client $LIB_HOME/async-mqtt-client 29 | install: 30 | - cp -r $TRAVIS_BUILD_DIR $ESPS_HOME 31 | - arduino --board esp8266com:esp8266:generic:xtal=160,vt=flash,exception=disabled,ResetMethod=nodemcu,CrystalFreq=26,FlashFreq=40,FlashMode=dio,eesz=1M128,led=2,ip=lm2f,dbg=Disabled,lvl=None____,wipe=none,baud=115200 32 | --pref build.path=$BUILD --save-prefs 33 | before_script: 34 | - cd $ESPS_HOME 35 | - npm install -g gulp-cli 36 | - npm install 37 | script: 38 | - echo "#define ESPS_MODE_PIXEL" > $ESPS_HOME/Mode.h 39 | - arduino --verify $ESPS_HOME/ESPixelStick.ino 40 | - mv $BUILD/ESPixelStick.ino.bin $DIST/firmware/pixel-travis.bin 41 | - echo "#define ESPS_MODE_SERIAL" > $ESPS_HOME/Mode.h 42 | - arduino --verify $ESPS_HOME/ESPixelStick.ino 43 | - mv $BUILD/ESPixelStick.ino.bin $DIST/firmware/serial-travis.bin 44 | - gulp 45 | - gulp md 46 | - gulp travis 47 | - mv $ESPS_HOME/data $DIST/spiffs 48 | - mv $ESPS_HOME/travis/firmware.json $DIST/firmware/firmware.json 49 | - cd $DIST 50 | - zip -r $HOME/$DIST_FILE * 51 | notifications: 52 | email: 53 | on_success: change 54 | on_failure: change 55 | deploy: 56 | provider: releases 57 | api_key: 58 | secure: VJ/XLiQfMz7HOf8iA5p6ctRezOjiQzF69YZTzeji6eSB7kTbqpnLg+qcgaF6wmEchJHfe+7nDFuMClyOgnlF9htrnmumY6ssr/070Ehw276/CylrV8POCfL3WF6LEK/s04I7n6Lq6mDH/jQKDiQ/byp+Ir8BIXgznrJyt1Vsd9V/+w2IPZaJq4K/BbbbByzdhvWF+CF2fZDC6NdYw2ec/vxUFDqE5DnN9t5HvhzNo2/QyWeerWViRXIgSZOq72NxCcurNxPvQhY+H7iRHOJoCd7X4RdvAPNic3Fc+3tEpuMSMDjiIlzTTibATtBS3esl6I2h58nqhqdvjbDC2/pvCXCnWUWBmzB4VR1gKoDq7S5IlWhTJjhi5nOAnMKHdMBkJ7pOPJ1TIzxNq/Fqx/VRbTAZyt0s9MOOv/lfwlPj0WR98Lc0Xy5L0EzcWMZiUfnTcowg9GXLPibGQBF9l4vL8+isfAvgijcjwiPl7dG1dK4NHnggN9168rb6r4eQ5lJrsU6zc/jt6B6q4joD6IyvoNDIUOrJAtL26rzMvOZ1yVTXxynGzoGNmmd60jIcnC93KK7q9crf/4ofFii3PnHpz29asd/2s0VEC43vQWfw267t3iuXyBpiA47r6aggcoyEPk1FA0h2B5LK1uUJ1u+pBvTQsS3JKZpJ+24Z0H1PgfQ= 59 | name: ESPixelStick Firmware (Travis-CI build) 60 | body: This release includes binaries which were automatically built by Travis-CI and should not be considered stable. 61 | prerelease: true 62 | file: $HOME/$DIST_FILE 63 | skip_cleanup: true 64 | on: 65 | repo: forkineye/ESPixelStick 66 | branch: master 67 | -------------------------------------------------------------------------------- /.vscode/arduino.json: -------------------------------------------------------------------------------- 1 | { 2 | "board": "esp8266com:esp8266:generic", 3 | "configuration": "xtal=160,vt=flash,exception=disabled,ResetMethod=nodemcu,CrystalFreq=26,FlashFreq=40,FlashMode=dio,eesz=1M128,led=2,ip=lm2f,dbg=Disabled,lvl=None____,wipe=none,baud=115200", 4 | "sketch": "ESPixelStick.ino", 5 | "output": ".build" 6 | } -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "WSL", 5 | "includePath": [ 6 | "${workspaceFolder}/**", 7 | "${workspaceFolder}/../libraries/**", 8 | "${workspaceFolder}/../hardware/esp8266com/esp8266/**" 9 | ], 10 | "defines": [ 11 | "_DEBUG", 12 | "UNICODE", 13 | "_UNICODE" 14 | ], 15 | "cStandard": "c11", 16 | "cppStandard": "c++17", 17 | "intelliSenseMode": "gcc-x64", 18 | "forcedInclude": [] 19 | } 20 | ], 21 | "version": 4 22 | } -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | 2 | # Changelog 3 | 4 | ### 3.1-dev (on-going) 5 | 6 | - Added brightness support. 7 | - Added calculated gamma support. 8 | - Added grouping and zigzag for pixels. 9 | - Added startup and idle effect options. 10 | - Implemented a new method for GECE support utilzing the UART. 11 | - Better MQTT support (usage detailed in the README). 12 | - Removed PWM support. A version supporting PWM is maintaned by [penfold42](https://github.com/penfold42/ESPixelBoard). 13 | 14 | ### 3.0 15 | 16 | - Compiled against [2017.11.20 Arduino core](https://github.com/esp8266/Arduino/tree/117bc875ffdd1f4b11af0dd236e0e12a84748e53) 17 | - Includes WPA2 KRACK security fix 18 | - Migrated to [ESPAsyncE131](https://github.com/forkineye/ESPAsyncE131) library 19 | - Fixed [Issue #56](https://github.com/forkineye/ESPixelStick/issues/56) - Read too far 20 | - Fixed [Issue #65](https://github.com/forkineye/ESPixelStick/issues/65) - Websocket queuing 21 | - Fixed [Issue #80](https://github.com/forkineye/ESPixelStick/issues/80) - Test mode state reporting 22 | 23 | ### 3.0-rc3 24 | 25 | - Fixed IGMP timer bug in lwip (Fix submitted to ESP8266 Arduino project) 26 | 27 | ### 3.0-rc2 28 | 29 | - Fixed IGMP subscription bug introduced in 3.0-dev1 30 | - Fixed issue receiving E1.31 alternate start codes (fixed in E131 library) 31 | - Added Universe Boundary configuration 32 | 33 | ### 3.0-rc1 34 | 35 | - Fixed validation errors on wifi setup page. 36 | - Fixed AP reboot loop. 37 | - Fixed color order issue in testing interface. 38 | 39 | ### 3.0-dev1 40 | 41 | - Complete re-write of the web frontend. Requires a WebSockets capable browser. 42 | - Migrated to ESPAsyncUDP for E131 parsing (E131 library updated). 43 | - Increased WS2811 reset time from 50us to 300us to support newer WS2811 chips. 44 | - Added hostname configuration. 45 | - Added GBR and BGR color order for pixels. 46 | 47 | ### 2.0 48 | 49 | - Added web based OTA update capability. 50 | - Changed output type (pixel vs serial) to compile time option. Check top of main sketch. 51 | - Migrated to ESPAsyncWebserver for the web server. 52 | - Migrated all web pages to SPIFFS. 53 | - Migrated configuration structure to SPIFFS as a JSON file. 54 | - Added Gulp script to assist with preparing web pages. 55 | - Added mDNS / DNS-SD responder. 56 | - Made WS2811 stream generation asynchronous. 57 | - Removed PPU configuration for pixels. 58 | - Pixel data now will utilize all 512 channels of a universe if required. 59 | - Changed default UART for Renard / DMX to UART1. Can be changed in SerialDriver.h. 60 | - Made Serial stream generation asynchronous (Renard and DMX). 61 | - Added multi-universe support for Renard. 62 | - Added 480kbps for Renard. 63 | 64 | ### 1.4 65 | 66 | - Arduino 1.6.8 compatibility. 67 | - Added SoftAP support (credit: Bill Porter) 68 | - Added Renard Support (credit: Bill Porter) 69 | - Added DMX Support (credit: Bill Porter and Grayson Lough) 70 | 71 | ### 1.3 72 | 73 | - Fix for issues when compiling on Mac and Linux. 74 | - Web page optimizations. 75 | - Configuration structure update to fix alignment errors. 76 | 77 | ### 1.2 78 | 79 | - Added Administration Page. 80 | - Disconnect from WiFi before rebooting. 81 | - Code cleanup / housekeeping. 82 | 83 | ### 1.1 84 | 85 | - Migration to UART code for handling WS281x streams. 86 | - Initial GECE support. 87 | - Support for multiple universes. 88 | - Pixel per Universe configuration to allow subsets of multiple universes to be used. 89 | - Removed external library dependencies. 90 | - Added stream timeout and refresh overflow handling. 91 | 92 | ### 1.0 93 | 94 | - Initial release. 95 | - Single Universe. 96 | - WS281x only. 97 | -------------------------------------------------------------------------------- /EFUpdate.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * EFUpdate.cpp 3 | * 4 | * Project: ESPixelStick - An ESP8266 and E1.31 based pixel driver 5 | * Copyright (c) 2016 Shelby Merrick 6 | * http://www.forkineye.com 7 | * 8 | * This program is provided free for you to use in any way that you wish, 9 | * subject to the laws and regulations where you are using it. Due diligence 10 | * is strongly suggested before using this code. Please give credit where due. 11 | * 12 | * The Author makes no warranty of any kind, express or implied, with regard 13 | * to this program or the documentation contained in this document. The 14 | * Author shall not be liable in any event for incidental or consequential 15 | * damages in connection with, or arising out of, the furnishing, performance 16 | * or use of these programs. 17 | * 18 | */ 19 | 20 | #include 21 | #include 22 | #include 23 | #include "EFUpdate.h" 24 | 25 | void EFUpdate::begin() { 26 | _maxSketchSpace = (ESP.getFreeSketchSpace() - 0x1000) & 0xFFFFF000; 27 | _state = State::HEADER; 28 | _loc = 0; 29 | _error = EFUPDATE_ERROR_OK; 30 | } 31 | 32 | bool EFUpdate::process(uint8_t *data, size_t len) { 33 | size_t index = 0; 34 | bool retval = true; 35 | 36 | while (index < len) { 37 | switch (_state) { 38 | case State::HEADER: 39 | _header.raw[_loc++] = data[index++]; 40 | if (_loc == sizeof(efuheader_t)) { 41 | if (_header.signature == EFU_ID) { 42 | _header.version = ntohs(_header.version); 43 | memset(&_record, 0, sizeof(efurecord_t)); 44 | _loc = 0; 45 | _state = State::RECORD; 46 | } else { 47 | _state = State::FAIL; 48 | _error = EFUPDATE_ERROR_SIG; 49 | } 50 | } 51 | break; 52 | case State::RECORD: 53 | _record.raw[_loc++] = data[index++]; 54 | if (_loc == sizeof(efurecord_t)) { 55 | _record.type = RecordType(ntohs((uint16_t)_record.type)); 56 | _record.size = ntohl(_record.size); 57 | _loc = 0; 58 | if (_record.type == RecordType::SKETCH_IMAGE) { 59 | // Begin sketch update 60 | if (!Update.begin(_record.size, U_FLASH)) { 61 | _state = State::FAIL; 62 | _error = Update.getError(); 63 | } else { 64 | _state = State::DATA; 65 | } 66 | } else if (_record.type == RecordType::SPIFFS_IMAGE) { 67 | // Begin spiffs update 68 | SPIFFS.end(); 69 | if (!Update.begin(_record.size, U_SPIFFS)) { 70 | _state = State::FAIL; 71 | _error = Update.getError(); 72 | } else { 73 | _state = State::DATA; 74 | } 75 | } else { 76 | _state = State::FAIL; 77 | _error = EFUPDATE_ERROR_REC; 78 | } 79 | } 80 | break; 81 | case State::DATA: 82 | size_t toWrite; 83 | 84 | toWrite = (_record.size - _loc < len) ? _record.size - _loc : len - index; 85 | Update.write(data + index, toWrite); 86 | index = index + toWrite; 87 | _loc = _loc + toWrite; 88 | 89 | if (_record.size == _loc) { 90 | Update.end(true); 91 | memset(&_record, 0, sizeof(efurecord_t)); 92 | _loc = 0; 93 | _state = State::RECORD; 94 | } 95 | break; 96 | case State::FAIL: 97 | index = len; 98 | retval = false; 99 | break; 100 | } 101 | } 102 | 103 | return retval; 104 | } 105 | 106 | bool EFUpdate::hasError() { 107 | return _error != EFUPDATE_ERROR_OK; 108 | } 109 | 110 | uint8_t EFUpdate::getError() { 111 | return _error; 112 | } 113 | 114 | bool EFUpdate::end() { 115 | if (_state == State::FAIL) 116 | return false; 117 | else 118 | return true; 119 | } 120 | -------------------------------------------------------------------------------- /EFUpdate.h: -------------------------------------------------------------------------------- 1 | /* 2 | * EFUpdate.h 3 | * 4 | * Project: ESPixelStick - An ESP8266 and E1.31 based pixel driver 5 | * Copyright (c) 2016 Shelby Merrick 6 | * http://www.forkineye.com 7 | * 8 | * This program is provided free for you to use in any way that you wish, 9 | * subject to the laws and regulations where you are using it. Due diligence 10 | * is strongly suggested before using this code. Please give credit where due. 11 | * 12 | * The Author makes no warranty of any kind, express or implied, with regard 13 | * to this program or the documentation contained in this document. The 14 | * Author shall not be liable in any event for incidental or consequential 15 | * damages in connection with, or arising out of, the furnishing, performance 16 | * or use of these programs. 17 | * 18 | */ 19 | 20 | #ifndef EFUPDATE_H_ 21 | #define EFUPDATE_H_ 22 | 23 | #define EFUPDATE_ERROR_OK (0) 24 | #define EFUPDATE_ERROR_SIG (100) 25 | #define EFUPDATE_ERROR_REC (101) 26 | 27 | class EFUpdate { 28 | public: 29 | const uint32_t EFU_ID = 0x00554645; // 'E', 'F', 'U', 0x00 30 | 31 | void begin(); 32 | bool process(uint8_t *data, size_t len); 33 | bool hasError(); 34 | uint8_t getError(); 35 | bool end(); 36 | 37 | private: 38 | /* Record types */ 39 | enum class RecordType : uint16_t { 40 | NULL_RECORD, 41 | SKETCH_IMAGE, 42 | SPIFFS_IMAGE, 43 | EEPROM_IMAGE 44 | }; 45 | 46 | /* Update State */ 47 | enum class State : uint8_t { 48 | HEADER, 49 | RECORD, 50 | DATA, 51 | FAIL 52 | }; 53 | 54 | /* EFU Header */ 55 | typedef union { 56 | struct { 57 | uint32_t signature; 58 | uint16_t version; 59 | } __attribute__((packed)); 60 | 61 | uint8_t raw[6]; 62 | } efuheader_t; 63 | 64 | /* EFU Record */ 65 | typedef union { 66 | struct { 67 | RecordType type; 68 | uint32_t size; 69 | } __attribute__((packed)); 70 | 71 | uint8_t raw[6]; 72 | } efurecord_t; 73 | 74 | State _state = State::FAIL; 75 | size_t _loc = 0; 76 | efuheader_t _header; 77 | efurecord_t _record; 78 | uint32_t _maxSketchSpace; 79 | uint8_t _error; 80 | }; 81 | 82 | #endif /* EFUPDATE_H_ */ 83 | -------------------------------------------------------------------------------- /ESPixelStick.h: -------------------------------------------------------------------------------- 1 | /* 2 | * ESPixelStick.h 3 | * 4 | * Project: ESPixelStick - An ESP8266 and E1.31 based pixel driver 5 | * Copyright (c) 2016 Shelby Merrick 6 | * http://www.forkineye.com 7 | * 8 | * This program is provided free for you to use in any way that you wish, 9 | * subject to the laws and regulations where you are using it. Due diligence 10 | * is strongly suggested before using this code. Please give credit where due. 11 | * 12 | * The Author makes no warranty of any kind, express or implied, with regard 13 | * to this program or the documentation contained in this document. The 14 | * Author shall not be liable in any event for incidental or consequential 15 | * damages in connection with, or arising out of, the furnishing, performance 16 | * or use of these programs. 17 | * 18 | */ 19 | 20 | #ifndef ESPIXELSTICK_H_ 21 | #define ESPIXELSTICK_H_ 22 | 23 | const char VERSION[] = "3.1-dev"; 24 | const char BUILD_DATE[] = __DATE__; 25 | 26 | // Mode configuration moved to Mode.h to ease things with Travis 27 | #include "Mode.h" 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | 37 | #if defined(ESPS_MODE_PIXEL) 38 | #include "PixelDriver.h" 39 | #elif defined(ESPS_MODE_SERIAL) 40 | #include "SerialDriver.h" 41 | #endif 42 | 43 | #include "EffectEngine.h" 44 | 45 | #define HTTP_PORT 80 /* Default web server port */ 46 | #define MQTT_PORT 1883 /* Default MQTT port */ 47 | #define DATA_PIN 2 /* Pixel output - GPIO2 (D4 on NodeMCU) */ 48 | #define UNIVERSE_MAX 512 /* Max channels in a DMX Universe */ 49 | #define PIXEL_LIMIT 1360 /* Total pixel limit - 40.85ms for 8 universes */ 50 | #define RENARD_LIMIT 2048 /* Channel limit for serial outputs */ 51 | #define E131_TIMEOUT 1000 /* Force refresh every second an E1.31 packet is not seen */ 52 | #define CLIENT_TIMEOUT 15 /* In station/client mode try to connection for 15 seconds */ 53 | #define AP_TIMEOUT 60 /* In AP mode, wait 60 seconds for a connection or reboot */ 54 | #define REBOOT_DELAY 100 /* Delay for rebooting once reboot flag is set */ 55 | #define LOG_PORT Serial /* Serial port for console logging */ 56 | 57 | // E1.33 / RDMnet stuff - to be moved to library 58 | #define RDMNET_DNSSD_SRV_TYPE "draft-e133.tcp" 59 | #define RDMNET_DEFAULT_SCOPE "default" 60 | #define RDMNET_DEFAULT_DOMAIN "local" 61 | #define RDMNET_DNSSD_TXTVERS 1 62 | #define RDMNET_DNSSD_E133VERS 1 63 | 64 | // Configuration file params 65 | #define CONFIG_MAX_SIZE 4096 /* Sanity limit for config file */ 66 | 67 | // Pixel Types 68 | class DevCap { 69 | public: 70 | bool MPIXEL : 1; 71 | bool MSERIAL : 1; 72 | bool MPWM : 1; 73 | uint8_t toInt() { 74 | return (MPWM << 2 | MSERIAL << 1 | MPIXEL); 75 | } 76 | }; 77 | 78 | // Data Source to use 79 | enum class DataSource : uint8_t { 80 | E131, 81 | MQTT, 82 | WEB, 83 | IDLEWEB 84 | }; 85 | 86 | // Configuration structure 87 | typedef struct { 88 | /* Device */ 89 | String id; /* Device ID */ 90 | DevCap devmode; /* Used for reporting device mode, not stored */ 91 | DataSource ds; /* Used to track current data source, not stored */ 92 | 93 | 94 | /* Network */ 95 | String ssid; 96 | String passphrase; 97 | String hostname; 98 | uint8_t ip[4]; 99 | uint8_t netmask[4]; 100 | uint8_t gateway[4]; 101 | bool dhcp; /* Use DHCP? */ 102 | bool ap_fallback; /* Fallback to AP if fail to associate? */ 103 | uint32_t sta_timeout; /* Timeout when connection as client (station) */ 104 | uint32_t ap_timeout; /* How long to wait in AP mode with no connection before rebooting */ 105 | 106 | /* UDP raw protocol */ 107 | bool udp_enabled; 108 | uint16_t udp_port; 109 | 110 | /* Effects */ 111 | String effect_name; 112 | CRGB effect_color; 113 | float effect_brightness; 114 | uint16_t effect_speed; /* 1..10 for web UI and MQTT */ 115 | bool effect_reverse; 116 | bool effect_mirror; 117 | bool effect_allleds; 118 | bool effect_startenabled; 119 | bool effect_idleenabled; 120 | uint16_t effect_idletimeout; 121 | 122 | /* Effect engine send over network */ 123 | int effect_sendprotocol; 124 | String effect_sendhost; 125 | IPAddress effect_sendIP; 126 | uint16_t effect_sendport; 127 | float effect_sendspeed; 128 | bool effect_sendmulticast; 129 | 130 | /* MQTT */ 131 | bool mqtt; /* Use MQTT? */ 132 | String mqtt_ip = " "; 133 | uint16_t mqtt_port; 134 | String mqtt_user = " "; 135 | String mqtt_password = " "; 136 | String mqtt_topic; 137 | bool mqtt_clean; 138 | bool mqtt_hadisco; 139 | String mqtt_haprefix; 140 | 141 | /* E131 */ 142 | uint16_t universe; /* Universe to listen for */ 143 | uint16_t universe_limit; /* Universe boundary limit */ 144 | uint16_t channel_start; /* Channel to start listening at - 1 based */ 145 | uint16_t channel_count; /* Number of channels */ 146 | bool multicast; /* Enable multicast listener */ 147 | 148 | #if defined(ESPS_MODE_PIXEL) 149 | /* Pixels */ 150 | PixelType pixel_type; /* Pixel type */ 151 | PixelColor pixel_color; /* Pixel color order */ 152 | uint16_t zigSize; /* Zigsize count - 0 = no zigzag */ 153 | uint16_t groupSize; /* Group size - 1 = no grouping */ 154 | float gammaVal; /* gamma value to use */ 155 | float briteVal; /* brightness lto use */ 156 | #elif defined(ESPS_MODE_SERIAL) 157 | /* Serial */ 158 | SerialType serial_type; /* Serial type */ 159 | BaudRate baudrate; /* Baudrate */ 160 | #endif 161 | 162 | #if defined(ESPS_SUPPORT_PWM) 163 | bool pwm_global_enabled; /* is pwm runtime enabled? */ 164 | int pwm_freq; /* pwm frequency */ 165 | bool pwm_gamma; /* is pwm gamma enabled? */ 166 | uint16_t pwm_gpio_dmx[17]; /* which dmx channel is gpio[n] mapped to? */ 167 | uint32_t pwm_gpio_enabled; /* is gpio[n] enabled? */ 168 | uint32_t pwm_gpio_invert; /* is gpio[n] active high or active low? */ 169 | uint32_t pwm_gpio_digital; /* is gpio[n] digital or "analog"? */ 170 | String pwm_gpio_comment[17]; /* Free text description */ 171 | #endif 172 | } config_t; 173 | 174 | // Forward Declarations 175 | void serializeConfig(String &jsonString, bool pretty = false, bool creds = false); 176 | void dsNetworkConfig(JsonObject &json); 177 | void dsDeviceConfig(JsonObject &json); 178 | void dsEffectConfig(JsonObject &json); 179 | void saveConfig(); 180 | void dsGammaConfig(JsonObject &json); 181 | 182 | void connectWifi(); 183 | void onWifiConnect(const WiFiEventStationModeGotIP &event); 184 | void onWiFiDisconnect(const WiFiEventStationModeDisconnected &event); 185 | void connectToMqtt(); 186 | void onMqttConnect(bool sessionPresent); 187 | void onMqttDisconnect(AsyncMqttClientDisconnectReason reason); 188 | void onMqttMessage(char* topic, char* p_payload, 189 | AsyncMqttClientMessageProperties properties, size_t len,size_t index, size_t total); 190 | void publishState(); 191 | void publishRGBState(); 192 | void publishRGBBrightness(); 193 | void publishRGBColor(); 194 | void setStatic(uint8_t r, uint8_t g, uint8_t b); 195 | void idleTimeout(); 196 | 197 | 198 | #endif // ESPIXELSTICK_H_ 199 | -------------------------------------------------------------------------------- /EffectEngine.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include "ESPixelStick.h" 4 | #include "EffectEngine.h" 5 | 6 | extern config_t config; 7 | 8 | // List of all the supported effects and their names 9 | const EffectDesc EFFECT_LIST[] = { 10 | // Mirror AllLeds 11 | // name; func; htmlid; Color; Reverse wsTCode 12 | 13 | { "Disabled", nullptr, "t_disabled", 1, 1, 1, 1, "T0" }, 14 | { "Solid", &EffectEngine::effectSolidColor, "t_static", 1, 0, 0, 0, "T1" }, 15 | { "Blink", &EffectEngine::effectBlink, "t_blink", 1, 0, 0, 0, "T2" }, 16 | { "Flash", &EffectEngine::effectFlash, "t_flash", 1, 0, 0, 0, "T3" }, 17 | { "Rainbow", &EffectEngine::effectRainbow, "t_rainbow", 1, 1, 1, 1, "T5" }, 18 | { "Chase", &EffectEngine::effectChase, "t_chase", 1, 1, 1, 0, "T4" }, 19 | { "Fire flicker", &EffectEngine::effectFireFlicker,"t_fireflicker", 1, 0, 0, 0, "T6" }, 20 | { "Lightning", &EffectEngine::effectLightning, "t_lightning", 1, 0, 0, 0, "T7" }, 21 | { "Breathe", &EffectEngine::effectBreathe, "t_breathe", 1, 0, 0, 0, "T8" } 22 | }; 23 | 24 | // Effect defaults 25 | #define DEFAULT_EFFECT_NAME "Disabled" 26 | #define DEFAULT_EFFECT_COLOR { 183, 0, 255 } 27 | #define DEFAULT_EFFECT_BRIGHTNESS 1.0 28 | #define DEFAULT_EFFECT_REVERSE false 29 | #define DEFAULT_EFFECT_MIRROR false 30 | #define DEFAULT_EFFECT_ALLLEDS false 31 | #define DEFAULT_EFFECT_SPEED 6 32 | 33 | EffectEngine::EffectEngine() { 34 | // Initialize with defaults 35 | setFromDefaults(); 36 | } 37 | 38 | void EffectEngine::setFromDefaults() { 39 | config.effect_name = DEFAULT_EFFECT_NAME; 40 | config.effect_color = DEFAULT_EFFECT_COLOR; 41 | config.effect_brightness = DEFAULT_EFFECT_BRIGHTNESS; 42 | config.effect_reverse = DEFAULT_EFFECT_REVERSE; 43 | config.effect_mirror = DEFAULT_EFFECT_MIRROR; 44 | config.effect_allleds = DEFAULT_EFFECT_ALLLEDS; 45 | config.effect_speed = DEFAULT_EFFECT_SPEED; 46 | setFromConfig(); 47 | } 48 | 49 | void EffectEngine::setFromConfig() { 50 | setEffect(config.effect_name); 51 | setColor(config.effect_color); 52 | setBrightness(config.effect_brightness); 53 | setReverse(config.effect_reverse); 54 | setMirror(config.effect_mirror); 55 | setAllLeds(config.effect_allleds); 56 | setSpeed(config.effect_speed); 57 | } 58 | 59 | void EffectEngine::setBrightness(float brightness) { 60 | _effectBrightness = brightness; 61 | if (_effectBrightness > 1.0) 62 | _effectBrightness = 1.0; 63 | if (_effectBrightness < 0.0) 64 | _effectBrightness = 0.0; 65 | } 66 | 67 | // Yukky maths here. Input speeds from 1..10 get mapped to 17782..100 68 | void EffectEngine::setSpeed(uint16_t speed) { 69 | _effectSpeed = speed; 70 | setDelay( pow (10, (10-speed)/4.0 +2 ) ); 71 | } 72 | 73 | void EffectEngine::setDelay(uint16_t delay) { 74 | _effectDelay = delay; 75 | if (_effectDelay < MIN_EFFECT_DELAY) 76 | _effectDelay = MIN_EFFECT_DELAY; 77 | } 78 | 79 | void EffectEngine::begin(DRIVER* ledDriver, uint16_t ledCount) { 80 | _ledDriver = ledDriver; 81 | _ledCount = ledCount; 82 | _initialized = true; 83 | _forwarder.begin(9374); 84 | } 85 | 86 | void EffectEngine::run() { 87 | if (_initialized && _activeEffect && _activeEffect->func) { 88 | if (millis() - _effectLastRun >= _effectWait) { 89 | _effectLastRun = millis(); 90 | uint16_t wait = (this->*_activeEffect->func)(); 91 | _effectWait = max((int)wait, MIN_EFFECT_DELAY); 92 | _effectCounter++; 93 | } 94 | } 95 | } 96 | 97 | void EffectEngine::setEffect(const String effectName) { 98 | const uint8_t effectCount = sizeof(EFFECT_LIST) / sizeof(EffectDesc); 99 | for (uint8_t effect = 0; effect < effectCount; effect++) { 100 | if ( effectName.equalsIgnoreCase(EFFECT_LIST[effect].name) ) { 101 | if (_activeEffect != &EFFECT_LIST[effect]) { 102 | _activeEffect = &EFFECT_LIST[effect]; 103 | _effectLastRun = millis(); 104 | _effectWait = MIN_EFFECT_DELAY; 105 | _effectCounter = 0; 106 | _effectStep = 0; 107 | } 108 | return; 109 | } 110 | } 111 | 112 | _activeEffect = nullptr; 113 | clearAll(); 114 | } 115 | 116 | int EffectEngine::getEffectCount() { 117 | return sizeof(EFFECT_LIST) / sizeof(EffectDesc); 118 | } 119 | 120 | // find effect info by its index in the table 121 | const EffectDesc* EffectEngine::getEffectInfo(unsigned a) { 122 | if (a >= sizeof(EFFECT_LIST) / sizeof(EffectDesc)) 123 | a = 0; 124 | 125 | return &EFFECT_LIST[a]; 126 | } 127 | 128 | // find effect info by its web services Tcode 129 | const EffectDesc* EffectEngine::getEffectInfo(const String TCode) { 130 | const uint8_t effectCount = sizeof(EFFECT_LIST) / sizeof(EffectDesc); 131 | for (uint8_t effect = 0; effect < effectCount; effect++) { 132 | if ( TCode.equalsIgnoreCase(EFFECT_LIST[effect].wsTCode) ) { 133 | return &EFFECT_LIST[effect]; 134 | } 135 | } 136 | return nullptr; 137 | } 138 | 139 | bool EffectEngine::isValidEffect(const String effectName) { 140 | const uint8_t effectCount = sizeof(EFFECT_LIST) / sizeof(EffectDesc); 141 | for (uint8_t effect = 0; effect < effectCount; effect++) { 142 | if ( effectName.equalsIgnoreCase(EFFECT_LIST[effect].name) ) { 143 | return true; 144 | } 145 | } 146 | return false; 147 | } 148 | 149 | void EffectEngine::setPixel(uint16_t idx, CRGB color) { 150 | _ledDriver->setValue(3 * idx + 0, (uint8_t)(color.r * _effectBrightness) ); 151 | _ledDriver->setValue(3 * idx + 1, (uint8_t)(color.g * _effectBrightness) ); 152 | _ledDriver->setValue(3 * idx + 2, (uint8_t)(color.b * _effectBrightness) ); 153 | } 154 | 155 | void EffectEngine::setRange(uint16_t first, uint16_t len, CRGB color) { 156 | for (uint16_t i=first; i < min(uint16_t(first+len), _ledCount); i++) { 157 | setPixel(i, color); 158 | } 159 | } 160 | 161 | void EffectEngine::clearRange(uint16_t first, uint16_t len) { 162 | for (uint16_t i=first; i < min(uint16_t(first+len), _ledCount); i++) { 163 | setPixel(i, {0, 0, 0}); 164 | } 165 | } 166 | 167 | void EffectEngine::setAll(CRGB color) { 168 | setRange(0, _ledCount, color); 169 | } 170 | 171 | void EffectEngine::clearAll() { 172 | clearRange(0, _ledCount); 173 | } 174 | 175 | CRGB EffectEngine::colorWheel(uint8_t pos) { 176 | pos = 255 - pos; 177 | if (pos < 85) { 178 | return { 255 - pos * 3, 0, pos * 3}; 179 | } else if (pos < 170) { 180 | pos -= 85; 181 | return { 0, pos * 3, 255 - pos * 3 }; 182 | } else { 183 | pos -= 170; 184 | return { pos * 3, 255 - pos * 3, 0 }; 185 | } 186 | } 187 | 188 | uint16_t EffectEngine::effectSolidColor() { 189 | for (uint16_t i=0; i < _ledCount; i++) { 190 | setPixel(i, _effectColor); 191 | } 192 | return 32; 193 | } 194 | 195 | uint16_t EffectEngine::effectChase() { 196 | // calculate only half the pixels if mirroring 197 | uint16_t lc = _ledCount; 198 | if (_effectMirror) { 199 | lc = lc / 2; 200 | } 201 | // Prevent errors if we come from another effect with more steps 202 | // or switch from the upper half of non-mirror to mirror mode 203 | _effectStep = _effectStep % lc; 204 | 205 | for (uint16_t i=0; i < lc; i++) { 206 | if (i != _effectStep) { 207 | if (_effectMirror) { 208 | setPixel(i + lc, {0, 0, 0}); 209 | setPixel(lc - 1 - i, {0, 0, 0}); 210 | } else { 211 | setPixel(i, {0, 0, 0}); 212 | } 213 | } 214 | } 215 | uint16_t pixel = _effectStep; 216 | if (_effectReverse) { 217 | pixel = lc - 1 - pixel; 218 | } 219 | if (_effectMirror) { 220 | setPixel(pixel + lc, _effectColor); 221 | setPixel(lc - 1 - pixel, _effectColor); 222 | } else { 223 | setPixel(pixel, _effectColor); 224 | } 225 | 226 | _effectStep = (1+_effectStep) % lc; 227 | return _effectDelay / 32; 228 | } 229 | 230 | uint16_t EffectEngine::effectRainbow() { 231 | // calculate only half the pixels if mirroring 232 | uint16_t lc = _ledCount; 233 | if (_effectMirror) { 234 | lc = lc / 2; 235 | } 236 | for (uint16_t i=0; i < lc; i++) { 237 | // CRGB color = colorWheel(((i * 256 / lc) + _effectStep) & 0xFF); 238 | 239 | double hue = 0; 240 | if (_effectAllLeds) { 241 | hue = _effectStep*360.0d / 256; // all same colour 242 | } else { 243 | hue = 360.0 * (((i * 256 / lc) + _effectStep) & 0xFF) / 255; 244 | } 245 | // dCHSV hue 0->360 sat 0->1.0 val 0->1.0 246 | dCHSV my_hsv = rgb2hsv(_effectColor); 247 | double sat = my_hsv.s; 248 | double val = my_hsv.v; 249 | CRGB color = hsv2rgb ( { hue, sat, val } ); 250 | 251 | uint16_t pixel = i; 252 | if (_effectReverse) { 253 | pixel = lc - 1 - pixel; 254 | } 255 | if (_effectMirror) { 256 | setPixel(pixel + lc, color); 257 | setPixel(lc - 1 - pixel, color); 258 | } else { 259 | setPixel(pixel, color); 260 | } 261 | } 262 | 263 | _effectStep = (1+_effectStep) & 0xFF; 264 | return _effectDelay / 256; 265 | } 266 | 267 | uint16_t EffectEngine::effectBlink() { 268 | // The Blink effect uses two "time slots": on, off 269 | // Using default delay, a complete sequence takes 2s. 270 | if (_effectStep % 2) { 271 | clearAll(); 272 | } else { 273 | setAll(_effectColor); 274 | } 275 | 276 | _effectStep = (1+_effectStep) % 2; 277 | return _effectDelay / 1; 278 | } 279 | 280 | uint16_t EffectEngine::effectFlash() { 281 | // The Flash effect uses 6 "time slots": on, off, on, off, off, off 282 | // Using default delay, a complete sequence takes 2s. 283 | // Prevent errors if we come from another effect with more steps 284 | _effectStep = _effectStep % 6; 285 | 286 | switch (_effectStep) { 287 | case 0: 288 | case 2: 289 | setAll(_effectColor); 290 | break; 291 | default: 292 | clearAll(); 293 | } 294 | 295 | _effectStep = (1+_effectStep) % 6; 296 | return _effectDelay / 3; 297 | } 298 | 299 | uint16_t EffectEngine::effectFireFlicker() { 300 | byte rev_intensity = 6; // more=less intensive, less=more intensive 301 | byte lum = max(_effectColor.r, max(_effectColor.g, _effectColor.b)) / rev_intensity; 302 | for ( int i = 0; i < _ledCount; i++) { 303 | byte flicker = random(lum); 304 | setPixel(i, CRGB { max(_effectColor.r - flicker, 0), max(_effectColor.g - flicker, 0), max(_effectColor.b - flicker, 0) }); 305 | } 306 | _effectStep = (1+_effectStep) % _ledCount; 307 | return _effectDelay / 10; 308 | } 309 | 310 | uint16_t EffectEngine::effectLightning() { 311 | static byte maxFlashes; 312 | static int timeslot = _effectDelay / 1000; // 1ms 313 | int flashPause = 10; // 10ms 314 | uint16_t ledStart = random(_ledCount); 315 | uint16_t ledLen = random(1, _ledCount - ledStart); 316 | byte intensity; // flash intensity 317 | 318 | if (_effectStep % 2) { 319 | // odd steps = clear 320 | clearAll(); 321 | if (_effectStep == 1) { 322 | // pause after 1st flash is longer 323 | flashPause = 130; 324 | } else { 325 | flashPause = random(50, 151); // pause between flashes 50-150ms 326 | } 327 | } else { 328 | // even steps = flashes 329 | if (_effectStep == 0) { 330 | // first flash (weaker and longer pause) 331 | maxFlashes = random(3, 8); // 2-6 follow-up flashes 332 | intensity = random(128); 333 | } else { 334 | // follow-up flashes (stronger) 335 | intensity = random(128, 256); // next flashes are stronger 336 | } 337 | CRGB temprgb = { _effectColor.r*intensity/256, _effectColor.g*intensity/256, _effectColor.b*intensity/256 }; 338 | setRange(ledStart, ledLen, temprgb ); 339 | flashPause = random(4, 21); // flash duration 4-20ms 340 | } 341 | 342 | _effectStep++; 343 | 344 | if (_effectStep >= maxFlashes * 2) { 345 | _effectStep = 0; 346 | flashPause = random(100, 5001); // between 0.1 and 5s 347 | } 348 | return timeslot * flashPause; 349 | } 350 | 351 | uint16_t EffectEngine::effectBreathe() { 352 | /* 353 | * Subtle "breathing" effect, works best with gamma correction on. 354 | * 355 | * The average resting respiratory rate of an adult is 12–18 breaths/minute. 356 | * We use 12 breaths/minute = 5.0s/breath at the default _effectDelay. 357 | * The tidal volume (~0.5l) is much less than the total lung capacity, 358 | * so we vary only between 75% and 100% of the set brightness. 359 | * 360 | * Per default, this is subtle enough to use with a flood, spot, ceiling or 361 | * even bedside light. If you want more variation, use the values given 362 | * below for a 33%/67% variation. 363 | * 364 | * In the calculation, we use some constants to make it faster: 365 | * 0.367879441 is: 1/e 366 | * 0.106364766 is: 0.25/(e-1/e) [25% brightness variation, use 0.140401491 for 33%] 367 | * 0.75 is the offset [75% min brightness, use 0.67 for 67%] 368 | * 369 | * See also https://sean.voisen.org/blog/2011/10/breathing-led-with-arduino/ 370 | * for a nice explanation of the math. 371 | */ 372 | // sin() is in radians, so 2*PI rad is a full period; compiler should optimize. 373 | float val = (exp(sin(millis()/(_effectDelay*5.0)*2*PI)) - 0.367879441) * 0.106364766 + 0.75; 374 | setAll({_effectColor.r*val, _effectColor.g*val, _effectColor.b*val}); 375 | return _effectDelay / 40; // update every 25ms 376 | } 377 | 378 | void EffectEngine::sendUDPData() { 379 | if ( (config.effect_sendprotocol == 1) && (config.effect_sendIP) ) { 380 | if ( (config.ds == DataSource::WEB) || (config.ds == DataSource::MQTT) ) { 381 | if (config.effect_sendmulticast) 382 | _forwarder.beginPacketMulticast(config.effect_sendIP, config.effect_sendport, WiFi.localIP()); 383 | else 384 | _forwarder.beginPacket(config.effect_sendIP, config.effect_sendport); 385 | _forwarder.write(_ledDriver->getData(), config.channel_count); 386 | _forwarder.endPacket(); 387 | } 388 | } 389 | } 390 | 391 | // dCHSV hue 0->360 sat 0->1.0 val 0->1.0 392 | dCHSV EffectEngine::rgb2hsv(CRGB in_int) 393 | { 394 | dCHSV out; 395 | dCRGB in = {in_int.r/255.0d, in_int.g/255.0d, in_int.b/255.0d}; 396 | double min, max, delta; 397 | 398 | min = in.r < in.g ? in.r : in.g; 399 | min = min < in.b ? min : in.b; 400 | 401 | max = in.r > in.g ? in.r : in.g; 402 | max = max > in.b ? max : in.b; 403 | 404 | out.v = max; // v 405 | delta = max - min; 406 | if (delta < 0.00001) 407 | { 408 | out.s = 0; 409 | out.h = 0; // undefined, maybe nan? 410 | return out; 411 | } 412 | if( max > 0.0 ) { // NOTE: if Max is == 0, this divide would cause a crash 413 | out.s = (delta / max); // s 414 | } else { 415 | // if max is 0, then r = g = b = 0 416 | // s = 0, v is undefined 417 | out.s = 0.0; 418 | out.h = NAN; // its now undefined 419 | return out; 420 | } 421 | if( in.r >= max ) // > is bogus, just keeps compilor happy 422 | out.h = ( in.g - in.b ) / delta; // between yellow & magenta 423 | else 424 | if( in.g >= max ) 425 | out.h = 2.0 + ( in.b - in.r ) / delta; // between cyan & yellow 426 | else 427 | out.h = 4.0 + ( in.r - in.g ) / delta; // between magenta & cyan 428 | 429 | out.h *= 60.0; // degrees 430 | 431 | if( out.h < 0.0 ) 432 | out.h += 360.0; 433 | 434 | return out; 435 | } 436 | 437 | 438 | // dCHSV hue 0->360 sat 0->1.0 val 0->1.0 439 | CRGB EffectEngine::hsv2rgb(dCHSV in) 440 | { 441 | double hh, p, q, t, ff; 442 | long i; 443 | dCRGB out; 444 | CRGB out_int = {0,0,0}; 445 | 446 | if(in.s <= 0.0) { // < is bogus, just shuts up warnings 447 | out.r = in.v; 448 | out.g = in.v; 449 | out.b = in.v; 450 | out_int = {255*out.r, 255*out.g, 255*out.b}; 451 | return out_int; 452 | } 453 | hh = in.h; 454 | if(hh >= 360.0) hh = 0.0; 455 | hh /= 60.0; 456 | i = (long)hh; 457 | ff = hh - i; 458 | p = in.v * (1.0 - in.s); 459 | q = in.v * (1.0 - (in.s * ff)); 460 | t = in.v * (1.0 - (in.s * (1.0 - ff))); 461 | 462 | switch(i) { 463 | case 0: 464 | out.r = in.v; 465 | out.g = t; 466 | out.b = p; 467 | break; 468 | case 1: 469 | out.r = q; 470 | out.g = in.v; 471 | out.b = p; 472 | break; 473 | case 2: 474 | out.r = p; 475 | out.g = in.v; 476 | out.b = t; 477 | break; 478 | 479 | case 3: 480 | out.r = p; 481 | out.g = q; 482 | out.b = in.v; 483 | break; 484 | case 4: 485 | out.r = t; 486 | out.g = p; 487 | out.b = in.v; 488 | break; 489 | case 5: 490 | default: 491 | out.r = in.v; 492 | out.g = p; 493 | out.b = q; 494 | break; 495 | } 496 | out_int = {255*out.r, 255*out.g, 255*out.b}; 497 | return out_int; 498 | } 499 | 500 | // dump the current running effect options to the supplied json 501 | void EffectEngine::runningEffectToJson (JsonObject &json) { 502 | JsonObject &effect = json.createNestedObject("currentEffect"); 503 | if (config.ds == DataSource::E131) { 504 | effect["name"] = "Disabled"; 505 | } else { 506 | effect["name"] = (String)getEffect() ? getEffect() : ""; 507 | } 508 | effect["brightness"] = getBrightness(); 509 | effect["speed"] = getSpeed(); 510 | effect["r"] = getColor().r; 511 | effect["g"] = getColor().g; 512 | effect["b"] = getColor().b; 513 | effect["reverse"] = getReverse(); 514 | effect["mirror"] = getMirror(); 515 | effect["allleds"] = getAllLeds(); 516 | effect["startenabled"] = config.effect_startenabled; 517 | effect["idleenabled"] = config.effect_idleenabled; 518 | effect["idletimeout"] = config.effect_idletimeout; 519 | effect["sendprotocol"] = config.effect_sendprotocol; 520 | effect["sendhost"] = config.effect_sendhost; 521 | effect["sendport"] = config.effect_sendport; 522 | effect["sendspeed"] = config.effect_sendspeed; 523 | } 524 | 525 | 526 | // dump all the known effect and options to the supplied json 527 | void EffectEngine::EffectListToJson (JsonObject &json) { 528 | JsonObject &effectList = json.createNestedObject("effectList"); 529 | for(int i=0; i < getEffectCount(); i++){ 530 | // hide the "view" option from effect list 531 | if ( getEffectInfo(i)->name != "View") { 532 | JsonObject &effect = effectList.createNestedObject( getEffectInfo(i)->htmlid ); 533 | effect["name"] = getEffectInfo(i)->name; 534 | effect["htmlid"] = getEffectInfo(i)->htmlid; 535 | effect["hasColor"] = getEffectInfo(i)->hasColor; 536 | effect["hasMirror"] = getEffectInfo(i)->hasMirror; 537 | effect["hasReverse"] = getEffectInfo(i)->hasReverse; 538 | effect["hasAllLeds"] = getEffectInfo(i)->hasAllLeds; 539 | effect["wsTCode"] = getEffectInfo(i)->wsTCode; 540 | } 541 | } 542 | } 543 | 544 | -------------------------------------------------------------------------------- /EffectEngine.h: -------------------------------------------------------------------------------- 1 | #ifndef EFFECTENGINE_H_ 2 | #define EFFECTENGINE_H_ 3 | 4 | #include 5 | 6 | #define MIN_EFFECT_DELAY 10 7 | #define MAX_EFFECT_DELAY 65535 8 | #define DEFAULT_EFFECT_DELAY 1000 9 | 10 | #if defined(ESPS_MODE_PIXEL) 11 | #define DRIVER PixelDriver 12 | #elif defined(ESPS_MODE_SERIAL) 13 | #define DRIVER SerialDriver 14 | #endif 15 | 16 | class EffectEngine; 17 | // CRGB red, green, blue 0->255 18 | struct CRGB { 19 | uint8_t r; 20 | uint8_t g; 21 | uint8_t b; 22 | }; 23 | 24 | // dCRGB red, green, blue 0->1.0 25 | struct dCRGB { 26 | double r; 27 | double g; 28 | double b; 29 | }; 30 | 31 | // dCHSV hue 0->360 sat 0->1.0 val 0->1.0 32 | struct dCHSV { 33 | double h; 34 | double s; 35 | double v; 36 | }; 37 | 38 | /* 39 | * EffectFunc is the signiture used for all effects. Returns 40 | * the desired delay before the effect should trigger again 41 | */ 42 | typedef uint16_t (EffectEngine::*EffectFunc)(void); 43 | struct EffectDesc { 44 | String name; 45 | EffectFunc func; 46 | const char* htmlid; 47 | bool hasColor; 48 | bool hasMirror; 49 | bool hasReverse; 50 | bool hasAllLeds; 51 | String wsTCode; 52 | }; 53 | 54 | class EffectEngine { 55 | 56 | private: 57 | using timeType = decltype(millis()); 58 | 59 | const EffectDesc* _activeEffect = nullptr; /* Pointer to the active effect descriptor */ 60 | uint32_t _effectWait = 0; /* How long to wait for the effect to run again */ 61 | timeType _effectLastRun = 0; /* When did the effect last run ? in millis() */ 62 | uint32_t _effectCounter = 0; /* Counter for the number of calls to the active effect */ 63 | uint16_t _effectSpeed = 6; /* Externally controlled effect speed 1..10 */ 64 | uint16_t _effectDelay = 1000; /* Internal representation of speed */ 65 | bool _effectReverse = false; /* Externally controlled effect reverse option */ 66 | bool _effectMirror = false; /* Externally controlled effect mirroring (start at center) */ 67 | bool _effectAllLeds = false; /* Externally controlled effect all leds = 1st led */ 68 | float _effectBrightness = 1.0; /* Externally controlled effect brightness [0, 255] */ 69 | CRGB _effectColor = {0,0,0}; /* Externally controlled effect color */ 70 | 71 | uint32_t _effectStep = 0; /* Shared mutable effect step counter */ 72 | 73 | bool _initialized = false; /* Boolean indicating if the engine is initialzied */ 74 | DRIVER* _ledDriver = nullptr; /* Pointer to the active LED driver */ 75 | uint16_t _ledCount = 0; /* Number of RGB leds (not channels) */ 76 | 77 | WiFiUDP _forwarder; 78 | 79 | public: 80 | EffectEngine(); 81 | 82 | void begin(DRIVER* ledDriver, uint16_t ledCount); 83 | void run(); 84 | 85 | String getEffect() { return _activeEffect ? _activeEffect->name : ""; } 86 | bool getReverse() { return _effectReverse; } 87 | bool getMirror() { return _effectMirror; } 88 | bool getAllLeds() { return _effectAllLeds; } 89 | float getBrightness() { return _effectBrightness; } 90 | uint16_t getDelay() { return _effectDelay; } 91 | uint16_t getSpeed() { return _effectSpeed; } 92 | CRGB getColor() { return _effectColor; } 93 | 94 | int getEffectCount(); 95 | const EffectDesc* getEffectInfo(unsigned a); 96 | const EffectDesc* getEffectInfo(String s); 97 | void setFromConfig(); 98 | void setFromDefaults(); 99 | 100 | void runningEffectToJson (JsonObject &json); 101 | void EffectListToJson (JsonObject &json ); 102 | 103 | bool isValidEffect(const String effectName); 104 | void setEffect(const String effectName); 105 | void setReverse(bool reverse) { _effectReverse = reverse; } 106 | void setMirror(bool mirror) { _effectMirror = mirror; } 107 | void setAllLeds(bool allleds) { _effectAllLeds = allleds; } 108 | void setBrightness(float brightness); 109 | void setSpeed(uint16_t speed); 110 | void setDelay(uint16_t delay); 111 | void setColor(CRGB color) { _effectColor = color; } 112 | 113 | // Effect functions 114 | uint16_t effectSolidColor(); 115 | uint16_t effectRainbow(); 116 | uint16_t effectChase(); 117 | uint16_t effectBlink(); 118 | uint16_t effectFlash(); 119 | uint16_t effectFireFlicker(); 120 | uint16_t effectLightning(); 121 | uint16_t effectBreathe(); 122 | uint16_t effectNull(); 123 | void clearAll(); 124 | 125 | void sendUDPData(); 126 | 127 | private: 128 | 129 | void setPixel(uint16_t idx, CRGB color); 130 | void setRange(uint16_t first, uint16_t len, CRGB color); 131 | void clearRange(uint16_t first, uint16_t len); 132 | void setAll(CRGB color); 133 | 134 | CRGB colorWheel(uint8_t pos); 135 | dCHSV rgb2hsv(CRGB in); 136 | CRGB hsv2rgb(dCHSV in); 137 | }; 138 | 139 | #endif 140 | -------------------------------------------------------------------------------- /Mode.h: -------------------------------------------------------------------------------- 1 | /* 2 | * Mode.h 3 | * 4 | * Project: ESPixelStick - An ESP8266 and E1.31 based pixel driver 5 | * Copyright (c) 2018 Shelby Merrick 6 | * http://www.forkineye.com 7 | * 8 | * This program is provided free for you to use in any way that you wish, 9 | * subject to the laws and regulations where you are using it. Due diligence 10 | * is strongly suggested before using this code. Please give credit where due. 11 | * 12 | * The Author makes no warranty of any kind, express or implied, with regard 13 | * to this program or the documentation contained in this document. The 14 | * Author shall not be liable in any event for incidental or consequential 15 | * damages in connection with, or arising out of, the furnishing, performance 16 | * or use of these programs. 17 | * 18 | */ 19 | 20 | #ifndef MODE_H_ 21 | #define MODE_H_ 22 | 23 | /* Output Mode - There can be only one! (-Conor MacLeod) */ 24 | #define ESPS_MODE_PIXEL 25 | //#define ESPS_MODE_SERIAL 26 | 27 | /* Include support for PWM */ 28 | #define ESPS_SUPPORT_PWM 29 | 30 | /* Enable support for rotary encoder */ 31 | //#define ESPS_ENABLE_BUTTONS 32 | 33 | /* Enable support for udpraw packets on port 2801 */ 34 | #define ESPS_ENABLE_UDPRAW 35 | 36 | #endif // MODE_H_ 37 | -------------------------------------------------------------------------------- /PixelDriver.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * PixelDriver.cpp - Pixel driver code for ESPixelStick 3 | * 4 | * Project: ESPixelStick - An ESP8266 and E1.31 based pixel driver 5 | * Copyright (c) 2015 Shelby Merrick 6 | * http://www.forkineye.com 7 | * 8 | * This program is provided free for you to use in any way that you wish, 9 | * subject to the laws and regulations where you are using it. Due diligence 10 | * is strongly suggested before using this code. Please give credit where due. 11 | * 12 | * The Author makes no warranty of any kind, express or implied, with regard 13 | * to this program or the documentation contained in this document. The 14 | * Author shall not be liable in any event for incidental or consequential 15 | * damages in connection with, or arising out of, the furnishing, performance 16 | * or use of these programs. 17 | * 18 | */ 19 | 20 | #include 21 | #include 22 | #include 23 | #include "PixelDriver.h" 24 | 25 | extern "C" { 26 | #include 27 | #include 28 | #include 29 | #include 30 | } 31 | 32 | static const uint8_t *uart_buffer; // Buffer tracker 33 | static const uint8_t *uart_buffer_tail; // Buffer tracker 34 | 35 | uint8_t PixelDriver::rOffset = 0; 36 | uint8_t PixelDriver::gOffset = 1; 37 | uint8_t PixelDriver::bOffset = 2; 38 | 39 | int PixelDriver::begin() { 40 | return begin(PixelType::WS2811, PixelColor::RGB, 170); 41 | } 42 | 43 | int PixelDriver::begin(PixelType type) { 44 | return begin(type, PixelColor::RGB, 170); 45 | } 46 | 47 | int PixelDriver::begin(PixelType type, PixelColor color, uint16_t length) { 48 | int retval = true; 49 | 50 | this->type = type; 51 | this->color = color; 52 | 53 | updateOrder(color); 54 | 55 | if (pixdata) free(pixdata); 56 | szBuffer = length * 3; 57 | if (pixdata = static_cast(malloc(szBuffer))) { 58 | memset(pixdata, 0, szBuffer); 59 | numPixels = length; 60 | } else { 61 | numPixels = 0; 62 | szBuffer = 0; 63 | retval = false; 64 | } 65 | 66 | uint16_t szAsync = szBuffer; 67 | if (type == PixelType::GECE) { 68 | if (pbuff) free(pbuff); 69 | if (pbuff = static_cast(malloc(GECE_PSIZE))) { 70 | memset(pbuff, 0, GECE_PSIZE); 71 | } else { 72 | numPixels = 0; 73 | szBuffer = 0; 74 | retval = false; 75 | } 76 | szAsync = GECE_PSIZE; 77 | } 78 | 79 | if (asyncdata) free(asyncdata); 80 | if (asyncdata = static_cast(malloc(szAsync))) { 81 | memset(asyncdata, 0, szAsync); 82 | } else { 83 | numPixels = 0; 84 | szBuffer = 0; 85 | retval = false; 86 | } 87 | 88 | if (type == PixelType::WS2811) { 89 | refreshTime = WS2811_TFRAME * length + WS2811_TIDLE; 90 | ws2811_init(); 91 | } else if (type == PixelType::GECE) { 92 | refreshTime = (GECE_TFRAME + GECE_TIDLE) * length; 93 | gece_init(); 94 | } else { 95 | retval = false; 96 | } 97 | 98 | return retval; 99 | } 100 | 101 | void PixelDriver::setPin(uint8_t pin) { 102 | if (this->pin >= 0) 103 | this->pin = pin; 104 | } 105 | 106 | void PixelDriver::ws2811_init() { 107 | /* Serial rate is 4x 800KHz for WS2811 */ 108 | Serial1.begin(3200000, SERIAL_6N1, SERIAL_TX_ONLY); 109 | CLEAR_PERI_REG_MASK(UART_CONF0(UART), UART_INV_MASK); 110 | SET_PERI_REG_MASK(UART_CONF0(UART), (BIT(22))); 111 | 112 | /* Clear FIFOs */ 113 | SET_PERI_REG_MASK(UART_CONF0(UART), UART_RXFIFO_RST | UART_TXFIFO_RST); 114 | CLEAR_PERI_REG_MASK(UART_CONF0(UART), UART_RXFIFO_RST | UART_TXFIFO_RST); 115 | 116 | /* Disable all interrupts */ 117 | ETS_UART_INTR_DISABLE(); 118 | 119 | /* Atttach interrupt handler */ 120 | ETS_UART_INTR_ATTACH(handleWS2811, NULL); 121 | 122 | /* Set TX FIFO trigger. 80 bytes gives 200 microsecs to refill the FIFO */ 123 | WRITE_PERI_REG(UART_CONF1(UART), 80 << UART_TXFIFO_EMPTY_THRHD_S); 124 | 125 | /* Disable RX & TX interrupts. It is enabled by uart.c in the SDK */ 126 | CLEAR_PERI_REG_MASK(UART_INT_ENA(UART), UART_RXFIFO_FULL_INT_ENA | UART_TXFIFO_EMPTY_INT_ENA); 127 | 128 | /* Clear all pending interrupts in UART1 */ 129 | WRITE_PERI_REG(UART_INT_CLR(UART), 0xffff); 130 | 131 | /* Reenable interrupts */ 132 | ETS_UART_INTR_ENABLE(); 133 | } 134 | 135 | void PixelDriver::gece_init() { 136 | // Serial rate is 3x 100KHz for GECE 137 | Serial1.begin(300000, SERIAL_7N1, SERIAL_TX_ONLY); 138 | SET_PERI_REG_MASK(UART_CONF0(UART), UART_TXD_BRK); 139 | delayMicroseconds(GECE_TIDLE); 140 | } 141 | 142 | void PixelDriver::updateOrder(PixelColor color) { 143 | this->color = color; 144 | 145 | switch (color) { 146 | case PixelColor::GRB: 147 | rOffset = 1; 148 | gOffset = 0; 149 | bOffset = 2; 150 | break; 151 | case PixelColor::BRG: 152 | rOffset = 1; 153 | gOffset = 2; 154 | bOffset = 0; 155 | break; 156 | case PixelColor::RBG: 157 | rOffset = 0; 158 | gOffset = 2; 159 | bOffset = 1; 160 | break; 161 | case PixelColor::GBR: 162 | rOffset = 2; 163 | gOffset = 0; 164 | bOffset = 1; 165 | break; 166 | case PixelColor::BGR: 167 | rOffset = 2; 168 | gOffset = 1; 169 | bOffset = 0; 170 | break; 171 | default: 172 | rOffset = 0; 173 | gOffset = 1; 174 | bOffset = 2; 175 | } 176 | } 177 | 178 | void ICACHE_RAM_ATTR PixelDriver::handleWS2811(void *param) { 179 | /* Process if UART1 */ 180 | if (READ_PERI_REG(UART_INT_ST(UART1))) { 181 | // Fill the FIFO with new data 182 | uart_buffer = fillWS2811(uart_buffer, uart_buffer_tail); 183 | 184 | // Disable TX interrupt when done 185 | if (uart_buffer == uart_buffer_tail) 186 | CLEAR_PERI_REG_MASK(UART_INT_ENA(UART1), UART_TXFIFO_EMPTY_INT_ENA); 187 | 188 | // Clear all interrupts flags (just in case) 189 | WRITE_PERI_REG(UART_INT_CLR(UART1), 0xffff); 190 | } 191 | 192 | /* Clear if UART0 */ 193 | if (READ_PERI_REG(UART_INT_ST(UART0))) 194 | WRITE_PERI_REG(UART_INT_CLR(UART0), 0xffff); 195 | } 196 | 197 | const uint8_t* ICACHE_RAM_ATTR PixelDriver::fillWS2811(const uint8_t *buff, 198 | const uint8_t *tail) { 199 | uint8_t avail = (UART_TX_FIFO_SIZE - getFifoLength()) / 4; 200 | if (tail - buff > avail) 201 | tail = buff + avail; 202 | 203 | while (buff + 2 < tail) { 204 | uint8_t subpix = buff[rOffset]; 205 | enqueue(LOOKUP_2811[(GAMMA_TABLE[subpix] >> (6+GAMMA_SHIFT)) & 0x3]); 206 | enqueue(LOOKUP_2811[(GAMMA_TABLE[subpix] >> (4+GAMMA_SHIFT)) & 0x3]); 207 | enqueue(LOOKUP_2811[(GAMMA_TABLE[subpix] >> (2+GAMMA_SHIFT)) & 0x3]); 208 | enqueue(LOOKUP_2811[(GAMMA_TABLE[subpix] >> (0+GAMMA_SHIFT)) & 0x3]); 209 | 210 | subpix = buff[gOffset]; 211 | enqueue(LOOKUP_2811[(GAMMA_TABLE[subpix] >> (6+GAMMA_SHIFT)) & 0x3]); 212 | enqueue(LOOKUP_2811[(GAMMA_TABLE[subpix] >> (4+GAMMA_SHIFT)) & 0x3]); 213 | enqueue(LOOKUP_2811[(GAMMA_TABLE[subpix] >> (2+GAMMA_SHIFT)) & 0x3]); 214 | enqueue(LOOKUP_2811[(GAMMA_TABLE[subpix] >> (0+GAMMA_SHIFT)) & 0x3]); 215 | 216 | subpix = buff[bOffset]; 217 | enqueue(LOOKUP_2811[(GAMMA_TABLE[subpix] >> (6+GAMMA_SHIFT)) & 0x3]); 218 | enqueue(LOOKUP_2811[(GAMMA_TABLE[subpix] >> (4+GAMMA_SHIFT)) & 0x3]); 219 | enqueue(LOOKUP_2811[(GAMMA_TABLE[subpix] >> (2+GAMMA_SHIFT)) & 0x3]); 220 | enqueue(LOOKUP_2811[(GAMMA_TABLE[subpix] >> (0+GAMMA_SHIFT)) & 0x3]); 221 | 222 | buff += 3; 223 | } 224 | 225 | return buff; 226 | } 227 | 228 | void ICACHE_RAM_ATTR PixelDriver::show() { 229 | if (!pixdata) return; 230 | 231 | if (type == PixelType::WS2811) { 232 | if (!cntZigzag) { // Normal / group copy 233 | for (size_t led = 0; led < szBuffer / 3; led++) { 234 | uint16 modifier = led / cntGroup; 235 | asyncdata[3 * led + 0] = pixdata[3 * modifier + 0]; 236 | asyncdata[3 * led + 1] = pixdata[3 * modifier + 1]; 237 | asyncdata[3 * led + 2] = pixdata[3 * modifier + 2]; 238 | } 239 | } else { // Zigzag copy 240 | for (size_t led = 0; led < szBuffer / 3; led++) { 241 | uint16 modifier = led / cntGroup; 242 | if (led / cntZigzag % 2) { // Odd "zig" 243 | int group = cntZigzag * (led / cntZigzag); 244 | int this_led = (group + cntZigzag - (led % cntZigzag) - 1) / cntGroup; 245 | asyncdata[3 * led + 0] = pixdata[3 * this_led + 0]; 246 | asyncdata[3 * led + 1] = pixdata[3 * this_led + 1]; 247 | asyncdata[3 * led + 2] = pixdata[3 * this_led + 2]; 248 | } else { // Even "zag" 249 | asyncdata[3 * led + 0] = pixdata[3 * modifier + 0]; 250 | asyncdata[3 * led + 1] = pixdata[3 * modifier + 1]; 251 | asyncdata[3 * led + 2] = pixdata[3 * modifier + 2]; 252 | } 253 | } 254 | 255 | } 256 | 257 | uart_buffer = asyncdata; 258 | uart_buffer_tail = asyncdata + szBuffer; 259 | 260 | SET_PERI_REG_MASK(UART_INT_ENA(1), UART_TXFIFO_EMPTY_INT_ENA); 261 | startTime = micros(); 262 | 263 | } else if (type == PixelType::GECE) { 264 | uint32_t packet = 0; 265 | uint32_t pTime = 0; 266 | 267 | // Build a GECE packet 268 | startTime = micros(); 269 | for (uint8_t i = 0; i < numPixels; i++) { 270 | packet = (packet & ~GECE_ADDRESS_MASK) | (i << 20); 271 | packet = (packet & ~GECE_BRIGHTNESS_MASK) | 272 | (GECE_DEFAULT_BRIGHTNESS << 12); 273 | packet = (packet & ~GECE_BLUE_MASK) | (pixdata[i*3+2] << 4); 274 | packet = (packet & ~GECE_GREEN_MASK) | pixdata[i*3+1]; 275 | packet = (packet & ~GECE_RED_MASK) | (pixdata[i*3] >> 4); 276 | 277 | uint8_t shift = GECE_PSIZE; 278 | for (uint8_t i = 0; i < GECE_PSIZE; i++) 279 | pbuff[i] = LOOKUP_GECE[(packet >> --shift) & 0x1]; 280 | 281 | // Wait until ready 282 | while ((micros() - pTime) < (GECE_TFRAME + GECE_TIDLE)) {} 283 | 284 | // 10us start bit 285 | pTime = micros(); 286 | uint32_t c = _getCycleCount(); 287 | CLEAR_PERI_REG_MASK(UART_CONF0(UART), UART_TXD_BRK); 288 | while ((_getCycleCount() - c) < CYCLES_GECE_START - 100) {} 289 | 290 | // Send packet and idle low (break) 291 | Serial1.write(pbuff, GECE_PSIZE); 292 | SET_PERI_REG_MASK(UART_CONF0(UART), UART_TXD_BRK); 293 | } 294 | } 295 | } 296 | 297 | uint8_t* PixelDriver::getData() { 298 | return asyncdata; // data post grouping or zigzaging 299 | // return pixdata; 300 | } 301 | -------------------------------------------------------------------------------- /PixelDriver.h: -------------------------------------------------------------------------------- 1 | /* 2 | * PixelDriver.h - Pixel driver code for ESPixelStick 3 | * 4 | * Project: ESPixelStick - An ESP8266 and E1.31 based pixel driver 5 | * Copyright (c) 2015 Shelby Merrick 6 | * http://www.forkineye.com 7 | * 8 | * This program is provided free for you to use in any way that you wish, 9 | * subject to the laws and regulations where you are using it. Due diligence 10 | * is strongly suggested before using this code. Please give credit where due. 11 | * 12 | * The Author makes no warranty of any kind, express or implied, with regard 13 | * to this program or the documentation contained in this document. The 14 | * Author shall not be liable in any event for incidental or consequential 15 | * damages in connection with, or arising out of, the furnishing, performance 16 | * or use of these programs. 17 | * 18 | */ 19 | 20 | #ifndef PIXELDRIVER_H_ 21 | #define PIXELDRIVER_H_ 22 | 23 | #define UART_INV_MASK (0x3f << 19) 24 | #define UART 1 25 | 26 | /* Gamma correction table */ 27 | #include "gamma.h" 28 | 29 | /* 30 | * Inverted 6N1 UART lookup table for ws2811, first 2 bits ignored. 31 | * Start and stop bits are part of the pixel stream. 32 | */ 33 | const char LOOKUP_2811[4] = { 34 | 0b00110111, // 00 - (1)000 100(0) 35 | 0b00000111, // 01 - (1)000 111(0) 36 | 0b00110100, // 10 - (1)110 100(0) 37 | 0b00000100 // 11 - (1)110 111(0) 38 | }; 39 | 40 | /* 41 | * 7N1 UART lookup table for GECE, first bit is ignored. 42 | * Start bit and stop bits are part of the packet. 43 | * Bits are backwards since we need MSB out. 44 | */ 45 | 46 | const char LOOKUP_GECE[2] = { 47 | 0b01111100, // 0 - (0)00 111 11(1) 48 | 0b01100000 // 1 - (0)00 000 11(1) 49 | }; 50 | 51 | #define GECE_DEFAULT_BRIGHTNESS 0xCC 52 | 53 | #define GECE_ADDRESS_MASK 0x03F00000 54 | #define GECE_BRIGHTNESS_MASK 0x000FF000 55 | #define GECE_BLUE_MASK 0x00000F00 56 | #define GECE_GREEN_MASK 0x000000F0 57 | #define GECE_RED_MASK 0x0000000F 58 | 59 | #define GECE_GET_ADDRESS(packet) (packet >> 20) & 0x3F 60 | #define GECE_GET_BRIGHTNESS(packet) (packet >> 12) & 0xFF 61 | #define GECE_GET_BLUE(packet) (packet >> 8) & 0x0F 62 | #define GECE_GET_GREEN(packet) (packet >> 4) & 0x0F 63 | #define GECE_GET_RED(packet) packet & 0x0F 64 | #define GECE_PSIZE 26 65 | 66 | #define WS2811_TFRAME 30L /* 30us frame time */ 67 | #define WS2811_TIDLE 300L /* 300us idle time */ 68 | #define GECE_TFRAME 790L /* 790us frame time */ 69 | #define GECE_TIDLE 45L /* 45us idle time - should be 30us */ 70 | 71 | #define CYCLES_GECE_START (F_CPU / 100000) // 10us 72 | 73 | /* Pixel Types */ 74 | enum class PixelType : uint8_t { 75 | WS2811, 76 | GECE 77 | }; 78 | 79 | /* Color Order */ 80 | enum class PixelColor : uint8_t { 81 | RGB, 82 | GRB, 83 | BRG, 84 | RBG, 85 | GBR, 86 | BGR 87 | }; 88 | 89 | class PixelDriver { 90 | public: 91 | int begin(); 92 | int begin(PixelType type); 93 | int begin(PixelType type, PixelColor color, uint16_t length); 94 | void setPin(uint8_t pin); 95 | void updateOrder(PixelColor color); 96 | void ICACHE_RAM_ATTR show(); 97 | uint8_t* getData(); 98 | 99 | /* Set channel value at address */ 100 | inline void setValue(uint16_t address, uint8_t value) { 101 | pixdata[address] = value; 102 | } 103 | 104 | /* Get channel value at address */ 105 | inline uint8_t getValue(uint16_t address) { 106 | return pixdata[address]; 107 | } 108 | 109 | /* Set group / zigzag counts */ 110 | inline void setGroup(uint16_t _group, uint16_t _zigzag) { 111 | this->cntGroup = _group; 112 | this->cntZigzag = _zigzag; 113 | } 114 | 115 | /* Drop the update if our refresh rate is too high */ 116 | inline bool canRefresh() { 117 | return (micros() - startTime) >= refreshTime; 118 | } 119 | 120 | private: 121 | PixelType type; // Pixel type 122 | PixelColor color; // Color Order 123 | uint16_t cntGroup; // Output modifying interval (in LEDs, not channels) 124 | uint16_t cntZigzag; // Zigzag every cntZigzag physical pixels 125 | uint8_t pin; // Pin for bit-banging 126 | uint8_t *pixdata; // Pixel buffer 127 | uint8_t *asyncdata; // Async buffer 128 | uint8_t *pbuff; // GECE Packet Buffer 129 | uint16_t numPixels; // Number of pixels 130 | uint16_t szBuffer; // Size of Pixel buffer 131 | uint32_t startTime; // When the last frame TX started 132 | uint32_t refreshTime; // Time until we can refresh after starting a TX 133 | static uint8_t rOffset; // Index of red byte 134 | static uint8_t gOffset; // Index of green byte 135 | static uint8_t bOffset; // Index of blue byte 136 | 137 | void ws2811_init(); 138 | void gece_init(); 139 | 140 | /* FIFO Handlers */ 141 | static const uint8_t* ICACHE_RAM_ATTR fillWS2811(const uint8_t *buff, 142 | const uint8_t *tail); 143 | 144 | /* Interrupt Handlers */ 145 | static void ICACHE_RAM_ATTR handleWS2811(void *param); 146 | 147 | /* Returns number of bytes waiting in the TX FIFO of UART1 */ 148 | static inline uint8_t getFifoLength() { 149 | return (U1S >> USTXC) & 0xff; 150 | } 151 | 152 | /* Append a byte to the TX FIFO of UART1 */ 153 | static inline void enqueue(uint8_t byte) { 154 | U1F = byte; 155 | } 156 | }; 157 | 158 | // Cycle counter 159 | static uint32_t _getCycleCount(void) __attribute__((always_inline)); 160 | static inline uint32_t _getCycleCount(void) { 161 | uint32_t ccount; 162 | __asm__ __volatile__("rsr %0,ccount":"=a" (ccount)); 163 | return ccount; 164 | } 165 | 166 | #endif /* PIXELDRIVER_H_ */ 167 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESPixelStick Firmware 2 | 3 | [![Join the chat at https://gitter.im/forkineye/ESPixelStick](https://badges.gitter.im/forkineye/ESPixelStick.svg)](https://gitter.im/forkineye/ESPixelStick) 4 | [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://paypal.me/ShelbyMerrick) 5 | [![Build Status](https://travis-ci.org/forkineye/ESPixelStick.svg?branch=master)](https://travis-ci.org/forkineye/ESPixelStick) 6 | 7 | This is the Arduino firmware for the ESP8266 based ESPixelStick. The ESPixelStick is a small wireless E1.31 sACN pixel controller designed to control a single strand of pixels. Pixel limitations are mostly based upon your desired refresh rate, around 680 pixels (4 universes) for a 25ms E1.31 source rate. MQTT support is provided as well for integration into home automation systems where an E1.31 source may not be present. 8 | 9 | Since this project began, the firmware has moved beyond just pixel support for those with other ESP8266 based devices. The ESPixelStick firmware now supports outputting E1.31 streams to serial links as well. Note this is not supported on the ESPixelStick hardware, but intended for other ESP8266 devices such as Bill's RenardESP. 10 | 11 | ## Hardware 12 | 13 | Being open source, you are free to use the ESPixelStick firmware on the device of your choice. The code however is written specifically for the [ESPixelStick](http://forkineye.com/espixelstick). The ESPixelStick V2 utilizes an ESP-01 module and provides high current connectors, fusing, power filtering, a programming interface and proper logic level buffering. If you're in the US and would like to purchase an ESPixelStick, they are available via [Amazon](http://amzn.to/2uqBFuX). The proceeds go towards things like keeping my wife happy so I can work on this project :) 14 | 15 | ## Requirements 16 | 17 | Along with the Arduino IDE, you'll need the following software to build this project: 18 | 19 | - [Adruino for ESP8266](https://github.com/esp8266/Arduino) - Arduino core for ESP8266 20 | - [Arduino ESP8266 Filesystem Uploader](https://github.com/esp8266/arduino-esp8266fs-plugin) - Arduino plugin for uploading files to SPIFFS 21 | - [gulp](http://gulpjs.com/) - Build system required to process web sources. Refer to the html [README](html/README.md) for more information. 22 | 23 | The following libraries are required: 24 | 25 | - [ArduinoJson](https://github.com/bblanchon/ArduinoJson) - Arduino JSON Library 26 | - [ESPAsyncE131](https://github.com/forkineye/ESPAsyncE131) - Asynchronous E1.31 (sACN) library 27 | - [RotaryEnoder](https://github.com/mathertel/RotaryEncoder) - Rotary Encoder Library 28 | - [ESPAsyncTCP](https://github.com/me-no-dev/ESPAsyncTCP) - Asynchronous TCP Library 29 | - [ESPAsyncUDP](https://github.com/me-no-dev/ESPAsyncUDP) - Asynchronous UDP Library 30 | - [ESPAsyncWebServer](https://github.com/me-no-dev/ESPAsyncWebServer) - Asynchronous Web Server Library 31 | - [async-mqtt-client](https://github.com/marvinroger/async-mqtt-client) - Asynchronous MQTT Client 32 | 33 | ## Important Notes on Compiling and Flashing 34 | 35 | - Device mode is now a compile time option to set your device type and is configured in the top of the main sketch file. Current options are ```ESPS_MODE_PIXEL``` and ```ESPS_MODE_SERIAL```. The default is ```ESPS_MODE_PIXEL``` for the ESPixelStick hardware. 36 | - Web pages **must** be processed, placed into ```data/www```, and uploaded with the upload plugin. Gulp will process the pages and put them in ```data/www``` for you. Refer to the html [README](html/README.md) for more information. 37 | - In order to use the upload plugin, the ESP8266 **must** be placed into programming mode and the Arduino serial monitor **must** be closed. 38 | - ESP-01 modules **must** be configured for 1M flash and 128k SPIFFS within the Arduino IDE for OTA updates to work. 39 | - For best performance, set the CPU frequency to 160MHz (Tools->CPU Frequency). You may experience lag and other issues if running at 80MHz. 40 | 41 | ## Supported Outputs 42 | 43 | The ESPixelStick firmware can generate the following outputs from incoming E1.31 streams, however your hardware must support the physical interface. 44 | 45 | ### Pixel Protocols 46 | 47 | - WS2811 / WS2812 / WS2812b 48 | - GE Color Effects 49 | 50 | ### Serial Protocols 51 | 52 | - DMX512 53 | - Renard 54 | 55 | ## MQTT Support 56 | 57 | MQTT can be configured via the web interface. When enabled, a payload of "ON" will tell the ESPixelStick to override any incoming E1.31 data with MQTT data. When a payload of "OFF" is received, E1.31 processing will resume. The configured topic is used for state, and the command topic will be the state topic appended with ```/set```. 58 | 59 | For example, if you enter ```porch/esps``` as the topic, the state can be queried from ```porch/esps``` and commands can be sent to ```porch/esps/set``` 60 | 61 | If using [Home Assistant](https://home-assistant.io/), it is recommended to enable Home Assistant Discovery in the MQTT configuration. Your ESPixelStick along with all effects will be automatically imported as an entity within Home Assistant utilzing "Device ID" as the friendly name. For manual configuration, you can use the following as an example. When disabling Home Assistant Discovery, ESPixelStick will attempt to remove its configuration entry from your MQTTT broker. 62 | 63 | ```yaml 64 | light: 65 | - platform: mqtt 66 | schema: json 67 | name: "Front Porch ESPixelStick" 68 | state_topic: "porch/esps" 69 | command_topic: "porch/esps/set" 70 | brightness: true 71 | rgb: true 72 | effect: true 73 | effect_list: 74 | - Solid 75 | - Blink 76 | - Flash 77 | - Rainbow 78 | - Chase 79 | - Fire flicker 80 | - Lightning 81 | - Breathe 82 | ``` 83 | 84 | Here's an example using the mosquitto_pub command line tool: 85 | 86 | ```bash 87 | mosquitto_pub -t porch/esps/set -m '{"state":"ON","color":{"r":255,"g":128,"b":64},"brightness":255,"effect":"solid","reverse":false,"mirror":false}' 88 | ``` 89 | 90 | ## Resources 91 | 92 | - Firmware: [http://github.com/forkineye/ESPixelStick](http://github.com/forkineye/ESPixelStick) 93 | - Hardware: [http://forkineye.com/ESPixelStick](http://forkineye.com/ESPixelStick) 94 | 95 | ## Credits 96 | 97 | - The great people at [diychristmas.org](http://diychristmas.org) and [doityourselfchristmas.com](http://doityourselfchristmas.com) for inspiration and support. 98 | - [Bill Porter](https://github.com/madsci1016) for initial Renard and SoftAP support. 99 | - Bill Porter and [Grayson Lough](https://github.com/GraysonLough) for initial DMX support. 100 | - [Rich Danby](https://github.com/cinoan) for fixes and helping polish the front-end. 101 | - [penfold42](https://github.com/penfold42) for fixes, brightness, gamma support, and zig-zag / grouping. 102 | - penfold42 also maintains PWM support in their fork located [here](https://github.com/penfold42/ESPixelBoard). 103 | - [Austin Hodges](https://github.com/ahodges9) for effects support and MQTT cleanup. 104 | - [Matthias C. Hormann](https://github.com/Moonbase59) — some MQTT & effects cleanup. 105 | -------------------------------------------------------------------------------- /SerialDriver.cpp: -------------------------------------------------------------------------------- 1 | /****************************************************************** 2 | * 3 | * Project: ESPixelStick - An ESP8266 and E1.31 based pixel (And Serial!) driver 4 | * Orginal ESPixelStickproject by 2015 Shelby Merrick 5 | * 6 | * Brought to you by: 7 | * Bill Porter 8 | * www.billporter.info 9 | * 10 | * See Readme for other info and version history 11 | * 12 | * 13 | *This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or(at your option) any later version. 14 | This program is distributed in the hope that it will be useful, 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | GNU General Public License for more details. 18 | 19 | * 20 | *This work is licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported License. 21 | *To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/3.0/ or 22 | *send a letter to Creative Commons, 444 Castro Street, Suite 900, Mountain View, California, 94041, USA. 23 | ******************************************************************/ 24 | 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include "SerialDriver.h" 30 | 31 | extern "C" { 32 | #include 33 | #include 34 | #include 35 | #include 36 | } 37 | 38 | /* Uart Buffer tracker */ 39 | static const uint8_t *uart_buffer; 40 | static const uint8_t *uart_buffer_tail; 41 | 42 | int SerialDriver::begin(HardwareSerial *theSerial, SerialType type, 43 | uint16_t length) { 44 | return begin(theSerial, type, length, BaudRate::BR_57600); 45 | } 46 | 47 | int SerialDriver::begin(HardwareSerial *theSerial, SerialType type, 48 | uint16_t length, BaudRate baud) { 49 | int retval = true; 50 | 51 | _type = type; 52 | _serial = theSerial; 53 | _size = length; 54 | 55 | /* Initialize uart */ 56 | /* frameTime = szSymbol * 1000000 / baud * szBuffer */ 57 | if (type == SerialType::RENARD) { 58 | _size = length + 2; 59 | /* 10 bit symbols, no idle */ 60 | frameTime = ceil(10.0 * 1000000.0 61 | / static_cast(baud) 62 | * static_cast(_size)); 63 | _serial->begin(static_cast(baud)); 64 | } else if (type == SerialType::DMX512) { 65 | _size = length + 1; 66 | /* 11 bit symbols, add BREAK and MAB */ 67 | frameTime = ceil(11.0 * 1000000.0 68 | / static_cast(BaudRate::BR_250000) 69 | * static_cast(_size) 70 | + static_cast(DMX_BREAK) 71 | + static_cast(DMX_MAB)); 72 | _serial->begin(static_cast(BaudRate::BR_250000), SERIAL_8N2); 73 | } else { 74 | retval = false; 75 | } 76 | 77 | /* Setup buffers */ 78 | if (_serialdata) free(_serialdata); 79 | if (_serialdata = static_cast(malloc(_size))) { 80 | memset(_serialdata, 0, _size); 81 | } else { 82 | _size = 0; 83 | retval = false; 84 | } 85 | 86 | if (_asyncdata) free(_asyncdata); 87 | if (_asyncdata = static_cast(malloc(_size))) 88 | memset(_asyncdata, 0, _size); 89 | else 90 | retval = false; 91 | 92 | if (_serialdata && type == SerialType::RENARD) { 93 | _serialdata[0] = 0x7E; 94 | _serialdata[1] = 0x80; 95 | } 96 | 97 | /* Clear FIFOs */ 98 | SET_PERI_REG_MASK(UART_CONF0(SEROUT_UART), UART_RXFIFO_RST | UART_TXFIFO_RST); 99 | CLEAR_PERI_REG_MASK(UART_CONF0(SEROUT_UART), UART_RXFIFO_RST | UART_TXFIFO_RST); 100 | 101 | /* Disable all interrupts */ 102 | ETS_UART_INTR_DISABLE(); 103 | 104 | /* Atttach interrupt handler */ 105 | ETS_UART_INTR_ATTACH(serial_handle, NULL); 106 | 107 | /* Set TX FIFO trigger. 80 bytes gives 200 microsecs to refill the FIFO */ 108 | WRITE_PERI_REG(UART_CONF1(SEROUT_UART), 80 << UART_TXFIFO_EMPTY_THRHD_S); 109 | 110 | /* Disable RX & TX interrupts. It is enabled by uart.c in the SDK */ 111 | CLEAR_PERI_REG_MASK(UART_INT_ENA(SEROUT_UART), UART_RXFIFO_FULL_INT_ENA | UART_TXFIFO_EMPTY_INT_ENA); 112 | 113 | /* Clear all pending interrupts in SEROUT_UART */ 114 | WRITE_PERI_REG(UART_INT_CLR(SEROUT_UART), 0xffff); 115 | 116 | /* Reenable interrupts */ 117 | ETS_UART_INTR_ENABLE(); 118 | 119 | return retval; 120 | } 121 | 122 | /* move buffer creation to being, added header bytes on eneqeue / fill fifo */ 123 | void SerialDriver::startPacket() { 124 | // Create a buffer and fill in header 125 | if (_type == SerialType::RENARD) { 126 | _serialdata = static_cast(malloc(_size + 2)); 127 | _serialdata[0] = 0x7E; 128 | _serialdata[1] = 0x80; 129 | // Create buffer 130 | } else if (_type == SerialType::DMX512) { 131 | _serialdata = static_cast(malloc(_size)); 132 | } 133 | } 134 | 135 | const uint8_t* ICACHE_RAM_ATTR SerialDriver::fillFifo(const uint8_t *buff, const uint8_t *tail) { 136 | uint8_t avail = (UART_TX_FIFO_SIZE - getFifoLength()); 137 | if (tail - buff > avail) tail = buff + avail; 138 | while (buff < tail) enqueue(*buff++); 139 | return buff; 140 | } 141 | 142 | void ICACHE_RAM_ATTR SerialDriver::serial_handle(void *param) { 143 | /* Process and clear SEROUT_UART */ 144 | if (READ_PERI_REG(UART_INT_ST(SEROUT_UART))) { 145 | // Fill the FIFO with new data 146 | uart_buffer = fillFifo(uart_buffer, uart_buffer_tail); 147 | 148 | // Clear TX interrupt when done 149 | if (uart_buffer == uart_buffer_tail) 150 | CLEAR_PERI_REG_MASK(UART_INT_ENA(SEROUT_UART), UART_TXFIFO_EMPTY_INT_ENA); 151 | 152 | // Clear all interrupts flags (just in case) 153 | WRITE_PERI_REG(UART_INT_CLR(SEROUT_UART), 0xffff); 154 | } 155 | 156 | #if SEROUT_UART == 0 157 | /* Clear UART1 if needed */ 158 | if (READ_PERI_REG(UART_INT_ST(UART1))) 159 | WRITE_PERI_REG(UART_INT_CLR(UART1), 0xffff); 160 | #elif SEROUT_UART == 1 161 | /* Clear if UART0 if needed */ 162 | if (READ_PERI_REG(UART_INT_ST(UART0))) 163 | WRITE_PERI_REG(UART_INT_CLR(UART0), 0xffff); 164 | #endif 165 | } 166 | 167 | 168 | void SerialDriver::show() { 169 | if (!_serialdata) return; 170 | 171 | uart_buffer = _serialdata; 172 | uart_buffer_tail = _serialdata + _size; 173 | 174 | if (_type == SerialType::DMX512) { 175 | SET_PERI_REG_MASK(UART_CONF0(SEROUT_UART), UART_TXD_BRK); 176 | delayMicroseconds(DMX_BREAK); 177 | CLEAR_PERI_REG_MASK(UART_CONF0(SEROUT_UART), UART_TXD_BRK); 178 | delayMicroseconds(DMX_MAB); 179 | } 180 | 181 | SET_PERI_REG_MASK(UART_INT_ENA(SEROUT_UART), UART_TXFIFO_EMPTY_INT_ENA); 182 | 183 | startTime = micros(); 184 | 185 | /* Copy data to the idle buffer and swap it */ 186 | memcpy(_asyncdata, _serialdata, _size); 187 | std::swap(_asyncdata, _serialdata); 188 | } 189 | 190 | 191 | uint8_t* SerialDriver::getData() { 192 | return _serialdata; 193 | } 194 | -------------------------------------------------------------------------------- /SerialDriver.h: -------------------------------------------------------------------------------- 1 | /****************************************************************** 2 | * 3 | * Project: ESPixelStick - An ESP8266 and E1.31 based pixel (And Serial!) driver 4 | * Orginal ESPixelStickproject by 2015 Shelby Merrick 5 | * 6 | * Brought to you by: 7 | * Bill Porter 8 | * www.billporter.info 9 | * 10 | * See Readme for other info and version history 11 | * 12 | * 13 | *This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or(at your option) any later version. 14 | This program is distributed in the hope that it will be useful, 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | GNU General Public License for more details. 18 | 19 | * 20 | *This work is licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported License. 21 | *To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/3.0/ or 22 | *send a letter to Creative Commons, 444 Castro Street, Suite 900, Mountain View, California, 94041, USA. 23 | ******************************************************************/ 24 | 25 | 26 | #ifndef SERIALDRIVER_H_ 27 | #define SERIALDRIVER_H_ 28 | 29 | #include "HardwareSerial.h" 30 | 31 | /* UART for Renard / DMX output */ 32 | #define SEROUT_UART 1 33 | 34 | #if SEROUT_UART == 0 35 | #define SEROUT_PORT Serial 36 | #elif SEROUT_UART == 1 37 | #define SEROUT_PORT Serial1 38 | #else 39 | #error "Invalid SEROUT_UART specified" 40 | #endif 41 | 42 | /* DMX minimum timings per E1.11 */ 43 | #define DMX_BREAK 92 44 | #define DMX_MAB 12 45 | 46 | /* Serial Types */ 47 | enum class SerialType : uint8_t { 48 | DMX512, 49 | RENARD 50 | }; 51 | 52 | enum class BaudRate : uint32_t { 53 | BR_38400 = 38400, 54 | BR_57600 = 57600, 55 | BR_115200 = 115200, 56 | BR_230400 = 230400, 57 | BR_250000 = 250000, 58 | BR_460800 = 460800 59 | }; 60 | 61 | class SerialDriver { 62 | public: 63 | int begin(HardwareSerial *theSerial, SerialType type, uint16_t length); 64 | int begin(HardwareSerial *theSerial, SerialType type, uint16_t length, 65 | BaudRate baud); 66 | void startPacket(); 67 | void show(); 68 | uint8_t* getData(); 69 | 70 | /* Set the value */ 71 | inline void setValue(uint16_t address, uint8_t value) { 72 | // Avoid the special characters by rounding 73 | if (_type == SerialType::RENARD) { 74 | switch (value) { 75 | case 0x7d: 76 | _serialdata[address + 2] = 0x7c; 77 | break; 78 | case 0x7e: 79 | case 0x7f: 80 | _serialdata[address + 2] = 0x80; 81 | break; 82 | default: 83 | _serialdata[address + 2] = value; 84 | break; 85 | } 86 | } else if (_type == SerialType::DMX512) { 87 | _serialdata[address + 1] = value; 88 | } 89 | } 90 | 91 | /* Drop the update if our refresh rate is too high */ 92 | inline bool canRefresh() { 93 | return (micros() - startTime) >= frameTime; 94 | } 95 | 96 | private: 97 | SerialType _type; // Output Serial type 98 | HardwareSerial *_serial; // The Serial Port 99 | uint16_t _size; // Size of buffer 100 | uint8_t *_serialdata; // Serial data buffer 101 | uint8_t *_asyncdata; // Async buffer 102 | uint32_t frameTime; // Time it takes for a frame TX to complete 103 | uint32_t startTime; // When the last frame TX started 104 | 105 | 106 | /* Fill the FIFO */ 107 | static const uint8_t* ICACHE_RAM_ATTR fillFifo(const uint8_t *buff, const uint8_t *tail); 108 | 109 | /* Serial interrupt handler */ 110 | static void ICACHE_RAM_ATTR serial_handle(void *param); 111 | 112 | /* Returns number of bytes waiting in the TX FIFO of SEROUT_UART */ 113 | static inline uint8_t getFifoLength() { 114 | return (ESP8266_REG(U0F+(0xF00*SEROUT_UART)) >> USTXC) & 0xff; 115 | } 116 | 117 | /* Append a byte to the TX FIFO of SEROUT_UART */ 118 | static inline void enqueue(uint8_t byte) { 119 | ESP8266_REG(U0F+(0xF00*SEROUT_UART)) = byte; 120 | } 121 | }; 122 | 123 | #endif /* SERIALDRIVER_H_ */ 124 | -------------------------------------------------------------------------------- /buttons.cpp: -------------------------------------------------------------------------------- 1 | #include "buttons.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include "ESPixelStick.h" 7 | #include "rgbhsv.h" 8 | 9 | // GPIO for rotary encoder 10 | #define BUTTON 13 11 | #define ROTARY_A 12 12 | #define ROTARY_B 14 13 | 14 | #define ROT_MAX 40 // 20 is one rotation 15 | 16 | RotaryEncoder encoder(ROTARY_A, ROTARY_B); 17 | 18 | // ESPixelStick globals 19 | extern config_t config; 20 | //extern testing_t testing; 21 | extern EffectEngine effects; // Effects Engine 22 | extern uint32_t pwm_valid_gpio_mask; 23 | 24 | int done_setup = 0; 25 | 26 | // 0 = normal mode, 1 = RGB selector, 2 = HSV selector 27 | int button_mode = 0; 28 | 29 | // (0,1,2) = (r,g,b) or (h,s,v) 30 | int selected_option = 0; 31 | 32 | // button counter thresholds in 0.010 s 33 | #define BUTTON_DEBOUNCE 10 34 | #define BUTTON_LONG_THRESHOLD 40 35 | #define BUTTON_MAX 50 36 | 37 | // globals to hold current colour 38 | rgb global_rgb; 39 | hsv global_hsv; 40 | 41 | unsigned long last_millis; 42 | 43 | int rotary_pos; // current position of the rotary encoder 44 | 45 | int button_counter; // debounce and duration counter 46 | int button_pushed; // 1 = pushed, 0 = not pushed 47 | int button_duration; // how long was it held? 48 | 49 | int anim_step; // which step of the animation to display 50 | int anim_mode; // which animation sequence 51 | 52 | void setupButtons() { 53 | selected_option = 0; 54 | pinMode(BUTTON, INPUT_PULLUP); 55 | pinMode(ROTARY_A, INPUT_PULLUP); 56 | pinMode(ROTARY_B, INPUT_PULLUP); 57 | pwm_valid_gpio_mask &= ~( 1< 16 | 17 | 18 | 74 | 75 | 81 | 82 | 83 | 183 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 42 | 43 |
44 | 45 |
46 |
47 |
48 |
49 | Network Status 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 |
SSID
Hostname
IP
MAC
RSSIdBm / %
Free Heap
Up Time
Data Source
Effect Name
61 |
62 |
63 |
64 |
65 | E1.31 Statistics 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
Universe Range to
Total Packets
Sequence Errors
Packet Errors
Source IP
Last Seen
74 |
75 |
76 |
77 |
78 | MQTT Statistics 79 | 80 | 81 | 82 |
Total Packets
Last Seen
83 |
84 |
85 |
86 |
87 | UDP Statistics 88 | 89 | 90 | 91 | 92 | 93 | 94 |
Total Packets
Short Packets
Long Packets
Source IP
Last Seen
95 |
96 |
97 |
98 |
99 | 100 | 164 | 165 | 166 | 349 | 350 | 351 | 450 | 451 | 452 | 478 | 479 | 480 | 528 |
529 | 530 |
Configuration Saved
531 | 532 | 533 | 543 | 544 | 545 | 558 | 559 | 560 | 570 | 571 | 572 |
573 |
574 | 575 |
576 |
577 | 578 | 584 | 585 | 586 | 589 | 590 | 591 | -------------------------------------------------------------------------------- /html/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.7 (http://getbootstrap.com) 3 | * Copyright 2011-2017 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | 7 | /*! 8 | * Generated using the Bootstrap Customizer (http://getbootstrap.com/customize/?id=7dcc90e00a492a336841e2de308a63e0) 9 | * Config saved to config.json and https://gist.github.com/7dcc90e00a492a336841e2de308a63e0 10 | */ 11 | if (typeof jQuery === 'undefined') { 12 | throw new Error('Bootstrap\'s JavaScript requires jQuery') 13 | } 14 | +function ($) { 15 | 'use strict'; 16 | var version = $.fn.jquery.split(' ')[0].split('.') 17 | if ((version[0] < 2 && version[1] < 9) || (version[0] == 1 && version[1] == 9 && version[2] < 1) || (version[0] > 3)) { 18 | throw new Error('Bootstrap\'s JavaScript requires jQuery version 1.9.1 or higher, but lower than version 4') 19 | } 20 | }(jQuery); 21 | 22 | /* ======================================================================== 23 | * Bootstrap: button.js v3.3.7 24 | * http://getbootstrap.com/javascript/#buttons 25 | * ======================================================================== 26 | * Copyright 2011-2016 Twitter, Inc. 27 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 28 | * ======================================================================== */ 29 | 30 | 31 | +function ($) { 32 | 'use strict'; 33 | 34 | // BUTTON PUBLIC CLASS DEFINITION 35 | // ============================== 36 | 37 | var Button = function (element, options) { 38 | this.$element = $(element) 39 | this.options = $.extend({}, Button.DEFAULTS, options) 40 | this.isLoading = false 41 | } 42 | 43 | Button.VERSION = '3.3.7' 44 | 45 | Button.DEFAULTS = { 46 | loadingText: 'loading...' 47 | } 48 | 49 | Button.prototype.setState = function (state) { 50 | var d = 'disabled' 51 | var $el = this.$element 52 | var val = $el.is('input') ? 'val' : 'html' 53 | var data = $el.data() 54 | 55 | state += 'Text' 56 | 57 | if (data.resetText == null) $el.data('resetText', $el[val]()) 58 | 59 | // push to event loop to allow forms to submit 60 | setTimeout($.proxy(function () { 61 | $el[val](data[state] == null ? this.options[state] : data[state]) 62 | 63 | if (state == 'loadingText') { 64 | this.isLoading = true 65 | $el.addClass(d).attr(d, d).prop(d, true) 66 | } else if (this.isLoading) { 67 | this.isLoading = false 68 | $el.removeClass(d).removeAttr(d).prop(d, false) 69 | } 70 | }, this), 0) 71 | } 72 | 73 | Button.prototype.toggle = function () { 74 | var changed = true 75 | var $parent = this.$element.closest('[data-toggle="buttons"]') 76 | 77 | if ($parent.length) { 78 | var $input = this.$element.find('input') 79 | if ($input.prop('type') == 'radio') { 80 | if ($input.prop('checked')) changed = false 81 | $parent.find('.active').removeClass('active') 82 | this.$element.addClass('active') 83 | } else if ($input.prop('type') == 'checkbox') { 84 | if (($input.prop('checked')) !== this.$element.hasClass('active')) changed = false 85 | this.$element.toggleClass('active') 86 | } 87 | $input.prop('checked', this.$element.hasClass('active')) 88 | if (changed) $input.trigger('change') 89 | } else { 90 | this.$element.attr('aria-pressed', !this.$element.hasClass('active')) 91 | this.$element.toggleClass('active') 92 | } 93 | } 94 | 95 | 96 | // BUTTON PLUGIN DEFINITION 97 | // ======================== 98 | 99 | function Plugin(option) { 100 | return this.each(function () { 101 | var $this = $(this) 102 | var data = $this.data('bs.button') 103 | var options = typeof option == 'object' && option 104 | 105 | if (!data) $this.data('bs.button', (data = new Button(this, options))) 106 | 107 | if (option == 'toggle') data.toggle() 108 | else if (option) data.setState(option) 109 | }) 110 | } 111 | 112 | var old = $.fn.button 113 | 114 | $.fn.button = Plugin 115 | $.fn.button.Constructor = Button 116 | 117 | 118 | // BUTTON NO CONFLICT 119 | // ================== 120 | 121 | $.fn.button.noConflict = function () { 122 | $.fn.button = old 123 | return this 124 | } 125 | 126 | 127 | // BUTTON DATA-API 128 | // =============== 129 | 130 | $(document) 131 | .on('click.bs.button.data-api', '[data-toggle^="button"]', function (e) { 132 | var $btn = $(e.target).closest('.btn') 133 | Plugin.call($btn, 'toggle') 134 | if (!($(e.target).is('input[type="radio"], input[type="checkbox"]'))) { 135 | // Prevent double click on radios, and the double selections (so cancellation) on checkboxes 136 | e.preventDefault() 137 | // The target component still receive the focus 138 | if ($btn.is('input,button')) $btn.trigger('focus') 139 | else $btn.find('input:visible,button:visible').first().trigger('focus') 140 | } 141 | }) 142 | .on('focus.bs.button.data-api blur.bs.button.data-api', '[data-toggle^="button"]', function (e) { 143 | $(e.target).closest('.btn').toggleClass('focus', /^focus(in)?$/.test(e.type)) 144 | }) 145 | 146 | }(jQuery); 147 | 148 | /* ======================================================================== 149 | * Bootstrap: modal.js v3.3.7 150 | * http://getbootstrap.com/javascript/#modals 151 | * ======================================================================== 152 | * Copyright 2011-2016 Twitter, Inc. 153 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 154 | * ======================================================================== */ 155 | 156 | 157 | +function ($) { 158 | 'use strict'; 159 | 160 | // MODAL CLASS DEFINITION 161 | // ====================== 162 | 163 | var Modal = function (element, options) { 164 | this.options = options 165 | this.$body = $(document.body) 166 | this.$element = $(element) 167 | this.$dialog = this.$element.find('.modal-dialog') 168 | this.$backdrop = null 169 | this.isShown = null 170 | this.originalBodyPad = null 171 | this.scrollbarWidth = 0 172 | this.ignoreBackdropClick = false 173 | 174 | if (this.options.remote) { 175 | this.$element 176 | .find('.modal-content') 177 | .load(this.options.remote, $.proxy(function () { 178 | this.$element.trigger('loaded.bs.modal') 179 | }, this)) 180 | } 181 | } 182 | 183 | Modal.VERSION = '3.3.7' 184 | 185 | Modal.TRANSITION_DURATION = 300 186 | Modal.BACKDROP_TRANSITION_DURATION = 150 187 | 188 | Modal.DEFAULTS = { 189 | backdrop: true, 190 | keyboard: true, 191 | show: true 192 | } 193 | 194 | Modal.prototype.toggle = function (_relatedTarget) { 195 | return this.isShown ? this.hide() : this.show(_relatedTarget) 196 | } 197 | 198 | Modal.prototype.show = function (_relatedTarget) { 199 | var that = this 200 | var e = $.Event('show.bs.modal', { relatedTarget: _relatedTarget }) 201 | 202 | this.$element.trigger(e) 203 | 204 | if (this.isShown || e.isDefaultPrevented()) return 205 | 206 | this.isShown = true 207 | 208 | this.checkScrollbar() 209 | this.setScrollbar() 210 | this.$body.addClass('modal-open') 211 | 212 | this.escape() 213 | this.resize() 214 | 215 | this.$element.on('click.dismiss.bs.modal', '[data-dismiss="modal"]', $.proxy(this.hide, this)) 216 | 217 | this.$dialog.on('mousedown.dismiss.bs.modal', function () { 218 | that.$element.one('mouseup.dismiss.bs.modal', function (e) { 219 | if ($(e.target).is(that.$element)) that.ignoreBackdropClick = true 220 | }) 221 | }) 222 | 223 | this.backdrop(function () { 224 | var transition = $.support.transition && that.$element.hasClass('fade') 225 | 226 | if (!that.$element.parent().length) { 227 | that.$element.appendTo(that.$body) // don't move modals dom position 228 | } 229 | 230 | that.$element 231 | .show() 232 | .scrollTop(0) 233 | 234 | that.adjustDialog() 235 | 236 | if (transition) { 237 | that.$element[0].offsetWidth // force reflow 238 | } 239 | 240 | that.$element.addClass('in') 241 | 242 | that.enforceFocus() 243 | 244 | var e = $.Event('shown.bs.modal', { relatedTarget: _relatedTarget }) 245 | 246 | transition ? 247 | that.$dialog // wait for modal to slide in 248 | .one('bsTransitionEnd', function () { 249 | that.$element.trigger('focus').trigger(e) 250 | }) 251 | .emulateTransitionEnd(Modal.TRANSITION_DURATION) : 252 | that.$element.trigger('focus').trigger(e) 253 | }) 254 | } 255 | 256 | Modal.prototype.hide = function (e) { 257 | if (e) e.preventDefault() 258 | 259 | e = $.Event('hide.bs.modal') 260 | 261 | this.$element.trigger(e) 262 | 263 | if (!this.isShown || e.isDefaultPrevented()) return 264 | 265 | this.isShown = false 266 | 267 | this.escape() 268 | this.resize() 269 | 270 | $(document).off('focusin.bs.modal') 271 | 272 | this.$element 273 | .removeClass('in') 274 | .off('click.dismiss.bs.modal') 275 | .off('mouseup.dismiss.bs.modal') 276 | 277 | this.$dialog.off('mousedown.dismiss.bs.modal') 278 | 279 | $.support.transition && this.$element.hasClass('fade') ? 280 | this.$element 281 | .one('bsTransitionEnd', $.proxy(this.hideModal, this)) 282 | .emulateTransitionEnd(Modal.TRANSITION_DURATION) : 283 | this.hideModal() 284 | } 285 | 286 | Modal.prototype.enforceFocus = function () { 287 | $(document) 288 | .off('focusin.bs.modal') // guard against infinite focus loop 289 | .on('focusin.bs.modal', $.proxy(function (e) { 290 | if (document !== e.target && 291 | this.$element[0] !== e.target && 292 | !this.$element.has(e.target).length) { 293 | this.$element.trigger('focus') 294 | } 295 | }, this)) 296 | } 297 | 298 | Modal.prototype.escape = function () { 299 | if (this.isShown && this.options.keyboard) { 300 | this.$element.on('keydown.dismiss.bs.modal', $.proxy(function (e) { 301 | e.which == 27 && this.hide() 302 | }, this)) 303 | } else if (!this.isShown) { 304 | this.$element.off('keydown.dismiss.bs.modal') 305 | } 306 | } 307 | 308 | Modal.prototype.resize = function () { 309 | if (this.isShown) { 310 | $(window).on('resize.bs.modal', $.proxy(this.handleUpdate, this)) 311 | } else { 312 | $(window).off('resize.bs.modal') 313 | } 314 | } 315 | 316 | Modal.prototype.hideModal = function () { 317 | var that = this 318 | this.$element.hide() 319 | this.backdrop(function () { 320 | that.$body.removeClass('modal-open') 321 | that.resetAdjustments() 322 | that.resetScrollbar() 323 | that.$element.trigger('hidden.bs.modal') 324 | }) 325 | } 326 | 327 | Modal.prototype.removeBackdrop = function () { 328 | this.$backdrop && this.$backdrop.remove() 329 | this.$backdrop = null 330 | } 331 | 332 | Modal.prototype.backdrop = function (callback) { 333 | var that = this 334 | var animate = this.$element.hasClass('fade') ? 'fade' : '' 335 | 336 | if (this.isShown && this.options.backdrop) { 337 | var doAnimate = $.support.transition && animate 338 | 339 | this.$backdrop = $(document.createElement('div')) 340 | .addClass('modal-backdrop ' + animate) 341 | .appendTo(this.$body) 342 | 343 | this.$element.on('click.dismiss.bs.modal', $.proxy(function (e) { 344 | if (this.ignoreBackdropClick) { 345 | this.ignoreBackdropClick = false 346 | return 347 | } 348 | if (e.target !== e.currentTarget) return 349 | this.options.backdrop == 'static' 350 | ? this.$element[0].focus() 351 | : this.hide() 352 | }, this)) 353 | 354 | if (doAnimate) this.$backdrop[0].offsetWidth // force reflow 355 | 356 | this.$backdrop.addClass('in') 357 | 358 | if (!callback) return 359 | 360 | doAnimate ? 361 | this.$backdrop 362 | .one('bsTransitionEnd', callback) 363 | .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : 364 | callback() 365 | 366 | } else if (!this.isShown && this.$backdrop) { 367 | this.$backdrop.removeClass('in') 368 | 369 | var callbackRemove = function () { 370 | that.removeBackdrop() 371 | callback && callback() 372 | } 373 | $.support.transition && this.$element.hasClass('fade') ? 374 | this.$backdrop 375 | .one('bsTransitionEnd', callbackRemove) 376 | .emulateTransitionEnd(Modal.BACKDROP_TRANSITION_DURATION) : 377 | callbackRemove() 378 | 379 | } else if (callback) { 380 | callback() 381 | } 382 | } 383 | 384 | // these following methods are used to handle overflowing modals 385 | 386 | Modal.prototype.handleUpdate = function () { 387 | this.adjustDialog() 388 | } 389 | 390 | Modal.prototype.adjustDialog = function () { 391 | var modalIsOverflowing = this.$element[0].scrollHeight > document.documentElement.clientHeight 392 | 393 | this.$element.css({ 394 | paddingLeft: !this.bodyIsOverflowing && modalIsOverflowing ? this.scrollbarWidth : '', 395 | paddingRight: this.bodyIsOverflowing && !modalIsOverflowing ? this.scrollbarWidth : '' 396 | }) 397 | } 398 | 399 | Modal.prototype.resetAdjustments = function () { 400 | this.$element.css({ 401 | paddingLeft: '', 402 | paddingRight: '' 403 | }) 404 | } 405 | 406 | Modal.prototype.checkScrollbar = function () { 407 | var fullWindowWidth = window.innerWidth 408 | if (!fullWindowWidth) { // workaround for missing window.innerWidth in IE8 409 | var documentElementRect = document.documentElement.getBoundingClientRect() 410 | fullWindowWidth = documentElementRect.right - Math.abs(documentElementRect.left) 411 | } 412 | this.bodyIsOverflowing = document.body.clientWidth < fullWindowWidth 413 | this.scrollbarWidth = this.measureScrollbar() 414 | } 415 | 416 | Modal.prototype.setScrollbar = function () { 417 | var bodyPad = parseInt((this.$body.css('padding-right') || 0), 10) 418 | this.originalBodyPad = document.body.style.paddingRight || '' 419 | if (this.bodyIsOverflowing) this.$body.css('padding-right', bodyPad + this.scrollbarWidth) 420 | } 421 | 422 | Modal.prototype.resetScrollbar = function () { 423 | this.$body.css('padding-right', this.originalBodyPad) 424 | } 425 | 426 | Modal.prototype.measureScrollbar = function () { // thx walsh 427 | var scrollDiv = document.createElement('div') 428 | scrollDiv.className = 'modal-scrollbar-measure' 429 | this.$body.append(scrollDiv) 430 | var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth 431 | this.$body[0].removeChild(scrollDiv) 432 | return scrollbarWidth 433 | } 434 | 435 | 436 | // MODAL PLUGIN DEFINITION 437 | // ======================= 438 | 439 | function Plugin(option, _relatedTarget) { 440 | return this.each(function () { 441 | var $this = $(this) 442 | var data = $this.data('bs.modal') 443 | var options = $.extend({}, Modal.DEFAULTS, $this.data(), typeof option == 'object' && option) 444 | 445 | if (!data) $this.data('bs.modal', (data = new Modal(this, options))) 446 | if (typeof option == 'string') data[option](_relatedTarget) 447 | else if (options.show) data.show(_relatedTarget) 448 | }) 449 | } 450 | 451 | var old = $.fn.modal 452 | 453 | $.fn.modal = Plugin 454 | $.fn.modal.Constructor = Modal 455 | 456 | 457 | // MODAL NO CONFLICT 458 | // ================= 459 | 460 | $.fn.modal.noConflict = function () { 461 | $.fn.modal = old 462 | return this 463 | } 464 | 465 | 466 | // MODAL DATA-API 467 | // ============== 468 | 469 | $(document).on('click.bs.modal.data-api', '[data-toggle="modal"]', function (e) { 470 | var $this = $(this) 471 | var href = $this.attr('href') 472 | var $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) // strip for ie7 473 | var option = $target.data('bs.modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data()) 474 | 475 | if ($this.is('a')) e.preventDefault() 476 | 477 | $target.one('show.bs.modal', function (showEvent) { 478 | if (showEvent.isDefaultPrevented()) return // only register focus restorer if modal will actually get shown 479 | $target.one('hidden.bs.modal', function () { 480 | $this.is(':visible') && $this.trigger('focus') 481 | }) 482 | }) 483 | Plugin.call($target, option, this) 484 | }) 485 | 486 | }(jQuery); 487 | 488 | /* ======================================================================== 489 | * Bootstrap: collapse.js v3.3.7 490 | * http://getbootstrap.com/javascript/#collapse 491 | * ======================================================================== 492 | * Copyright 2011-2016 Twitter, Inc. 493 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 494 | * ======================================================================== */ 495 | 496 | /* jshint latedef: false */ 497 | 498 | +function ($) { 499 | 'use strict'; 500 | 501 | // COLLAPSE PUBLIC CLASS DEFINITION 502 | // ================================ 503 | 504 | var Collapse = function (element, options) { 505 | this.$element = $(element) 506 | this.options = $.extend({}, Collapse.DEFAULTS, options) 507 | this.$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' + 508 | '[data-toggle="collapse"][data-target="#' + element.id + '"]') 509 | this.transitioning = null 510 | 511 | if (this.options.parent) { 512 | this.$parent = this.getParent() 513 | } else { 514 | this.addAriaAndCollapsedClass(this.$element, this.$trigger) 515 | } 516 | 517 | if (this.options.toggle) this.toggle() 518 | } 519 | 520 | Collapse.VERSION = '3.3.7' 521 | 522 | Collapse.TRANSITION_DURATION = 350 523 | 524 | Collapse.DEFAULTS = { 525 | toggle: true 526 | } 527 | 528 | Collapse.prototype.dimension = function () { 529 | var hasWidth = this.$element.hasClass('width') 530 | return hasWidth ? 'width' : 'height' 531 | } 532 | 533 | Collapse.prototype.show = function () { 534 | if (this.transitioning || this.$element.hasClass('in')) return 535 | 536 | var activesData 537 | var actives = this.$parent && this.$parent.children('.panel').children('.in, .collapsing') 538 | 539 | if (actives && actives.length) { 540 | activesData = actives.data('bs.collapse') 541 | if (activesData && activesData.transitioning) return 542 | } 543 | 544 | var startEvent = $.Event('show.bs.collapse') 545 | this.$element.trigger(startEvent) 546 | if (startEvent.isDefaultPrevented()) return 547 | 548 | if (actives && actives.length) { 549 | Plugin.call(actives, 'hide') 550 | activesData || actives.data('bs.collapse', null) 551 | } 552 | 553 | var dimension = this.dimension() 554 | 555 | this.$element 556 | .removeClass('collapse') 557 | .addClass('collapsing')[dimension](0) 558 | .attr('aria-expanded', true) 559 | 560 | this.$trigger 561 | .removeClass('collapsed') 562 | .attr('aria-expanded', true) 563 | 564 | this.transitioning = 1 565 | 566 | var complete = function () { 567 | this.$element 568 | .removeClass('collapsing') 569 | .addClass('collapse in')[dimension]('') 570 | this.transitioning = 0 571 | this.$element 572 | .trigger('shown.bs.collapse') 573 | } 574 | 575 | if (!$.support.transition) return complete.call(this) 576 | 577 | var scrollSize = $.camelCase(['scroll', dimension].join('-')) 578 | 579 | this.$element 580 | .one('bsTransitionEnd', $.proxy(complete, this)) 581 | .emulateTransitionEnd(Collapse.TRANSITION_DURATION)[dimension](this.$element[0][scrollSize]) 582 | } 583 | 584 | Collapse.prototype.hide = function () { 585 | if (this.transitioning || !this.$element.hasClass('in')) return 586 | 587 | var startEvent = $.Event('hide.bs.collapse') 588 | this.$element.trigger(startEvent) 589 | if (startEvent.isDefaultPrevented()) return 590 | 591 | var dimension = this.dimension() 592 | 593 | this.$element[dimension](this.$element[dimension]())[0].offsetHeight 594 | 595 | this.$element 596 | .addClass('collapsing') 597 | .removeClass('collapse in') 598 | .attr('aria-expanded', false) 599 | 600 | this.$trigger 601 | .addClass('collapsed') 602 | .attr('aria-expanded', false) 603 | 604 | this.transitioning = 1 605 | 606 | var complete = function () { 607 | this.transitioning = 0 608 | this.$element 609 | .removeClass('collapsing') 610 | .addClass('collapse') 611 | .trigger('hidden.bs.collapse') 612 | } 613 | 614 | if (!$.support.transition) return complete.call(this) 615 | 616 | this.$element 617 | [dimension](0) 618 | .one('bsTransitionEnd', $.proxy(complete, this)) 619 | .emulateTransitionEnd(Collapse.TRANSITION_DURATION) 620 | } 621 | 622 | Collapse.prototype.toggle = function () { 623 | this[this.$element.hasClass('in') ? 'hide' : 'show']() 624 | } 625 | 626 | Collapse.prototype.getParent = function () { 627 | return $(this.options.parent) 628 | .find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]') 629 | .each($.proxy(function (i, element) { 630 | var $element = $(element) 631 | this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element) 632 | }, this)) 633 | .end() 634 | } 635 | 636 | Collapse.prototype.addAriaAndCollapsedClass = function ($element, $trigger) { 637 | var isOpen = $element.hasClass('in') 638 | 639 | $element.attr('aria-expanded', isOpen) 640 | $trigger 641 | .toggleClass('collapsed', !isOpen) 642 | .attr('aria-expanded', isOpen) 643 | } 644 | 645 | function getTargetFromTrigger($trigger) { 646 | var href 647 | var target = $trigger.attr('data-target') 648 | || (href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') // strip for ie7 649 | 650 | return $(target) 651 | } 652 | 653 | 654 | // COLLAPSE PLUGIN DEFINITION 655 | // ========================== 656 | 657 | function Plugin(option) { 658 | return this.each(function () { 659 | var $this = $(this) 660 | var data = $this.data('bs.collapse') 661 | var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option == 'object' && option) 662 | 663 | if (!data && options.toggle && /show|hide/.test(option)) options.toggle = false 664 | if (!data) $this.data('bs.collapse', (data = new Collapse(this, options))) 665 | if (typeof option == 'string') data[option]() 666 | }) 667 | } 668 | 669 | var old = $.fn.collapse 670 | 671 | $.fn.collapse = Plugin 672 | $.fn.collapse.Constructor = Collapse 673 | 674 | 675 | // COLLAPSE NO CONFLICT 676 | // ==================== 677 | 678 | $.fn.collapse.noConflict = function () { 679 | $.fn.collapse = old 680 | return this 681 | } 682 | 683 | 684 | // COLLAPSE DATA-API 685 | // ================= 686 | 687 | $(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function (e) { 688 | var $this = $(this) 689 | 690 | if (!$this.attr('data-target')) e.preventDefault() 691 | 692 | var $target = getTargetFromTrigger($this) 693 | var data = $target.data('bs.collapse') 694 | var option = data ? 'toggle' : $this.data() 695 | 696 | Plugin.call($target, option) 697 | }) 698 | 699 | }(jQuery); 700 | -------------------------------------------------------------------------------- /html/js/jqColorPicker.js: -------------------------------------------------------------------------------- 1 | /*! tinyColorPicker - v1.1.1 2016-08-30 */ 2 | 3 | !function(a,b){"object"==typeof exports?module.exports=b(a):"function"==typeof define&&define.amd?define("colors",[],function(){return b(a)}):a.Colors=b(a)}(this,function(a,b){"use strict";function c(a,c,d,f,g){if("string"==typeof c){var c=v.txt2color(c);d=c.type,p[d]=c[d],g=g!==b?g:c.alpha}else if(c)for(var h in c)a[d][h]=k(c[h]/l[d][h][1],0,1);return g!==b&&(a.alpha=k(+g,0,1)),e(d,f?a:b)}function d(a,b,c){var d=o.options.grey,e={};return e.RGB={r:a.r,g:a.g,b:a.b},e.rgb={r:b.r,g:b.g,b:b.b},e.alpha=c,e.equivalentGrey=n(d.r*a.r+d.g*a.g+d.b*a.b),e.rgbaMixBlack=i(b,{r:0,g:0,b:0},c,1),e.rgbaMixWhite=i(b,{r:1,g:1,b:1},c,1),e.rgbaMixBlack.luminance=h(e.rgbaMixBlack,!0),e.rgbaMixWhite.luminance=h(e.rgbaMixWhite,!0),o.options.customBG&&(e.rgbaMixCustom=i(b,o.options.customBG,c,1),e.rgbaMixCustom.luminance=h(e.rgbaMixCustom,!0),o.options.customBG.luminance=h(o.options.customBG,!0)),e}function e(a,b){var c,e,k,q=b||p,r=v,s=o.options,t=l,u=q.RND,w="",x="",y={hsl:"hsv",rgb:a},z=u.rgb;if("alpha"!==a){for(var A in t)if(!t[A][A]){a!==A&&(x=y[A]||"rgb",q[A]=r[x+"2"+A](q[x])),u[A]||(u[A]={}),c=q[A];for(w in c)u[A][w]=n(c[w]*t[A][w][1])}z=u.rgb,q.HEX=r.RGB2HEX(z),q.equivalentGrey=s.grey.r*q.rgb.r+s.grey.g*q.rgb.g+s.grey.b*q.rgb.b,q.webSave=e=f(z,51),q.webSmart=k=f(z,17),q.saveColor=z.r===e.r&&z.g===e.g&&z.b===e.b?"web save":z.r===k.r&&z.g===k.g&&z.b===k.b?"web smart":"",q.hueRGB=v.hue2RGB(q.hsv.h),b&&(q.background=d(z,q.rgb,q.alpha))}var B,C,D,E=q.rgb,F=q.alpha,G="luminance",H=q.background;return B=i(E,{r:0,g:0,b:0},F,1),B[G]=h(B,!0),q.rgbaMixBlack=B,C=i(E,{r:1,g:1,b:1},F,1),C[G]=h(C,!0),q.rgbaMixWhite=C,s.customBG&&(D=i(E,H.rgbaMixCustom,F,1),D[G]=h(D,!0),D.WCAG2Ratio=j(D[G],H.rgbaMixCustom[G]),q.rgbaMixBGMixCustom=D,D.luminanceDelta=m.abs(D[G]-H.rgbaMixCustom[G]),D.hueDelta=g(H.rgbaMixCustom,D,!0)),q.RGBLuminance=h(z),q.HUELuminance=h(q.hueRGB),s.convertCallback&&s.convertCallback(q,a),q}function f(a,b){var c={},d=0,e=b/2;for(var f in a)d=a[f]%b,c[f]=a[f]+(d>e?b-d:-d);return c}function g(a,b,c){return(m.max(a.r-b.r,b.r-a.r)+m.max(a.g-b.g,b.g-a.g)+m.max(a.b-b.b,b.b-a.b))*(c?255:1)/765}function h(a,b){for(var c=b?1:255,d=[a.r/c,a.g/c,a.b/c],e=o.options.luminance,f=d.length;f--;)d[f]=d[f]<=.03928?d[f]/12.92:m.pow((d[f]+.055)/1.055,2.4);return e.r*d[0]+e.g*d[1]+e.b*d[2]}function i(a,c,d,e){var f={},g=d!==b?d:1,h=e!==b?e:1,i=g+h*(1-g);for(var j in a)f[j]=(a[j]*g+c[j]*h*(1-g))/i;return f.a=i,f}function j(a,b){var c=1;return c=a>=b?(a+.05)/(b+.05):(b+.05)/(a+.05),n(100*c)/100}function k(a,b,c){return a>c?c:b>a?b:a}var l={rgb:{r:[0,255],g:[0,255],b:[0,255]},hsv:{h:[0,360],s:[0,100],v:[0,100]},hsl:{h:[0,360],s:[0,100],l:[0,100]},alpha:{alpha:[0,1]},HEX:{HEX:[0,16777215]}},m=a.Math,n=m.round,o={},p={},q={r:.298954,g:.586434,b:.114612},r={r:.2126,g:.7152,b:.0722},s=function(a){this.colors={RND:{}},this.options={color:"rgba(0,0,0,0)",grey:q,luminance:r,valueRanges:l},t(this,a||{})},t=function(a,d){var e,f=a.options;u(a);for(var g in d)d[g]!==b&&(f[g]=d[g]);e=f.customBG,f.customBG="string"==typeof e?v.txt2color(e).rgb:e,p=c(a.colors,f.color,b,!0)},u=function(a){o!==a&&(o=a,p=a.colors)};s.prototype.setColor=function(a,d,f){return u(this),a?c(this.colors,a,d,b,f):(f!==b&&(this.colors.alpha=k(f,0,1)),e(d))},s.prototype.setCustomBackground=function(a){return u(this),this.options.customBG="string"==typeof a?v.txt2color(a).rgb:a,c(this.colors,b,"rgb")},s.prototype.saveAsBackground=function(){return u(this),c(this.colors,b,"rgb",!0)},s.prototype.toString=function(a,b){return v.color2text((a||"rgb").toLowerCase(),this.colors,b)};var v={txt2color:function(a){var b={},c=a.replace(/(?:#|\)|%)/g,"").split("("),d=(c[1]||"").split(/,\s*/),e=c[1]?c[0].substr(0,3):"rgb",f="";if(b.type=e,b[e]={},c[1])for(var g=3;g--;)f=e[g]||e.charAt(g),b[e][f]=+d[g]/l[e][f][1];else b.rgb=v.HEX2rgb(c[0]);return b.alpha=d[3]?+d[3]:1,b},color2text:function(a,b,c){var d=c!==!1&&n(100*b.alpha)/100,e="number"==typeof d&&c!==!1&&(c||1!==d),f=b.RND.rgb,g=b.RND.hsl,h="hex"===a&&e,i="hex"===a&&!h,j="rgb"===a||h,k=j?f.r+", "+f.g+", "+f.b:i?"#"+b.HEX:g.h+", "+g.s+"%, "+g.l+"%";return i?k:(h?"rgb":a)+(e?"a":"")+"("+k+(e?", "+d:"")+")"},RGB2HEX:function(a){return((a.r<16?"0":"")+a.r.toString(16)+(a.g<16?"0":"")+a.g.toString(16)+(a.b<16?"0":"")+a.b.toString(16)).toUpperCase()},HEX2rgb:function(a){return a=a.split(""),{r:+("0x"+a[0]+a[a[3]?1:0])/255,g:+("0x"+a[a[3]?2:1]+(a[3]||a[1]))/255,b:+("0x"+(a[4]||a[2])+(a[5]||a[2]))/255}},hue2RGB:function(a){var b=6*a,c=~~b%6,d=6===b?0:b-c;return{r:n(255*[1,1-d,0,0,d,1][c]),g:n(255*[d,1,1,1-d,0,0][c]),b:n(255*[0,0,d,1,1,1-d][c])}},rgb2hsv:function(a){var b,c,d,e=a.r,f=a.g,g=a.b,h=0;return g>f&&(f=g+(g=f,0),h=-1),c=g,f>e&&(e=f+(f=e,0),h=-2/6-h,c=m.min(f,g)),b=e-c,d=e?b/e:0,{h:1e-15>d?p&&p.hsl&&p.hsl.h||0:b?m.abs(h+(f-g)/(6*b)):0,s:e?b/e:p&&p.hsv&&p.hsv.s||0,v:e}},hsv2rgb:function(a){var b=6*a.h,c=a.s,d=a.v,e=~~b,f=b-e,g=d*(1-c),h=d*(1-f*c),i=d*(1-(1-f)*c),j=e%6;return{r:[d,h,g,g,i,d][j],g:[i,d,d,h,g,g][j],b:[g,g,i,d,d,h][j]}},hsv2hsl:function(a){var b=(2-a.s)*a.v,c=a.s*a.v;return c=a.s?1>b?b?c/b:0:c/(2-b):0,{h:a.h,s:a.v||c?c:p&&p.hsl&&p.hsl.s||0,l:b/2}},rgb2hsl:function(a,b){var c=v.rgb2hsv(a);return v.hsv2hsl(b?c:p.hsv=c)},hsl2rgb:function(a){var b=6*a.h,c=a.s,d=a.l,e=.5>d?d*(1+c):d+c-c*d,f=d+d-e,g=e?(e-f)/e:0,h=~~b,i=b-h,j=e*g*i,k=f+j,l=e-j,m=h%6;return{r:[e,l,f,f,k,e][m],g:[k,e,e,l,f,f][m],b:[f,f,k,e,e,l][m]}}};return s}),function(a,b){"object"==typeof exports?module.exports=b(a,require("jquery"),require("colors")):"function"==typeof define&&define.amd?define(["jquery","colors"],function(c,d){return b(a,c,d)}):b(a,a.jQuery,a.Colors)}(this,function(a,b,c,d){"use strict";function e(a){return a.value||a.getAttribute("value")||b(a).css("background-color")||"#FFF"}function f(a){return a=a.originalEvent&&a.originalEvent.touches?a.originalEvent.touches[0]:a,a.originalEvent?a.originalEvent:a}function g(a){return b(a.find(r.doRender)[0]||a[0])}function h(c){var d=b(this),f=d.offset(),h=b(a),k=r.gap;c?(s=g(d),s._colorMode=s.data("colorMode"),p.$trigger=d,(t||i()).css(r.positionCallback.call(p,d)||{left:(t._left=f.left)-((t._left+=t._width-(h.scrollLeft()+h.width()))+k>0?t._left+k:0),top:(t._top=f.top+d.outerHeight())-((t._top+=t._height-(h.scrollTop()+h.height()))+k>0?t._top+k:0)}).show(r.animationSpeed,function(){c!==!0&&(y.toggle(!!r.opacity)._width=y.width(),v._width=v.width(),v._height=v.height(),u._height=u.height(),q.setColor(e(s[0])),n(!0))}).off(".tcp").on(D,".cp-xy-slider,.cp-z-slider,.cp-alpha",j)):p.$trigger&&b(t).hide(r.animationSpeed,function(){n(!1),p.$trigger=null}).off(".tcp")}function i(){return b("head")[r.cssPrepend?"prepend":"append"]('"),b(H).css({margin:r.margin}).appendTo("body").show(0,function(){p.$UI=t=b(this),F=r.GPU&&t.css("perspective")!==d,u=b(".cp-z-slider",this),v=b(".cp-xy-slider",this),w=b(".cp-xy-cursor",this),x=b(".cp-z-cursor",this),y=b(".cp-alpha",this),z=b(".cp-alpha-cursor",this),r.buildCallback.call(p,t),t.prepend("
").children().eq(0).css("width",t.children().eq(0).width()),t._width=this.offsetWidth,t._height=this.offsetHeight}).hide()}function j(a){var c=this.className.replace(/cp-(.*?)(?:\s*|$)/,"$1").replace("-","_");(a.button||a.which)>1||(a.preventDefault&&a.preventDefault(),a.returnValue=!1,s._offset=b(this).offset(),(c="xy_slider"===c?k:"z_slider"===c?l:m)(a),n(),A.on(E,function(){A.off(".tcp")}).on(C,function(a){c(a),n()}))}function k(a){var b=f(a),c=b.pageX-s._offset.left,d=b.pageY-s._offset.top;q.setColor({s:c/v._width*100,v:100-d/v._height*100},"hsv")}function l(a){var b=f(a).pageY-s._offset.top;q.setColor({h:360-b/u._height*360},"hsv")}function m(a){var b=f(a).pageX-s._offset.left,c=b/y._width;q.setColor({},"rgb",c)}function n(a){var b=q.colors,c=b.hueRGB,e=(b.RND.rgb,b.RND.hsl,r.dark),f=r.light,g=q.toString(s._colorMode,r.forceAlpha),h=b.HUELuminance>.22?e:f,i=b.rgbaMixBlack.luminance>.22?e:f,j=(1-b.hsv.h)*u._height,k=b.hsv.s*v._width,l=(1-b.hsv.v)*v._height,m=b.alpha*y._width,n=F?"translate3d":"",p=s[0].value,t=s[0].hasAttribute("value")&&""===p&&a!==d;v._css={backgroundColor:"rgb("+c.r+","+c.g+","+c.b+")"},w._css={transform:n+"("+k+"px, "+l+"px, 0)",left:F?"":k,top:F?"":l,borderColor:b.RGBLuminance>.22?e:f},x._css={transform:n+"(0, "+j+"px, 0)",top:F?"":j,borderColor:"transparent "+h},y._css={backgroundColor:"#"+b.HEX},z._css={transform:n+"("+m+"px, 0, 0)",left:F?"":m,borderColor:i+" transparent"},s._css={backgroundColor:t?"":g,color:t?"":b.rgbaMixBGMixCustom.luminance>.22?e:f},s.text=t?"":p!==g?g:"",a!==d?o(a):G(o)}function o(a){v.css(v._css),w.css(w._css),x.css(x._css),y.css(y._css),z.css(z._css),r.doRender&&s.css(s._css),s.text&&s.val(s.text),r.renderCallback.call(p,s,"boolean"==typeof a?a:d)}var p,q,r,s,t,u,v,w,x,y,z,A=b(document),B=b(),C="touchmove.tcp mousemove.tcp pointermove.tcp",D="touchstart.tcp mousedown.tcp pointerdown.tcp",E="touchend.tcp mouseup.tcp pointerup.tcp",F=!1,G=a.requestAnimationFrame||a.webkitRequestAnimationFrame||function(a){a()},H='
',I=".cp-color-picker{position:absolute;overflow:hidden;padding:6px 6px 0;background-color:#444;color:#bbb;font-family:Arial,Helvetica,sans-serif;font-size:12px;font-weight:400;cursor:default;border-radius:5px}.cp-color-picker>div{position:relative;overflow:hidden}.cp-xy-slider{float:left;height:128px;width:128px;margin-bottom:6px;background:linear-gradient(to right,#FFF,rgba(255,255,255,0))}.cp-white{height:100%;width:100%;background:linear-gradient(rgba(0,0,0,0),#000)}.cp-xy-cursor{position:absolute;top:0;width:10px;height:10px;margin:-5px;border:1px solid #fff;border-radius:100%;box-sizing:border-box}.cp-z-slider{float:right;margin-left:6px;height:128px;width:20px;background:linear-gradient(red 0,#f0f 17%,#00f 33%,#0ff 50%,#0f0 67%,#ff0 83%,red 100%)}.cp-z-cursor{position:absolute;margin-top:-4px;width:100%;border:4px solid #fff;border-color:transparent #fff;box-sizing:border-box}.cp-alpha{clear:both;width:100%;height:16px;margin:6px 0;background:linear-gradient(to right,#444,rgba(0,0,0,0))}.cp-alpha-cursor{position:absolute;margin-left:-4px;height:100%;border:4px solid #fff;border-color:#fff transparent;box-sizing:border-box}",J=function(a){q=this.color=new c(a),r=q.options,p=this};J.prototype={render:n,toggle:h},b.fn.colorPicker=function(c){var d=this,f=function(){};return c=b.extend({animationSpeed:150,GPU:!0,doRender:!0,customBG:"#FFF",opacity:!0,renderCallback:f,buildCallback:f,positionCallback:f,body:document.body,scrollResize:!0,gap:4,dark:"#222",light:"#DDD"},c),!p&&c.scrollResize&&b(a).on("resize.tcp scroll.tcp",function(){p.$trigger&&p.toggle.call(p.$trigger[0],!0)}),B=B.add(this),this.colorPicker=p||new J(c),this.options=c,b(c.body).off(".tcp").on(D,function(a){-1===B.add(t).add(b(t).find(a.target)).index(a.target)&&h()}),this.on("focusin.tcp click.tcp",function(a){p.color.options=b.extend(p.color.options,r=d.options),h.call(this,a)}).on("change.tcp",function(){q.setColor(this.value||"#FFF"),d.colorPicker.render(!0)}).each(function(){var a=e(this),d=a.split("("),f=g(b(this));f.data("colorMode",d[1]?d[0].substr(0,3):"HEX").attr("readonly",r.preventFocus),c.doRender&&f.css({"background-color":a,color:function(){return q.setColor(a).rgbaMixBGMixCustom.luminance>.22?c.dark:c.light}})})},b.fn.colorPicker.destroy=function(){b("*").off(".tcp"),p.toggle(!1),B=b()}}); 4 | //# sourceMappingURL=jqColorPicker.js.map -------------------------------------------------------------------------------- /html/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 60px; 3 | padding-bottom: 20px; 4 | } 5 | 6 | /* Fix single page anchor nav */ 7 | .anchor:before { 8 | display: block; 9 | content: " "; 10 | margin-top: -75px; 11 | height: 75px; 12 | visibility: hidden; 13 | } 14 | 15 | .esps { 16 | text-align: center; 17 | } 18 | 19 | .esps-legend { 20 | margin-top: 5px; 21 | margin-bottom: 5px; 22 | } 23 | 24 | .esps-table { 25 | margin-left: 10px; 26 | border-spacing: 10px; 27 | width: 100%; 28 | } 29 | 30 | .footer { 31 | position: fixed; 32 | bottom: 0; 33 | width: 100%; 34 | height: 20px; 35 | background-color: #222; 36 | color: #9d9d9d; 37 | } 38 | 39 | /* Color picker */ 40 | .colorpicker-saturation { 41 | width: 200px; 42 | height: 200px; 43 | } 44 | 45 | .colorpicker-hue, 46 | .colorpicker-alpha { 47 | width: 30px; 48 | height: 200px; 49 | } 50 | 51 | .colorpicker-color, 52 | .colorpicker-color div { 53 | height: 30px; 54 | } 55 | 56 | .btn-cp { 57 | border: 1px solid; 58 | width: 100%; 59 | } 60 | 61 | .white { 62 | background-color: #FFFFFF; 63 | border: 1px solid; 64 | width: 100%; 65 | } 66 | 67 | .red { 68 | background-color: #FF0000; 69 | border: 1px solid; 70 | width: 100%; 71 | } 72 | 73 | .green { 74 | background-color: #00FF00; 75 | border: 1px solid; 76 | width: 100%; 77 | } 78 | 79 | .blue { 80 | background-color: #0000FF; 81 | border: 1px solid; 82 | width: 100%; 83 | } 84 | 85 | canvas { 86 | background-color: black; 87 | width: 100%; 88 | height: auto; 89 | } 90 | 91 | /* Snackbar - position it at the bottom and in the middle of the screen */ 92 | #snackbar { 93 | visibility: hidden; /* Hidden by default. Visible on click */ 94 | min-width: 250px; /* Set a default minimum width */ 95 | margin-left: -125px; /* Divide value of min-width by 2 */ 96 | background-color: #333; /* Black background color */ 97 | color: #fff; /* White text color */ 98 | text-align: center; /* Centered text */ 99 | border-radius: 2px; /* Rounded borders */ 100 | padding: 16px; /* Padding */ 101 | position: fixed; /* Sit on top of the screen */ 102 | z-index: 1; /* Add a z-index if needed */ 103 | left: 50%; /* Center the snackbar */ 104 | bottom: 30px; /* 30px from the bottom */ 105 | } 106 | 107 | /* Show the snackbar when clicking on a button (class added with JavaScript) */ 108 | #snackbar.show { 109 | visibility: visible; /* Show the snackbar */ 110 | 111 | /* Add animation: Take 0.5 seconds to fade in and out the snackbar. 112 | However, delay the fade out process for 2.5 seconds */ 113 | -webkit-animation: fadein 0.5s, fadeout 0.5s 2.5s; 114 | animation: fadein 0.5s, fadeout 0.5s 2.5s; 115 | } 116 | 117 | /* Animations to fade the snackbar in and out */ 118 | @-webkit-keyframes fadein { 119 | from {bottom: 0; opacity: 0;} 120 | to {bottom: 30px; opacity: 1;} 121 | } 122 | 123 | @keyframes fadein { 124 | from {bottom: 0; opacity: 0;} 125 | to {bottom: 30px; opacity: 1;} 126 | } 127 | 128 | @-webkit-keyframes fadeout { 129 | from {bottom: 30px; opacity: 1;} 130 | to {bottom: 0; opacity: 0;} 131 | } 132 | 133 | @keyframes fadeout { 134 | from {bottom: 30px; opacity: 1;} 135 | to {bottom: 0; opacity: 0;} 136 | } 137 | 138 | /* Progress bar */ 139 | .bar { 140 | width: 100%; 141 | height: 20px; 142 | border: 1px solid #2980b9; 143 | border-radius: 3px; 144 | background-image: 145 | repeating-linear-gradient( 146 | -45deg, 147 | #2980b9, 148 | #2980b9 11px, 149 | #eee 10px, 150 | #eee 20px /* determines size */ 151 | ); 152 | background-size: 28px 28px; 153 | animation: move .5s linear infinite; 154 | } 155 | 156 | @keyframes move { 157 | 0% { 158 | background-position: 0 0; 159 | } 160 | 100% { 161 | background-position: 28px 0; 162 | } 163 | } 164 | 165 | .form-horizontal .control-label.text-left { 166 | text-align: left; 167 | } 168 | 169 | .btn { 170 | margin: 4px auto; 171 | } 172 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "espixelstick", 3 | "version": "1.0.0", 4 | "description": "Gulp based build system for ESPixelStick web files", 5 | "main": "gulpfile.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/forkineye/ESPixelStick.git" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/forkineye/ESPixelStick/issues" 17 | }, 18 | "homepage": "https://github.com/forkineye/ESPixelStick#readme", 19 | "devDependencies": { 20 | "del": "^3.0.0", 21 | "gulp": "^4.0.0", 22 | "gulp-clean-css": "^4.0.0", 23 | "gulp-concat": "^2.6.1", 24 | "gulp-gzip": "^1.4.2", 25 | "gulp-htmlmin": "^5.0.1", 26 | "gulp-inline-source": "^4.0.0", 27 | "gulp-markdown-github-style": "^1.1.1", 28 | "gulp-plumber": "^1.2.1", 29 | "gulp-rename": "^1.4.0", 30 | "gulp-uglify": "^3.0.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pwm.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "ESPixelStick.h" 3 | #include "pwm.h" 4 | 5 | extern config_t config; // Current configuration 6 | 7 | // PWM globals 8 | 9 | uint16_t last_pwm[NUM_GPIO]; // 0-1023, 0=dark 10 | 11 | // GPIO 6-11 are for flash chip 12 | #if defined (ESPS_MODE_PIXEL) || ( defined(ESPS_MODE_SERIAL) && (SEROUT_UART == 1)) 13 | // { 0,1, 3,4,5,12,13,14,15,16 }; // 2 is WS2811 led data 14 | uint32_t pwm_valid_gpio_mask = 0b11111000000111011; 15 | 16 | #elif defined(ESPS_MODE_SERIAL) && (SEROUT_UART == 0) 17 | // { 0, 2,3,4,5,12,13,14,15,16 }; // 1 is serial TX for DMX data 18 | uint32_t pwm_valid_gpio_mask = 0b11111000000111101; 19 | #endif 20 | 21 | 22 | #if defined(ESPS_SUPPORT_PWM) 23 | void setupPWM () { 24 | if ( config.pwm_global_enabled ) { 25 | if ( (config.pwm_freq >= 100) && (config.pwm_freq <= 1000) ) { 26 | analogWriteFreq(config.pwm_freq); 27 | } 28 | for (int gpio=0; gpio < NUM_GPIO; gpio++ ) { 29 | if ( ( pwm_valid_gpio_mask & 1<>6 : pixels.getData()[gpio_dmx]<<2; 51 | #elif defined(ESPS_MODE_SERIAL) 52 | pwm_val = (config.pwm_gamma) ? GAMMA_TABLE[serial.getData()[gpio_dmx]]>>6 : serial.getData()[gpio_dmx]<<2; 53 | #endif 54 | } else { 55 | pwm_val = 0; // dmx channel 65535 forces 0 pwm value 56 | } 57 | 58 | // relays dont like pwm, force output high or low if "digital" 59 | if (config.pwm_gpio_digital & 1<= 512) { 61 | pwm_val = 1023; 62 | } else { 63 | pwm_val = 0; 64 | } 65 | } 66 | 67 | // inverted pwm output 68 | if (config.pwm_gpio_invert & 1< 1023..0 70 | } 71 | 72 | if ( pwm_val != last_pwm[gpio]) { 73 | last_pwm[gpio] = pwm_val; 74 | analogWrite(gpio, pwm_val); 75 | } 76 | } 77 | } 78 | } 79 | } 80 | } 81 | #endif 82 | 83 | -------------------------------------------------------------------------------- /pwm.h: -------------------------------------------------------------------------------- 1 | #ifndef PWM_H_ 2 | #define PWM_H_ 3 | 4 | #include "gamma.h" 5 | 6 | #define NUM_GPIO 17 // 0 .. 16 inclusive 7 | 8 | extern uint32_t pwm_valid_gpio_mask; 9 | 10 | #if defined(ESPS_MODE_PIXEL) 11 | extern PixelDriver pixels; // Pixel object 12 | #elif defined(ESPS_MODE_SERIAL) 13 | extern SerialDriver serial; // Serial object 14 | #endif 15 | 16 | void setupPWM (); 17 | void handlePWM (); 18 | 19 | #endif // PWM_H_ 20 | -------------------------------------------------------------------------------- /rgbhsv.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "rgbhsv.h" 3 | 4 | hsv rgb2hsv(rgb in) 5 | { 6 | hsv out; 7 | double min, max, delta; 8 | 9 | min = in.r < in.g ? in.r : in.g; 10 | min = min < in.b ? min : in.b; 11 | 12 | max = in.r > in.g ? in.r : in.g; 13 | max = max > in.b ? max : in.b; 14 | 15 | out.v = max; // v 16 | delta = max - min; 17 | if (delta < 0.00001) 18 | { 19 | out.s = 0; 20 | out.h = 0; // undefined, maybe nan? 21 | return out; 22 | } 23 | if( max > 0.0 ) { // NOTE: if Max is == 0, this divide would cause a crash 24 | out.s = (delta / max); // s 25 | } else { 26 | // if max is 0, then r = g = b = 0 27 | // s = 0, v is undefined 28 | out.s = 0.0; 29 | out.h = NAN; // its now undefined 30 | return out; 31 | } 32 | if( in.r >= max ) // > is bogus, just keeps compilor happy 33 | out.h = ( in.g - in.b ) / delta; // between yellow & magenta 34 | else 35 | if( in.g >= max ) 36 | out.h = 2.0 + ( in.b - in.r ) / delta; // between cyan & yellow 37 | else 38 | out.h = 4.0 + ( in.r - in.g ) / delta; // between magenta & cyan 39 | 40 | out.h *= 60.0; // degrees 41 | 42 | if( out.h < 0.0 ) 43 | out.h += 360.0; 44 | 45 | return out; 46 | } 47 | 48 | 49 | rgb hsv2rgb(hsv in) 50 | { 51 | double hh, p, q, t, ff; 52 | long i; 53 | rgb out; 54 | 55 | if(in.s <= 0.0) { // < is bogus, just shuts up warnings 56 | out.r = in.v; 57 | out.g = in.v; 58 | out.b = in.v; 59 | return out; 60 | } 61 | hh = in.h; 62 | if(hh >= 360.0) hh = 0.0; 63 | hh /= 60.0; 64 | i = (long)hh; 65 | ff = hh - i; 66 | p = in.v * (1.0 - in.s); 67 | q = in.v * (1.0 - (in.s * ff)); 68 | t = in.v * (1.0 - (in.s * (1.0 - ff))); 69 | 70 | switch(i) { 71 | case 0: 72 | out.r = in.v; 73 | out.g = t; 74 | out.b = p; 75 | break; 76 | case 1: 77 | out.r = q; 78 | out.g = in.v; 79 | out.b = p; 80 | break; 81 | case 2: 82 | out.r = p; 83 | out.g = in.v; 84 | out.b = t; 85 | break; 86 | 87 | case 3: 88 | out.r = p; 89 | out.g = q; 90 | out.b = in.v; 91 | break; 92 | case 4: 93 | out.r = t; 94 | out.g = p; 95 | out.b = in.v; 96 | break; 97 | case 5: 98 | default: 99 | out.r = in.v; 100 | out.g = p; 101 | out.b = q; 102 | break; 103 | } 104 | return out; 105 | } 106 | 107 | -------------------------------------------------------------------------------- /rgbhsv.h: -------------------------------------------------------------------------------- 1 | 2 | typedef struct { 3 | double r; // a fraction between 0 and 1 4 | double g; // a fraction between 0 and 1 5 | double b; // a fraction between 0 and 1 6 | } rgb; 7 | 8 | typedef struct { 9 | double h; // angle in degrees 10 | double s; // a fraction between 0 and 1 11 | double v; // a fraction between 0 and 1 12 | } hsv; 13 | 14 | hsv rgb2hsv(rgb in); 15 | rgb hsv2rgb(hsv in); 16 | 17 | -------------------------------------------------------------------------------- /travis/firmware.json: -------------------------------------------------------------------------------- 1 | { 2 | "modes": [ 3 | { 4 | "name": "Pixel (WS2811 / GECE)", 5 | "description": "Pixel Mode", 6 | "version": "DEV (Travis-CI Build)", 7 | "file": "pixel-travis.bin" 8 | }, 9 | { 10 | "name": "Serial (DMX / Renard)", 11 | "description": "Serial Mode", 12 | "version": "DEV (Travis-CI Build)", 13 | "file": "serial-travis.bin" 14 | } 15 | ], 16 | 17 | "devices": [ 18 | { 19 | "name": "ESP01_1MB_128K", 20 | "esptool": { 21 | "reset": "none", 22 | "baudrate": "115200", 23 | "spiffsloc": "0xDB000" 24 | }, 25 | "mkspiffs": { 26 | "page": "256", 27 | "block": "4096", 28 | "size": "131072" 29 | } 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /travis/travis.md: -------------------------------------------------------------------------------- 1 | Development Release (Auto-Generated) 2 | ==================================== 3 | This release was auto-generated by [Travis-CI](https://travis-ci.org/forkineye/ESPixelStick). ESP8266 Arduino and all required libraries were pulled from their current github repositories. This release is meant for testing purposes and **is not** considered stable. The latest stable release can be found on the [GitHub Release Page](https://github.com/forkineye/ESPixelStick/releases/latest). EFU files for updating are not included, however you can create them yourself within ESPSFlashTool. 4 | -------------------------------------------------------------------------------- /udpraw.cpp: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "ESPixelStick.h" 10 | #include "udpraw.h" 11 | 12 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 13 | 14 | #ifdef ESPS_ENABLE_UDPRAW 15 | 16 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 17 | 18 | extern config_t config; 19 | extern Ticker idleTicker; 20 | 21 | #if defined(ESPS_MODE_PIXEL) 22 | extern PixelDriver pixels; // Pixel object 23 | #define _driver pixels 24 | #elif defined(ESPS_MODE_SERIAL) 25 | extern SerialDriver serial; // Serial object 26 | #define _driver serial 27 | #endif 28 | 29 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 30 | 31 | void UdpRaw::begin(uint16_t port /*= ESPS_UDP_RAW_DEFAULT_PORT*/) 32 | { 33 | bool isListening = false; 34 | 35 | if(config.multicast) 36 | { 37 | uint16_t universe = config.universe; 38 | IPAddress address = IPAddress(239, 255, ((universe >> 8) & 0xff), ((universe >> 0) & 0xff)); 39 | 40 | isListening = _udp.listenMulticast(address, port); 41 | } 42 | else 43 | { 44 | isListening = _udp.listen(port); 45 | } 46 | 47 | if(isListening) 48 | { 49 | MDNS.addService("hyperiond-rgbled", "udp", port); 50 | 51 | _udp.onPacket(std::bind(&UdpRaw::onPacket, this, std::placeholders::_1)); 52 | 53 | LOG_PORT.print(" - Local UDP RAW Port: "); 54 | LOG_PORT.println(port); 55 | } 56 | } 57 | 58 | void UdpRaw::onPacket(AsyncUDPPacket packet) 59 | { 60 | 61 | stats.last_seen = millis(); 62 | stats.last_clientIP = packet.remoteIP(); 63 | stats.num_packets++; 64 | if (packet.length() < config.channel_count) 65 | stats.short_packets++; 66 | if (packet.length() > config.channel_count) 67 | stats.long_packets++; 68 | 69 | // do not disturb effects... 70 | if ( (config.ds == DataSource::E131) || (config.ds == DataSource::IDLEWEB) ) { 71 | idleTicker.attach(config.effect_idletimeout, idleTimeout); 72 | if (config.ds == DataSource::IDLEWEB) { 73 | config.ds = DataSource::E131; 74 | } 75 | 76 | int nread = _min(packet.length(), config.channel_count); 77 | memcpy(_driver.getData(), packet.data(), nread); 78 | 79 | if (zeropad) { 80 | int nzero = config.channel_count - nread; 81 | if (nzero > 0) 82 | memset(_driver.getData() + nread, 0, nzero); 83 | } 84 | } 85 | } 86 | 87 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 88 | 89 | #endif //#ifdef ESPS_ENABLE_UDPRAW 90 | 91 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 92 | -------------------------------------------------------------------------------- /udpraw.h: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 2 | 3 | #ifndef __UDPRAW_H__ 4 | #define __UDPRAW_H__ 5 | 6 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 7 | 8 | #include 9 | 10 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 11 | 12 | #define ESPS_UDP_RAW_DEFAULT_PORT 2801 13 | 14 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 15 | 16 | // Status structure 17 | typedef struct { 18 | uint32_t num_packets; 19 | uint32_t long_packets; 20 | uint32_t short_packets; 21 | uint32_t packet_errors; 22 | IPAddress last_clientIP; 23 | uint16_t last_clientPort; 24 | unsigned long last_seen; 25 | } udpraw_stats_t; 26 | 27 | 28 | class UdpRaw 29 | { 30 | AsyncUDP _udp; 31 | 32 | public: 33 | udpraw_stats_t stats; // Statistics tracker 34 | bool zeropad = true; 35 | 36 | void begin(uint16_t port = ESPS_UDP_RAW_DEFAULT_PORT); 37 | 38 | void end() 39 | { 40 | _udp.close(); 41 | } 42 | 43 | private: 44 | void onPacket(AsyncUDPPacket packet); 45 | uint16_t port = ESPS_UDP_RAW_DEFAULT_PORT; 46 | }; 47 | 48 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 49 | 50 | #endif // #ifndef __UDPRAW_H__ 51 | 52 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 53 | -------------------------------------------------------------------------------- /wshandler.h: -------------------------------------------------------------------------------- 1 | /* 2 | * wshandler.h 3 | * 4 | * Project: ESPixelStick - An ESP8266 and E1.31 based pixel driver 5 | * Copyright (c) 2016 Shelby Merrick 6 | * http://www.forkineye.com 7 | * 8 | * This program is provided free for you to use in any way that you wish, 9 | * subject to the laws and regulations where you are using it. Due diligence 10 | * is strongly suggested before using this code. Please give credit where due. 11 | * 12 | * The Author makes no warranty of any kind, express or implied, with regard 13 | * to this program or the documentation contained in this document. The 14 | * Author shall not be liable in any event for incidental or consequential 15 | * damages in connection with, or arising out of, the furnishing, performance 16 | * or use of these programs. 17 | * 18 | */ 19 | 20 | #ifndef WSHANDLER_H_ 21 | #define WSHANDLER_H_ 22 | 23 | #include "ESPixelStick.h" 24 | 25 | #include "gpio.h" 26 | 27 | #if defined(ESPS_MODE_PIXEL) 28 | #include "PixelDriver.h" 29 | extern PixelDriver pixels; // Pixel object 30 | #elif defined(ESPS_MODE_SERIAL) 31 | #include "SerialDriver.h" 32 | extern SerialDriver serial; // Serial object 33 | #endif 34 | 35 | #if defined(ESPS_ENABLE_UDPRAW) 36 | #include "udpraw.h" 37 | extern UdpRaw udpraw; 38 | #endif 39 | 40 | extern EffectEngine effects; // EffectEngine for test modes 41 | 42 | extern AsyncWebSocket ws; 43 | extern ESPAsyncE131 e131; // ESPAsyncE131 with X buffers 44 | extern config_t config; // Current configuration 45 | extern uint32_t *seqError; // Sequence error tracking for each universe 46 | extern uint16_t uniLast; // Last Universe to listen for 47 | extern bool reboot; // Reboot flag 48 | 49 | extern unsigned long mqtt_last_seen; // millis() timestamp of last message 50 | extern uint32_t mqtt_num_packets; // count of message rcvd 51 | 52 | extern const char CONFIG_FILE[]; 53 | 54 | /* 55 | Packet Commands 56 | E1 - Get Elements 57 | 58 | G1 - Get Config 59 | G2 - Get Config Status 60 | G3 - Get Current Effect and Effect Config Options 61 | G4 - Get Gamma table values 62 | 63 | T0 - Disable Testing 64 | T1 - Static Testing 65 | T2 - Blink Test 66 | T3 - Flash Test 67 | T4 - Chase Test 68 | T5 - Rainbow Test 69 | T6 - Fire flicker 70 | T7 - Lightning 71 | T8 - Breathe 72 | 73 | V1 - View Stream 74 | 75 | S1 - Set Network Config 76 | S2 - Set Device Config 77 | S3 - Set Effect Startup Config 78 | S4 - Set Gamma and Brightness (but dont save) 79 | 80 | XJ - Get RSSI,heap,uptime, e131 stats 81 | 82 | X6 - Reboot 83 | */ 84 | 85 | EFUpdate efupdate; 86 | uint8_t * WSframetemp; 87 | uint8_t * confuploadtemp; 88 | 89 | // send unsolicited update to all clients with the client who initiated it 90 | void runningEffectSendAll(String updateSource) { 91 | String response; 92 | DynamicJsonBuffer jsonBuffer; 93 | JsonObject &json = jsonBuffer.createObject(); 94 | 95 | effects.runningEffectToJson (json); 96 | 97 | json["updateSource"] = updateSource; 98 | 99 | json.printTo(response); 100 | ws.textAll("G3" + response); 101 | } 102 | 103 | 104 | void procX(uint8_t *data, AsyncWebSocketClient *client) { 105 | switch (data[1]) { 106 | case 'J': { 107 | 108 | DynamicJsonBuffer jsonBuffer; 109 | JsonObject &json = jsonBuffer.createObject(); 110 | 111 | // system statistics 112 | JsonObject &system = json.createNestedObject("system"); 113 | system["rssi"] = (String)WiFi.RSSI(); 114 | system["freeheap"] = (String)ESP.getFreeHeap(); 115 | system["uptime"] = (String)millis(); 116 | switch (config.ds) { 117 | case DataSource::E131: 118 | system["datasource"] = "E131"; 119 | system["effectname"] = "N/A"; 120 | break; 121 | case DataSource::MQTT: 122 | system["datasource"] = "MQTT"; 123 | system["effectname"] = (String)effects.getEffect(); 124 | break; 125 | case DataSource::WEB: 126 | system["datasource"] = "web"; 127 | system["effectname"] = (String)effects.getEffect(); 128 | break; 129 | case DataSource::IDLEWEB: 130 | system["datasource"] = "Idle Effect"; 131 | system["effectname"] = (String)effects.getEffect(); 132 | break; 133 | default: 134 | system["datasource"] = "unknown"; 135 | system["effectname"] = (String)effects.getEffect(); 136 | break; 137 | } 138 | 139 | // E131 statistics 140 | JsonObject &e131J = json.createNestedObject("e131"); 141 | uint32_t seqErrors = 0; 142 | for (int i = 0; i < ((uniLast + 1) - config.universe); i++) 143 | seqErrors =+ seqError[i]; 144 | 145 | e131J["universe"] = (String)config.universe; 146 | e131J["uniLast"] = (String)uniLast; 147 | e131J["num_packets"] = (String)e131.stats.num_packets; 148 | e131J["seq_errors"] = (String)seqErrors; 149 | e131J["packet_errors"] = (String)e131.stats.packet_errors; 150 | e131J["last_clientIP"] = e131.stats.last_clientIP.toString(); 151 | e131J["last_seen"] = e131.stats.last_seen ? (String) (millis() - e131.stats.last_seen) : "never"; 152 | 153 | // MQTT statistics 154 | JsonObject &mqtt = json.createNestedObject("mqtt"); 155 | mqtt["num_packets"] = (String)mqtt_num_packets; 156 | mqtt["last_seen"] = mqtt_last_seen ? (String) (millis() - mqtt_last_seen) : "never"; 157 | 158 | #if defined(ESPS_ENABLE_UDPRAW) 159 | // UDP raw statistics 160 | JsonObject &udp = json.createNestedObject("udp"); 161 | udp["num_packets"] = (String)udpraw.stats.num_packets; 162 | udp["short_packets"] = (String)udpraw.stats.short_packets; 163 | udp["long_packets"] = (String)udpraw.stats.long_packets; 164 | udp["last_clientIP"] = udpraw.stats.last_clientIP.toString(); 165 | udp["last_seen"] = udpraw.stats.last_seen ? (String) (millis() - udpraw.stats.last_seen) : "never"; 166 | #endif 167 | 168 | String response; 169 | json.printTo(response); 170 | client->text("XJ" + response); 171 | break; 172 | } 173 | case '6': // Init 6 baby, reboot! 174 | reboot = true; 175 | } 176 | } 177 | 178 | void procE(uint8_t *data, AsyncWebSocketClient *client) { 179 | switch (data[1]) { 180 | case '1': 181 | // Create buffer and root object 182 | DynamicJsonBuffer jsonBuffer; 183 | JsonObject &json = jsonBuffer.createObject(); 184 | 185 | #if defined (ESPS_MODE_PIXEL) 186 | // Pixel Types 187 | JsonObject &p_type = json.createNestedObject("p_type"); 188 | p_type["WS2811 800kHz"] = static_cast(PixelType::WS2811); 189 | p_type["GE Color Effects"] = static_cast(PixelType::GECE); 190 | 191 | // Pixel Colors 192 | JsonObject &p_color = json.createNestedObject("p_color"); 193 | p_color["RGB"] = static_cast(PixelColor::RGB); 194 | p_color["GRB"] = static_cast(PixelColor::GRB); 195 | p_color["BRG"] = static_cast(PixelColor::BRG); 196 | p_color["RBG"] = static_cast(PixelColor::RBG); 197 | p_color["GBR"] = static_cast(PixelColor::GBR); 198 | p_color["BGR"] = static_cast(PixelColor::BGR); 199 | 200 | #elif defined (ESPS_MODE_SERIAL) 201 | // Serial Protocols 202 | JsonObject &s_proto = json.createNestedObject("s_proto"); 203 | s_proto["DMX512"] = static_cast(SerialType::DMX512); 204 | s_proto["Renard"] = static_cast(SerialType::RENARD); 205 | 206 | // Serial Baudrates 207 | JsonObject &s_baud = json.createNestedObject("s_baud"); 208 | s_baud["38400"] = static_cast(BaudRate::BR_38400); 209 | s_baud["57600"] = static_cast(BaudRate::BR_57600); 210 | s_baud["115200"] = static_cast(BaudRate::BR_115200); 211 | s_baud["230400"] = static_cast(BaudRate::BR_230400); 212 | s_baud["250000"] = static_cast(BaudRate::BR_250000); 213 | s_baud["460800"] = static_cast(BaudRate::BR_460800); 214 | #endif 215 | 216 | String response; 217 | json.printTo(response); 218 | client->text("E1" + response); 219 | break; 220 | } 221 | } 222 | 223 | void procG(uint8_t *data, AsyncWebSocketClient *client) { 224 | switch (data[1]) { 225 | case '1': { 226 | String response; 227 | serializeConfig(response, false, true); 228 | client->text("G1" + response); 229 | break; 230 | } 231 | 232 | case '2': { 233 | // Create buffer and root object 234 | DynamicJsonBuffer jsonBuffer; 235 | JsonObject &json = jsonBuffer.createObject(); 236 | 237 | json["ssid"] = (String)WiFi.SSID(); 238 | json["hostname"] = (String)WiFi.hostname(); 239 | json["ip"] = WiFi.localIP().toString(); 240 | json["mac"] = WiFi.macAddress(); 241 | json["version"] = (String)VERSION; 242 | json["built"] = (String)BUILD_DATE; 243 | json["flashchipid"] = String(ESP.getFlashChipId(), HEX); 244 | json["usedflashsize"] = (String)ESP.getFlashChipSize(); 245 | json["realflashsize"] = (String)ESP.getFlashChipRealSize(); 246 | json["freeheap"] = (String)ESP.getFreeHeap(); 247 | 248 | String response; 249 | json.printTo(response); 250 | client->text("G2" + response); 251 | break; 252 | } 253 | 254 | case '3': { 255 | String response; 256 | DynamicJsonBuffer jsonBuffer; 257 | JsonObject &json = jsonBuffer.createObject(); 258 | 259 | effects.runningEffectToJson(json); 260 | 261 | effects.EffectListToJson(json); 262 | 263 | // tell the client its "unique" id based on the IP and port as seen by us 264 | json["browserIPPort"] = String(client->remoteIP().toString() 265 | + ":" + client->remotePort()); 266 | 267 | json.printTo(response); 268 | client->text("G3" + response); 269 | //LOG_PORT.print(response); 270 | break; 271 | } 272 | 273 | case '4': { 274 | DynamicJsonBuffer jsonBuffer; 275 | JsonObject &json = jsonBuffer.createObject(); 276 | JsonArray &gamma = json.createNestedArray("gamma"); 277 | for (int i=0; i<256; i++) { 278 | gamma.add(GAMMA_TABLE[i] >> 8); 279 | } 280 | String response; 281 | json.printTo(response); 282 | client->text("G4" + response); 283 | break; 284 | } 285 | } 286 | } 287 | 288 | void procP(uint8_t *data, AsyncWebSocketClient *client) { 289 | client->text("PP" + handleGPIO(reinterpret_cast(data + 2)) ); 290 | } 291 | 292 | void procS(uint8_t *data, AsyncWebSocketClient *client) { 293 | DynamicJsonBuffer jsonBuffer; 294 | JsonObject &json = jsonBuffer.parseObject(reinterpret_cast(data + 2)); 295 | if (!json.success()) { 296 | LOG_PORT.println(F("*** procS(): Parse Error ***")); 297 | LOG_PORT.println(reinterpret_cast(data)); 298 | return; 299 | } 300 | 301 | bool reboot = false; 302 | switch (data[1]) { 303 | case '1': // Set Network Config 304 | dsNetworkConfig(json); 305 | saveConfig(); 306 | client->text("S1"); 307 | break; 308 | case '2': // Set Device Config 309 | // Reboot if MQTT changed 310 | if (config.mqtt != json["mqtt"]["enabled"]) 311 | reboot = true; 312 | 313 | dsDeviceConfig(json); 314 | saveConfig(); 315 | 316 | if (reboot) 317 | client->text("S1"); 318 | else 319 | client->text("S2"); 320 | break; 321 | case '3': // Set Effect Startup Config 322 | dsEffectConfig(json); 323 | saveConfig(); 324 | client->text("S3"); 325 | break; 326 | case '4': // Set Gamma (but no save) 327 | dsGammaConfig(json); 328 | client->text("S4"); 329 | break; 330 | } 331 | } 332 | 333 | void procT(uint8_t *data, AsyncWebSocketClient *client) { 334 | 335 | if (data[1] == '0') { 336 | //TODO: Store previous data source when effect is selected so we can switch back to it 337 | config.ds = DataSource::E131; 338 | effects.clearAll(); 339 | } 340 | else if ( (data[1] >= '1') && (data[1] <= '8') ) { 341 | String TCode; 342 | TCode += (char)data[0]; 343 | TCode += (char)data[1]; 344 | const EffectDesc* effectInfo = effects.getEffectInfo(TCode); 345 | 346 | if (effectInfo) { 347 | DynamicJsonBuffer jsonBuffer; 348 | JsonObject &json = jsonBuffer.parseObject(reinterpret_cast(data + 2)); 349 | 350 | config.ds = DataSource::WEB; 351 | effects.setEffect( effectInfo->name ); 352 | 353 | if ( effectInfo->hasColor ) { 354 | if (json.containsKey("r") && json.containsKey("g") && json.containsKey("b")) { 355 | effects.setColor({json["r"], json["g"], json["b"]}); 356 | } 357 | } 358 | if ( effectInfo->hasMirror ) { 359 | if (json.containsKey("mirror")) { 360 | effects.setMirror(json["mirror"]); 361 | } 362 | } 363 | if ( effectInfo->hasReverse ) { 364 | if (json.containsKey("reverse")) { 365 | effects.setReverse(json["reverse"]); 366 | } 367 | } 368 | if ( effectInfo->hasAllLeds ) { 369 | if (json.containsKey("allleds")) { 370 | effects.setAllLeds(json["allleds"]); 371 | } 372 | } 373 | if (json.containsKey("speed")) { 374 | effects.setSpeed(json["speed"]); 375 | } 376 | if (json.containsKey("brightness")) { 377 | effects.setBrightness(json["brightness"]); 378 | } 379 | client->text("OK"); 380 | // send unsolicited update to all clients with the client who initiated it 381 | runningEffectSendAll( String(client->remoteIP().toString() 382 | + ":" + client->remotePort())); 383 | } 384 | } 385 | 386 | if (config.mqtt) 387 | publishState(); 388 | } 389 | 390 | void procV(uint8_t *data, AsyncWebSocketClient *client) { 391 | switch (data[1]) { 392 | case '1': { // View stream 393 | #if defined(ESPS_MODE_PIXEL) 394 | client->binary(pixels.getData(), config.channel_count); 395 | #elif defined(ESPS_MODE_SERIAL) 396 | if (config.serial_type == SerialType::DMX512) 397 | client->binary(&serial.getData()[1], config.channel_count); 398 | else 399 | client->binary(&serial.getData()[2], config.channel_count); 400 | #endif 401 | break; 402 | } 403 | } 404 | } 405 | 406 | void handle_fw_upload(AsyncWebServerRequest *request, String filename, 407 | size_t index, uint8_t *data, size_t len, bool final) { 408 | if (!index) { 409 | WiFiUDP::stopAll(); 410 | LOG_PORT.print(F("* Upload Started: ")); 411 | LOG_PORT.println(filename.c_str()); 412 | efupdate.begin(); 413 | } 414 | 415 | if (!efupdate.process(data, len)) { 416 | LOG_PORT.print(F("*** UPDATE ERROR: ")); 417 | LOG_PORT.println(String(efupdate.getError())); 418 | } 419 | 420 | if (efupdate.hasError()) 421 | request->send(200, "text/plain", "Update Error: " + 422 | String(efupdate.getError())); 423 | 424 | if (final) { 425 | request->send(200, "text/plain", "Update Finished: " + 426 | String(efupdate.getError())); 427 | LOG_PORT.println(F("* Upload Finished.")); 428 | efupdate.end(); 429 | SPIFFS.begin(); 430 | saveConfig(); 431 | reboot = true; 432 | } 433 | } 434 | 435 | void handle_config_upload(AsyncWebServerRequest *request, String filename, 436 | size_t index, uint8_t *data, size_t len, bool final) { 437 | static File file; 438 | if (!index) { 439 | WiFiUDP::stopAll(); 440 | LOG_PORT.print(F("* Config Upload Started: ")); 441 | LOG_PORT.println(filename.c_str()); 442 | 443 | if (confuploadtemp) { 444 | free (confuploadtemp); 445 | confuploadtemp = nullptr; 446 | } 447 | confuploadtemp = (uint8_t*) malloc(CONFIG_MAX_SIZE); 448 | } 449 | 450 | LOG_PORT.printf("index %d len %d\n", index, len); 451 | memcpy(confuploadtemp + index, data, len); 452 | confuploadtemp[index + len] = 0; 453 | 454 | if (final) { 455 | int filesize = index+len; 456 | LOG_PORT.print(F("* Config Upload Finished:")); 457 | LOG_PORT.printf(" %d bytes", filesize); 458 | 459 | DynamicJsonBuffer jsonBuffer; 460 | JsonObject &json = jsonBuffer.parseObject(reinterpret_cast(confuploadtemp)); 461 | if (!json.success()) { 462 | LOG_PORT.println(F("*** Parse Error ***")); 463 | LOG_PORT.println(reinterpret_cast(confuploadtemp)); 464 | request->send(500, "text/plain", "Config Update Error." ); 465 | } else { 466 | dsNetworkConfig(json); 467 | dsDeviceConfig(json); 468 | dsEffectConfig(json); 469 | saveConfig(); 470 | request->send(200, "text/plain", "Config Update Finished: " ); 471 | // reboot = true; 472 | } 473 | 474 | if (confuploadtemp) { 475 | free (confuploadtemp); 476 | confuploadtemp = nullptr; 477 | } 478 | } 479 | } 480 | 481 | void wsEvent( __attribute__ ((unused)) AsyncWebSocket *server, AsyncWebSocketClient *client, 482 | AwsEventType type, void * arg, uint8_t *data, size_t len) { 483 | 484 | switch (type) { 485 | case WS_EVT_DATA: { 486 | AwsFrameInfo *info = static_cast(arg); 487 | if (info->final && info->index == 0 && info->len == len) { 488 | if (info->opcode == WS_TEXT) { 489 | data[len] = 0; 490 | switch (data[0]) { 491 | case 'X': 492 | procX(data, client); 493 | break; 494 | case 'E': 495 | procE(data, client); 496 | break; 497 | case 'G': 498 | procG(data, client); 499 | break; 500 | case 'P': 501 | procP(data, client); 502 | break; 503 | case 'S': 504 | procS(data, client); 505 | break; 506 | case 'T': 507 | procT(data, client); 508 | break; 509 | case 'V': 510 | procV(data, client); 511 | break; 512 | } 513 | } else { 514 | LOG_PORT.println(F("-- binary message --")); 515 | } 516 | } else { // multiframe WS message 517 | 518 | if ( (info->message_opcode == WS_TEXT) && (info->len < CONFIG_MAX_SIZE) ) { 519 | 520 | if ( (info->index == 0) && (info->num == 0) ) { 521 | if (WSframetemp) { 522 | free (WSframetemp); 523 | WSframetemp = nullptr; 524 | } 525 | WSframetemp = (uint8_t*) malloc(CONFIG_MAX_SIZE); 526 | } 527 | 528 | memcpy(WSframetemp + info->index, data, len); 529 | 530 | if ( (info->index + len) == info->len) { 531 | if (info->final) { 532 | WSframetemp[info->len] = 0; 533 | switch (WSframetemp[0]) { 534 | case 'S': 535 | procS(WSframetemp, client); 536 | break; 537 | } 538 | 539 | if (WSframetemp) { 540 | free (WSframetemp); 541 | WSframetemp = nullptr; 542 | } 543 | } 544 | } 545 | } 546 | } 547 | break; 548 | } 549 | case WS_EVT_CONNECT: 550 | LOG_PORT.print(F("* WS Connect - ")); 551 | LOG_PORT.println(client->id()); 552 | break; 553 | case WS_EVT_DISCONNECT: 554 | LOG_PORT.print(F("* WS Disconnect - ")); 555 | LOG_PORT.println(client->id()); 556 | break; 557 | case WS_EVT_PONG: 558 | LOG_PORT.println(F("* WS PONG *")); 559 | break; 560 | case WS_EVT_ERROR: 561 | LOG_PORT.println(F("** WS ERROR **")); 562 | break; 563 | } 564 | } 565 | 566 | #endif /* WSHANDLER_H_ */ 567 | --------------------------------------------------------------------------------