diff --git a/.github/workflows/wled-ci.yml b/.github/workflows/wled-ci.yml new file mode 100644 index 00000000..38e7c497 --- /dev/null +++ b/.github/workflows/wled-ci.yml @@ -0,0 +1,31 @@ +name: PlatformIO CI + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Cache pip + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + - name: Cache PlatformIO + uses: actions/cache@v2 + with: + path: ~/.platformio + key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} + - name: Set up Python + uses: actions/setup-python@v2 + - name: Install PlatformIO + run: | + python -m pip install --upgrade pip + pip install --upgrade platformio + - name: Run PlatformIO + run: pio run \ No newline at end of file diff --git a/.travis.yml b/.travis.yml.old similarity index 100% rename from .travis.yml rename to .travis.yml.old diff --git a/CHANGELOG.md b/CHANGELOG.md index 49841070..69ddeabb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,65 @@ ## WLED changelog -### WLED version 0.11.0 +### Development versions after 0.11.1 release + +#### Build 2012210 + +- Split index.htm in separate CSS + JS files (PR #1542) +- Minify UI HTML, saving >1.5kB flash +- Fixed JShint warnings + +#### Build 2012180 + +- Boot brightness 0 will now use the brightness from preset +- Add iOS scrolling momentum (from PR #1528) + +### WLED release 0.11.1 + +#### Build 2012180 + +- Release of WLED 0.11.1 "Mirai" +- Fixed AP hide not saving (fixes #1520) +- Fixed MQTT password re-transmitted to HTML +- Hide Update buttons while uploading, accept .bin +- Make sure AP password is at least 8 characters long + +### Development versions after 0.11.0 release + +#### Build 2012160 + +- Bump Espalexa to 2.5.0, fixing discovery (PR Espalexa/#152, originally PR #1497) + +#### Build 2012150 + +- Added Blends FX (PR #1491) +- Fixed an issue that made it impossible to deactivate timed presets + +#### Build 2012140 + +- Added Preset ID quick display option (PR #1462) +- Fixed LEDs not turning on when using gamma correct brightness and LEDPIN 2 (default) +- Fixed notifier applying main segment to selected segments on notification with FX/Col disabled + +#### Build 2012130 + +- Fixed RGBW mode not saved between reboots (fixes #1457) +- Added brightness scaling in palette function for default (PR #1484) + +#### Build 2012101 + +- Fixed preset cycle default duration rounded down to nearest 10sec interval (#1458) +- Enabled E1.31/DDP/Art-Net in AP mode + +#### Build 2012100 + +- Fixed multi-segment preset cycle +- Fixed EEPROM (pre-0.11 settings) not cleared on factory reset +- Fixed an issue with intermittent crashes on FX change (PR #1465) +- Added function to know if strip is updating (PR #1466) +- Fixed using colorwheel sliding the UI (PR #1459) +- Fixed analog clock settings not saving (PR #1448) +- Added Temperature palette (PR #1430) +- Added Candy cane FX (PR #1445) #### Build 2012020 @@ -11,6 +70,8 @@ - Fixed compilation for analog (PWM) LEDs +### WLED version 0.11.0 + #### Build 2011290 - Release of WLED 0.11.0 "Mirai" diff --git a/images/Readme.md b/images/Readme.md new file mode 100644 index 00000000..738a84f6 --- /dev/null +++ b/images/Readme.md @@ -0,0 +1,5 @@ +### Additional Logos + +Additional awesome logos for WLED can be found here [Aircoookie/Akemi](https://github.com/Aircoookie/Akemi). + + diff --git a/package-lock.json b/package-lock.json index 7462b5d8..42f12801 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "wled", - "version": "0.10.2", + "version": "0.11.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -958,9 +958,9 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, "inliner": { "version": "1.13.1", diff --git a/package.json b/package.json index 5d9ac9e4..ba3add2f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wled", - "version": "0.11.0", + "version": "0.11.1", "description": "Tools for WLED project", "main": "tools/cdata.js", "directories": { diff --git a/platformio.ini b/platformio.ini index 9d5ea9bc..7a8efcaf 100644 --- a/platformio.ini +++ b/platformio.ini @@ -179,7 +179,10 @@ extra_scripts = pio/name-firmware.py framework = arduino board_build.flash_mode = dout monitor_speed = 115200 +# slow upload speed (comment this out with a ';' when building for development use) upload_speed = 115200 +# fast upload speed (remove ';' when building for development use) +; upload_speed = 921600 # ------------------------------------------------------------------------------ # LIBRARIES: required dependencies @@ -361,6 +364,14 @@ board_build.ldscript = ${common.ldscript_4m1m} build_unflags = ${common.build_unflags} build_flags = ${common.build_flags_esp8266} +[env:anavi_miracle_controller] +board = d1_mini +platform = ${common.platform_wled_default} +platform_packages = ${common.platform_packages} +board_build.ldscript = ${common.ldscript_4m1m} +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags_esp8266} -D LEDPIN=12 -D IRPIN=-1 -D RLYPIN=2 + # ------------------------------------------------------------------------------ # custom board configurations # ------------------------------------------------------------------------------ diff --git a/tools/cdata.js b/tools/cdata.js index dd2bc7f2..201193f6 100644 --- a/tools/cdata.js +++ b/tools/cdata.js @@ -69,6 +69,8 @@ function writeHtmlGzipped(sourceFile, resultFile) { console.info("Reading " + sourceFile); new inliner(sourceFile, function (error, html) { console.info("Inlined " + html.length + " characters"); + html = filter(html, "html-minify-ui"); + console.info("Minified to " + html.length + " characters"); if (error) { console.warn(error); @@ -123,6 +125,16 @@ function filter(str, type) { continueOnParseError: false, removeComments: true, }); + } else if (type == "html-minify-ui") { + return MinifyHTML(str, { + collapseWhitespace: true, + conservativeCollapse: true, + maxLineLength: 80, + minifyCSS: true, + minifyJS: true, + continueOnParseError: false, + removeComments: true, + }); } else { console.warn("Unknown filter: " + type); return str; @@ -132,7 +144,7 @@ function filter(str, type) { function specToChunk(srcDir, s) { if (s.method == "plaintext") { const buf = fs.readFileSync(srcDir + "/" + s.file); - const str = buf.toString("ascii"); + const str = buf.toString("utf-8"); const chunk = ` // Autogenerated from ${srcDir}/${s.file}, do not edit!! const char ${s.name}[] PROGMEM = R"${s.prepend || ""}${filter(str, s.filter)}${ diff --git a/usermods/BME280_v2/README.md b/usermods/BME280_v2/README.md new file mode 100644 index 00000000..216ca630 --- /dev/null +++ b/usermods/BME280_v2/README.md @@ -0,0 +1,40 @@ +Hello! I have written a v2 usermod for the BME280/BMP280 sensor based on the [existing v1 usermod](https://github.com/Aircoookie/WLED/blob/master/usermods/Wemos_D1_mini%2BWemos32_mini_shield/usermod_bme280.cpp). It is not just a refactor, there are many changes which I made to fit my use case, and I hope they will fit the use cases of others as well! Most notably, this usermod is *just* for the BME280 and does not control a display like in the v1 usermod designed for the WeMos shield. + +- Requires libraries `BME280@~3.0.0` (by [finitespace](https://github.com/finitespace/BME280)) and `Wire`. Please add these under `lib_deps` in your `platform.ini` (or `platform_override.ini`). +- Data is published over MQTT so make sure you've enabled the MQTT sync interface. +- This usermod also writes to serial (GPIO1 on ESP8266). Please make sure nothing else listening on the serial TX pin of your board will get confused by log messages! + +To enable, compile with `USERMOD_BME280` defined (i.e. `platformio_override.ini`) +```ini +build_flags = + ${common.build_flags_esp8266} + -D USERMOD_BME280 +``` +or define `USERMOD_BME280` in `my_config.h` +```c++ +#define USERMOD_BME280 +``` + +Changes include: +- Adjustable measure intervals + - Temperature and pressure have separate intervals due to pressure not frequently changing at any constant altitude +- Adjustment of number of decimal places in published sensor values + - Separate adjustment for temperature, humidity and pressure values + - Values are rounded to the specified number of decimal places +- Pressure measured in units of hPa instead of Pa +- Calculation of heat index (apparent temperature) and dew point + - These, along with humidity measurements, are disabled if the sensor is a BMP280 +- 16x oversampling of sensor during measurement +- Values are only published if they are different from the previous value +- Values are published on startup (continually until the MQTT broker acknowledges a successful publication) + +Adjustments are made through preprocessor definitions at the start of the class definition. + +MQTT topics are as follows: +Measurement type | MQTT topic +--- | --- +Temperature | `/temperature` +Humidity | `/humidity` +Pressure | `/pressure` +Heat index | `/heat_index` +Dew point | `/dew_point` \ No newline at end of file diff --git a/usermods/BME280_v2/usermod_bme280.h b/usermods/BME280_v2/usermod_bme280.h new file mode 100644 index 00000000..80a31a4f --- /dev/null +++ b/usermods/BME280_v2/usermod_bme280.h @@ -0,0 +1,212 @@ +#pragma once + +#include "wled.h" +#include +#include +#include // BME280 sensor +#include // BME280 extended measurements + +class UsermodBME280 : public Usermod +{ +private: +// User-defined configuration +#define Celsius // Show temperature mesaurement in Celcius. Comment out for Fahrenheit +#define TemperatureDecimals 1 // Number of decimal places in published temperaure values +#define HumidityDecimals 0 // Number of decimal places in published humidity values +#define PressureDecimals 2 // Number of decimal places in published pressure values +#define TemperatureInterval 5 // Interval to measure temperature (and humidity, dew point if available) in seconds +#define PressureInterval 300 // Interval to measure pressure in seconds + +// Sanity checks +#if !defined(TemperatureDecimals) || TemperatureDecimals < 0 + #define TemperatureDecimals 0 +#endif +#if !defined(HumidityDecimals) || HumidityDecimals < 0 + #define HumidityDecimals 0 +#endif +#if !defined(PressureDecimals) || PressureDecimals < 0 + #define PressureDecimals 0 +#endif +#if !defined(TemperatureInterval) || TemperatureInterval < 0 + #define TemperatureInterval 1 +#endif +#if !defined(PressureInterval) || PressureInterval < 0 + #define PressureInterval TemperatureInterval +#endif + +#ifdef ARDUINO_ARCH_ESP32 // ESP32 boards + uint8_t SCL_PIN = 22; + uint8_t SDA_PIN = 21; +#else // ESP8266 boards + uint8_t SCL_PIN = 5; + uint8_t SDA_PIN = 4; + //uint8_t RST_PIN = 16; // Uncoment for Heltec WiFi-Kit-8 +#endif + + // BME280 sensor settings + BME280I2C::Settings settings{ + BME280::OSR_X16, // Temperature oversampling x16 + BME280::OSR_X16, // Humidity oversampling x16 + BME280::OSR_X16, // Pressure oversampling x16 + // Defaults + BME280::Mode_Forced, + BME280::StandbyTime_1000ms, + BME280::Filter_Off, + BME280::SpiEnable_False, + BME280I2C::I2CAddr_0x76 // I2C address. I2C specific. Default 0x76 + }; + + BME280I2C bme{settings}; + + uint8_t SensorType; + + // Measurement timers + long timer; + long lastTemperatureMeasure = 0; + long lastPressureMeasure = 0; + + // Current sensor values + float SensorTemperature; + float SensorHumidity; + float SensorHeatIndex; + float SensorDewPoint; + float SensorPressure; + // Track previous sensor values + float lastTemperature; + float lastHumidity; + float lastHeatIndex; + float lastDewPoint; + float lastPressure; + + // Store packet IDs of MQTT publications + uint16_t mqttTemperaturePub = 0; + uint16_t mqttPressurePub = 0; + + void UpdateBME280Data(int SensorType) + { + float _temperature, _humidity, _pressure; + #ifdef Celsius + BME280::TempUnit tempUnit(BME280::TempUnit_Celsius); + EnvironmentCalculations::TempUnit envTempUnit(EnvironmentCalculations::TempUnit_Celsius); + #else + BME280::TempUnit tempUnit(BME280::TempUnit_Fahrenheit); + EnvironmentCalculations::TempUnit envTempUnit(EnvironmentCalculations::TempUnit_Fahrenheit); + #endif + BME280::PresUnit presUnit(BME280::PresUnit_hPa); + + bme.read(_pressure, _temperature, _humidity, tempUnit, presUnit); + + SensorTemperature = _temperature; + SensorHumidity = _humidity; + SensorPressure = _pressure; + if (SensorType == 1) + { + SensorHeatIndex = EnvironmentCalculations::HeatIndex(_temperature, _humidity, envTempUnit); + SensorDewPoint = EnvironmentCalculations::DewPoint(_temperature, _humidity, envTempUnit); + } + } + +public: + void setup() + { + Wire.begin(SDA_PIN, SCL_PIN); + + if (!bme.begin()) + { + SensorType = 0; + Serial.println("Could not find BME280I2C sensor!"); + } + else + { + switch (bme.chipModel()) + { + case BME280::ChipModel_BME280: + SensorType = 1; + Serial.println("Found BME280 sensor! Success."); + break; + case BME280::ChipModel_BMP280: + SensorType = 2; + Serial.println("Found BMP280 sensor! No Humidity available."); + break; + default: + SensorType = 0; + Serial.println("Found UNKNOWN sensor! Error!"); + } + } + } + + void loop() + { + // BME280 sensor MQTT publishing + // Check if sensor present and MQTT Connected, otherwise it will crash the MCU + if (SensorType != 0 && mqtt != nullptr) + { + // Timer to fetch new temperature, humidity and pressure data at intervals + timer = millis(); + + if (timer - lastTemperatureMeasure >= TemperatureInterval * 1000 || mqttTemperaturePub == 0) + { + lastTemperatureMeasure = timer; + + UpdateBME280Data(SensorType); + + float Temperature = roundf(SensorTemperature * pow(10, TemperatureDecimals)) / pow(10, TemperatureDecimals); + float Humidity, HeatIndex, DewPoint; + + // If temperature has changed since last measure, create string populated with device topic + // from the UI and values read from sensor, then publish to broker + if (Temperature != lastTemperature) + { + String topic = String(mqttDeviceTopic) + "/temperature"; + mqttTemperaturePub = mqtt->publish(topic.c_str(), 0, false, String(Temperature, TemperatureDecimals).c_str()); + } + + lastTemperature = Temperature; // Update last sensor temperature for next loop + + if (SensorType == 1) // Only if sensor is a BME280 + { + Humidity = roundf(SensorHumidity * pow(10, HumidityDecimals)) / pow(10, HumidityDecimals); + HeatIndex = roundf(SensorHeatIndex * pow(10, TemperatureDecimals)) / pow(10, TemperatureDecimals); + DewPoint = roundf(SensorDewPoint * pow(10, TemperatureDecimals)) / pow(10, TemperatureDecimals); + + if (Humidity != lastHumidity) + { + String topic = String(mqttDeviceTopic) + "/humidity"; + mqtt->publish(topic.c_str(), 0, false, String(Humidity, HumidityDecimals).c_str()); + } + + if (HeatIndex != lastHeatIndex) + { + String topic = String(mqttDeviceTopic) + "/heat_index"; + mqtt->publish(topic.c_str(), 0, false, String(HeatIndex, TemperatureDecimals).c_str()); + } + + if (DewPoint != lastDewPoint) + { + String topic = String(mqttDeviceTopic) + "/dew_point"; + mqtt->publish(topic.c_str(), 0, false, String(DewPoint, TemperatureDecimals).c_str()); + } + + lastHumidity = Humidity; + lastHeatIndex = HeatIndex; + lastDewPoint = DewPoint; + } + } + + if (timer - lastPressureMeasure >= PressureInterval * 1000 || mqttPressurePub == 0) + { + lastPressureMeasure = timer; + + float Pressure = roundf(SensorPressure * pow(10, PressureDecimals)) / pow(10, PressureDecimals); + + if (Pressure != lastPressure) + { + String topic = String(mqttDeviceTopic) + "/pressure"; + mqttPressurePub = mqtt->publish(topic.c_str(), 0, true, String(Pressure, PressureDecimals).c_str()); + } + + lastPressure = Pressure; + } + } + } +}; \ No newline at end of file diff --git a/usermods/Fix_unreachable_netservices_v2/readme.md b/usermods/Fix_unreachable_netservices_v2/readme.md index f7d2aed6..f7b721dd 100644 --- a/usermods/Fix_unreachable_netservices_v2/readme.md +++ b/usermods/Fix_unreachable_netservices_v2/readme.md @@ -1,17 +1,32 @@ # Fix unreachable net services V2 +**Attention: This usermod compiles only for ESP8266** + This usermod-v2 modification performs a ping request to the local IP address every 60 seconds. By this procedure the net services of WLED remains accessible in some problematic WLAN environments. The modification works with static or DHCP IP address configuration. -**Webinterface**: The number of pings and reconnects is displayed on the info page in the web interface. - _Story:_ Unfortunately, with all ESP projects where a web server or other network services are running, I have the problem that after some time the web server is no longer accessible. Now I found out that the connection is at least reestablished when a ping request is executed by the device. With this modification, in the worst case, the network functions are not available for 60 seconds until the next ping request. +## Webinterface + +The number of pings and reconnects is displayed on the info page in the web interface. +The ping delay can be changed. Changes persist after a reboot. + +## JSON API + +The usermod supports the following state changes: + +| JSON key | Value range | Description | +|-------------|------------------|---------------------------------| +| PingDelayMs | 5000 to 18000000 | Deactivdate/activate the sensor | + + Changes also persist after a reboot. + ## Installation 1. Copy the file `usermod_Fix_unreachable_netservices.h` to the `wled00` directory. diff --git a/usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.h b/usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.h index 8ffc821e..cb2f1b0c 100644 --- a/usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.h +++ b/usermods/Fix_unreachable_netservices_v2/usermod_Fix_unreachable_netservices.h @@ -1,6 +1,14 @@ #pragma once #include "wled.h" +#if defined(ESP32) +#warning "Usermod FixUnreachableNetServices works only with ESP8266 builds" +class FixUnreachableNetServices : public Usermod +{ +}; +#endif + +#if defined(ESP8266) #include /* @@ -23,116 +31,138 @@ * 2. Register the usermod by adding #include "usermod_filename.h" in the top and registerUsermod(new MyUsermodClass()) in the bottom of usermods_list.cpp */ -class FixUnreachableNetServices : public Usermod { - private: - //Private class members. You can declare variables and functions only accessible to your usermod here - unsigned long m_lastTime = 0; +class FixUnreachableNetServices : public Usermod +{ +private: + //Private class members. You can declare variables and functions only accessible to your usermod here + unsigned long m_lastTime = 0; - // desclare required variables - const unsigned int PingDelayMs = 60000; - unsigned long m_connectedWiFi = 0; - ping_option m_pingOpt; - unsigned int m_pingCount = 0; + // declare required variables + unsigned long m_pingDelayMs = 60000; + unsigned long m_connectedWiFi = 0; + ping_option m_pingOpt; + unsigned int m_pingCount = 0; + bool m_updateConfig = false; - public: - //Functions called by WLED +public: + //Functions called by WLED - /* - * setup() is called once at boot. WiFi is not yet connected at this point. - * You can use it to initialize variables, sensors or similar. - */ - void setup() { - //Serial.println("Hello from my usermod!"); - } + /** + * setup() is called once at boot. WiFi is not yet connected at this point. + * You can use it to initialize variables, sensors or similar. + */ + void setup() + { + //Serial.println("Hello from my usermod!"); + } + /** + * connected() is called every time the WiFi is (re)connected + * Use it to initialize network interfaces + */ + void connected() + { + //Serial.println("Connected to WiFi!"); - /* - * connected() is called every time the WiFi is (re)connected - * Use it to initialize network interfaces - */ - void connected() { - //Serial.println("Connected to WiFi!"); + ++m_connectedWiFi; - ++m_connectedWiFi; - - // initialize ping_options structure - memset(&m_pingOpt, 0, sizeof(struct ping_option)); - m_pingOpt.count = 1; - m_pingOpt.ip = WiFi.localIP(); + // initialize ping_options structure + memset(&m_pingOpt, 0, sizeof(struct ping_option)); + m_pingOpt.count = 1; + m_pingOpt.ip = WiFi.localIP(); + } - } - - - /* - * loop() is called continuously. Here you can check for events, read sensors, etc. - * - * Tips: - * 1. You can use "if (WLED_CONNECTED)" to check for a successful network connection. - * Additionally, "if (WLED_MQTT_CONNECTED)" is available to check for a connection to an MQTT broker. - * - * 2. Try to avoid using the delay() function. NEVER use delays longer than 10 milliseconds. - * Instead, use a timer check as shown here. - */ - void loop() { - if (m_connectedWiFi > 0 && millis()-m_lastTime > PingDelayMs) - { - ping_start(&m_pingOpt); - m_lastTime = millis(); - ++m_pingCount; - } - } - - - /* - * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. - * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. - * Below it is shown how this could be used for e.g. a light sensor - */ - void addToJsonInfo(JsonObject& root) + /** + * loop + */ + void loop() + { + if (m_connectedWiFi > 0 && millis() - m_lastTime > m_pingDelayMs) { - //this code adds "u":{"⚡ Ping fix pings": m_pingCount} to the info object - JsonObject user = root["u"]; - if (user.isNull()) user = root.createNestedObject("u"); - - JsonArray infoArr = user.createNestedArray("⚡ Ping fix pings"); //name - infoArr.add(m_pingCount); //value - - //this code adds "u":{"⚡ Reconnects": m_connectedWiFi - 1} to the info object - infoArr = user.createNestedArray("⚡ Reconnects"); //name - infoArr.add(m_connectedWiFi - 1); //value + ping_start(&m_pingOpt); + m_lastTime = millis(); + ++m_pingCount; } - - - /* - * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ - void addToJsonState(JsonObject& root) + if (m_updateConfig) { - //root["user0"] = userVar0; + serializeConfig(); + m_updateConfig = false; } + } + /** + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + * Creating an "u" object allows you to add custom key/value pairs to the Info section of the WLED web UI. + * Below it is shown how this could be used for e.g. a light sensor + */ + void addToJsonInfo(JsonObject &root) + { + //this code adds "u":{"⚡ Ping fix pings": m_pingCount} to the info object + JsonObject user = root["u"]; + if (user.isNull()) + user = root.createNestedObject("u"); - /* - * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - */ - void readFromJsonState(JsonObject& root) + String uiDomString = "⚡ Ping fix pings\ +Delay sec"; + + JsonArray infoArr = user.createNestedArray(uiDomString); //name + infoArr.add(m_pingCount); //value + + //this code adds "u":{"⚡ Reconnects": m_connectedWiFi - 1} to the info object + infoArr = user.createNestedArray("⚡ Reconnects"); //name + infoArr.add(m_connectedWiFi - 1); //value + } + + /** + * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void addToJsonState(JsonObject &root) + { + root["PingDelay"] = (m_pingDelayMs/1000); + } + + /** + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void readFromJsonState(JsonObject &root) + { + if (root["PingDelay"] != nullptr) { - //userVar0 = root["user0"] | userVar0; //if "user0" key exists in JSON, update, else keep old value - //if (root["bri"] == 255) Serial.println(F("Don't burn down your garage!")); - } - - - /* - * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). - * This could be used in the future for the system to determine whether your usermod is installed. - */ - uint16_t getId() - { - return USERMOD_ID_FIXNETSERVICES; + m_pingDelayMs = (1000 * max(1UL, min(300UL, root["PingDelay"].as()))); + m_updateConfig = true; } + } - //More methods can be added in the future, this example will then be extended. - //Your usermod will remain compatible as it does not need to implement all methods from the Usermod base class! + /** + * provide the changeable values + */ + void addToConfig(JsonObject &root) + { + JsonObject top = root.createNestedObject("FixUnreachableNetServices"); + top["PingDelayMs"] = m_pingDelayMs; + } + + /** + * restore the changeable values + */ + void readFromConfig(JsonObject &root) + { + JsonObject top = root["FixUnreachableNetServices"]; + m_pingDelayMs = top["PingDelayMs"] | m_pingDelayMs; + m_pingDelayMs = max(5000UL, min(18000000UL, m_pingDelayMs)); + } + + /** + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() + { + return USERMOD_ID_FIXNETSERVICES; + } }; +#endif diff --git a/usermods/Fix_unreachable_webserver/readme.md b/usermods/Fix_unreachable_webserver/readme.md deleted file mode 100644 index 5ed17b87..00000000 --- a/usermods/Fix_unreachable_webserver/readme.md +++ /dev/null @@ -1,17 +0,0 @@ -# Fix unreachable Webserver - -This modification performs a ping request to the local IP address every 60 seconds. By this procedure the web server remains accessible in some problematic WLAN environments. - -The modification works with static or DHCP IP address configuration - -_Story:_ - -Unfortunately, with all ESP projects where a web server or other network services are running, I have the problem that after some time the web server is no longer accessible. Now I found out that the connection is at least reestablished when a ping request is executed by the device. - -With this modification, in the worst case, the network functions are not available for 60 seconds until the next ping request. - -## Installation - -Copy and replace the file `usermod.cpp` in wled00 directory. - - diff --git a/usermods/Fix_unreachable_webserver/usermod.cpp b/usermods/Fix_unreachable_webserver/usermod.cpp deleted file mode 100644 index f1957da2..00000000 --- a/usermods/Fix_unreachable_webserver/usermod.cpp +++ /dev/null @@ -1,43 +0,0 @@ -#include "wled.h" -/* - * This file allows you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality - * EEPROM bytes 2750+ are reserved for your custom use case. (if you extend #define EEPSIZE in const.h) - * bytes 2400+ are currently ununsed, but might be used for future wled features - */ - -#include - -const int PingDelayMs = 60000; -long lastCheckTime = 0; -bool connectedWiFi = false; -ping_option pingOpt; - -//Use userVar0 and userVar1 (API calls &U0=,&U1=, uint16_t) - -//gets called once at boot. Do all initialization that doesn't depend on network here -void userSetup() -{ - -} - - -//gets called every time WiFi is (re-)connected. Initialize own network interfaces here -void userConnected() -{ - connectedWiFi = true; - // initialize ping_options structure - memset(&pingOpt, 0, sizeof(struct ping_option)); - pingOpt.count = 1; - pingOpt.ip = WiFi.localIP(); -} - -//loop. You can use "if (WLED_CONNECTED)" to check for successful connection -void userLoop() -{ - if (connectedWiFi && millis()-lastCheckTime > PingDelayMs) - { - ping_start(&pingOpt); - lastCheckTime = millis(); - } -} diff --git a/usermods/PIR_sensor_switch/readme.md b/usermods/PIR_sensor_switch/readme.md index 79556db7..3d00b505 100644 --- a/usermods/PIR_sensor_switch/readme.md +++ b/usermods/PIR_sensor_switch/readme.md @@ -11,19 +11,21 @@ The LED strip is switched [using a relay](https://github.com/Aircoookie/WLED/wik The info page in the web interface shows the items below -- the state of the sensor. By clicking on the state the sensor can be deactivated/activated. -**I recommend to deactivate the sensor before installing an OTA update**. +- the state of the sensor. By clicking on the state the sensor can be deactivated/activated. Changes persist after a reboot. +**I recommend to deactivate the sensor before an OTA update and activate it again afterwards**. - the remaining time of the off timer. ## JSON API -The usermod supports the following state changes: +The usermod supports the following state changes: | JSON key | Value range | Description | |------------|-------------|---------------------------------| | PIRenabled | bool | Deactivdate/activate the sensor | | PIRoffSec | 60 to 43200 | Off timer seconds | + Changes also persist after a reboot. + ## Sensor connection My setup uses an HC-SR501 sensor, a HC-SR505 should also work. @@ -55,7 +57,7 @@ Example **usermods_list.cpp**: //#include "usermod_v2_example.h" //#include "usermod_temperature.h" //#include "usermod_v2_empty.h" -#include "usermod_PIR_sensor_switch.h" +#include "usermod_PIR_sensor_switch.h" void registerUsermods() { @@ -72,26 +74,36 @@ void registerUsermods() } ``` -## Usermod installation (advanced mode) +## API to enable/disable the PIR sensor from outside. For example from another usermod. -In this mode IR sensor will disable PIR when light ON by remote controller and enable PIR when light OFF. +The class provides the static method `PIRsensorSwitch* PIRsensorSwitch::GetInstance()` to get a pointer to the usermod object. -1. Copy the file `usermod_PIR_sensor_switch.h` to the `wled00` directory. -2. Register the usermod by adding `#include "usermod_PIR_sensor_switch.h"` in the top and `registerUsermod(new PIRsensorSwitch());` in the bottom of `usermods_list.cpp`. -3. Add to the line 237, on `wled.h` in the `wled00` directory: +To query or change the PIR sensor state the methods `bool PIRsensorEnabled()` and `void EnablePIRsensor(bool enable)` are available. - `WLED_GLOBAL bool m_PIRenabled _INIT(true); // enable PIR sensor` - -4. On `ir.cpp` in the `wled00` directory, add to the IR controller's mapping (beyond line 200): - -- To the off button: - `m_PIRenabled = true;` - -- To the on button: - `m_PIRenabled = false;` - -5. Edit line 40, on `usermod_PIR_sensor_switch.h` in the `wled00` directory: - - `\\bool m_PIRenabled = true;` +### There are two options to get access to the usermod instance: + +1. Include `usermod_PIR_sensor_switch.h` **before** you include the other usermod in `usermods_list.cpp' + +or + +2. Use `#include "usermod_PIR_sensor_switch.h"` at the top of the `usermod.h` where you need it. + +**Example usermod.h :** +```cpp +#include "wled.h" + +#include "usermod_PIR_sensor_switch.h" + +class MyUsermod : public Usermod { + //... + + void togglePIRSensor() { + if (PIRsensorSwitch::GetInstance() != nullptr) { + PIRsensorSwitch::GetInstance()->EnablePIRsensor(!PIRsensorSwitch::GetInstance()->PIRsensorEnabled()); + } + } + //... +}; +``` Have fun - @gegu diff --git a/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h b/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h index e87147a1..421528bf 100644 --- a/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h +++ b/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h @@ -24,228 +24,343 @@ * 2. Register the usermod by adding #include "usermod_filename.h" in the top and registerUsermod(new MyUsermodClass()) in the bottom of usermods_list.cpp */ -class PIRsensorSwitch : public Usermod { - private: - // PIR sensor pin - const uint8_t PIRsensorPin = 13; // D7 on D1 mini - // notification mode for colorUpdated() - const byte NotifyUpdateMode = NOTIFIER_CALL_MODE_NO_NOTIFY; // NOTIFIER_CALL_MODE_DIRECT_CHANGE - // delay before switch off after the sensor state goes LOW - uint32_t m_switchOffDelay = 600000; - // off timer start time - uint32_t m_offTimerStart = 0; - // current PIR sensor pin state - byte m_PIRsensorPinState = LOW; - // PIR sensor enabled - ISR attached - bool m_PIRenabled = true; +class PIRsensorSwitch : public Usermod +{ +public: + /** + * constructor + */ + PIRsensorSwitch() + { + // set static instance pointer + PIRsensorSwitchInstance(this); + } + /** + * desctructor + */ + ~PIRsensorSwitch() + { + PIRsensorSwitchInstance(nullptr, true); + ; + } - /* - * return or change if new PIR sensor state is available - */ - static volatile bool newPIRsensorState(bool changeState = false, bool newState = false) { - static volatile bool s_PIRsensorState = false; - if (changeState) { - s_PIRsensorState = newState; + /** + * return the instance pointer of the class + */ + static PIRsensorSwitch *GetInstance() { return PIRsensorSwitchInstance(); } + + /** + * Enable/Disable the PIR sensor + */ + void EnablePIRsensor(bool enable) { m_PIRenabled = enable; } + /** + * Get PIR sensor enabled/disabled state + */ + bool PIRsensorEnabled() { return m_PIRenabled; } + +private: + // PIR sensor pin + const uint8_t PIRsensorPin = 13; // D7 on D1 mini + // notification mode for colorUpdated() + const byte NotifyUpdateMode = NOTIFIER_CALL_MODE_NO_NOTIFY; // NOTIFIER_CALL_MODE_DIRECT_CHANGE + // delay before switch off after the sensor state goes LOW + uint32_t m_switchOffDelay = 600000; + // off timer start time + uint32_t m_offTimerStart = 0; + // current PIR sensor pin state + byte m_PIRsensorPinState = LOW; + // PIR sensor enabled - ISR attached + bool m_PIRenabled = true; + // state if serializeConfig() should be called + bool m_updateConfig = false; + + /** + * return or change if new PIR sensor state is available + */ + static volatile bool newPIRsensorState(bool changeState = false, bool newState = false); + + /** + * PIR sensor state has changed + */ + static void IRAM_ATTR ISR_PIRstateChange(); + + /** + * Set/get instance pointer + */ + static PIRsensorSwitch *PIRsensorSwitchInstance(PIRsensorSwitch *pInstance = nullptr, bool bRemoveInstance = false); + + /** + * switch strip on/off + */ + void switchStrip(bool switchOn) + { + if (switchOn && bri == 0) + { + bri = briLast; + colorUpdated(NotifyUpdateMode); + } + else if (!switchOn && bri != 0) + { + briLast = bri; + bri = 0; + colorUpdated(NotifyUpdateMode); + } + } + + /** + * Read and update PIR sensor state. + * Initilize/reset switch off timer + */ + bool updatePIRsensorState() + { + if (newPIRsensorState()) + { + m_PIRsensorPinState = digitalRead(PIRsensorPin); + + if (m_PIRsensorPinState == HIGH) + { + m_offTimerStart = 0; + switchStrip(true); } - return s_PIRsensorState; - } - - /* - * PIR sensor state has changed - */ - static void IRAM_ATTR ISR_PIRstateChange() { - newPIRsensorState(true, true); - } - - /* - * switch strip on/off - */ - void switchStrip(bool switchOn) { - if (switchOn && bri == 0) { - bri = briLast; - colorUpdated(NotifyUpdateMode); - } - else if (!switchOn && bri != 0) { - briLast = bri; - bri = 0; - colorUpdated(NotifyUpdateMode); + else if (bri != 0) + { + // start switch off timer + m_offTimerStart = millis(); } + newPIRsensorState(true, false); + return true; } + return false; + } - /* - * Read and update PIR sensor state. - * Initilize/reset switch off timer - */ - bool updatePIRsensorState() { - if (newPIRsensorState()) { - m_PIRsensorPinState = digitalRead(PIRsensorPin); - - if (m_PIRsensorPinState == HIGH) { - m_offTimerStart = 0; - switchStrip(true); - } - else if (bri != 0) { - // start switch off timer - m_offTimerStart = millis(); - } - newPIRsensorState(true, false); - return true; + /** + * switch off the strip if the delay has elapsed + */ + bool handleOffTimer() + { + if (m_offTimerStart > 0 && millis() - m_offTimerStart > m_switchOffDelay) + { + if (m_PIRenabled == true) + { + switchStrip(false); } - return false; + m_offTimerStart = 0; + return true; } + return false; + } - /* - * switch off the strip if the delay has elapsed - */ - bool handleOffTimer() { - if (m_offTimerStart > 0 && millis() - m_offTimerStart > m_switchOffDelay) { - if (m_PIRenabled == true){ - switchStrip(false); - } - m_offTimerStart = 0; - return true; - } - return false; - } +public: + //Functions called by WLED - public: - //Functions called by WLED - - /* - * setup() is called once at boot. WiFi is not yet connected at this point. - * You can use it to initialize variables, sensors or similar. - */ - void setup() { - // PIR Sensor mode INPUT_PULLUP - pinMode(PIRsensorPin, INPUT_PULLUP); + /** + * setup() is called once at boot. WiFi is not yet connected at this point. + * You can use it to initialize variables, sensors or similar. + */ + void setup() + { + // PIR Sensor mode INPUT_PULLUP + pinMode(PIRsensorPin, INPUT_PULLUP); + if (m_PIRenabled) + { // assign interrupt function and set CHANGE mode attachInterrupt(digitalPinToInterrupt(PIRsensorPin), ISR_PIRstateChange, CHANGE); } + } + /** + * connected() is called every time the WiFi is (re)connected + * Use it to initialize network interfaces + */ + void connected() + { + } - /* - * connected() is called every time the WiFi is (re)connected - * Use it to initialize network interfaces - */ - void connected() { - - } - - - /* - * loop() is called continuously. Here you can check for events, read sensors, etc. - */ - void loop() { - if (!updatePIRsensorState()) { - handleOffTimer(); - } - } - - /* - * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. - * - * Add PIR sensor state and switch off timer duration to jsoninfo - */ - void addToJsonInfo(JsonObject& root) + /** + * loop() is called continuously. Here you can check for events, read sensors, etc. + */ + void loop() + { + if (!updatePIRsensorState()) { - //this code adds "u":{"⏲ PIR sensor state":uiDomString} to the info object - // the value contains a button to toggle the sensor enabled/disabled - JsonObject user = root["u"]; - if (user.isNull()) user = root.createNestedObject("u"); - - JsonArray infoArr = user.createNestedArray("⏲ PIR sensor state"); //name - String uiDomString = ""; - infoArr.add(uiDomString); //value - - //this code adds "u":{"⏲ switch off timer":uiDomString} to the info object - infoArr = user.createNestedArray("⏲ switch off timer"); //name - - // off timer - if (m_offTimerStart > 0) { - uiDomString = ""; - unsigned int offSeconds = (m_switchOffDelay - (millis() - m_offTimerStart)) / 1000; - if (offSeconds >= 3600) { - uiDomString += (offSeconds / 3600); - uiDomString += " hours "; - offSeconds %= 3600; - } - if (offSeconds >= 60) { - uiDomString += (offSeconds / 60); - offSeconds %= 60; - } else if (uiDomString.length() > 0){ - uiDomString += 0; - } - if (uiDomString.length() > 0){ - uiDomString += " min "; - } - uiDomString += (offSeconds); - infoArr.add(uiDomString + " sec"); - } else { - infoArr.add("inactive"); + handleOffTimer(); + if (m_updateConfig) + { + serializeConfig(); + m_updateConfig = false; } } + } + /** + * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. + * + * Add PIR sensor state and switch off timer duration to jsoninfo + */ + void addToJsonInfo(JsonObject &root) + { + //this code adds "u":{"⏲ PIR sensor state":uiDomString} to the info object + // the value contains a button to toggle the sensor enabled/disabled + JsonObject user = root["u"]; + if (user.isNull()) + user = root.createNestedObject("u"); - /* - * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). - * Values in the state object may be modified by connected clients - * Add "PIRenabled" to json state. This can be used to disable/enable the sensor. - * Add "PIRoffSec" to json state. This can be used to adjust milliseconds . - */ - void addToJsonState(JsonObject& root) + JsonArray infoArr = user.createNestedArray("⏲ PIR sensor state"); //name + String uiDomString = ""; + infoArr.add(uiDomString); //value + + //this code adds "u":{"⏲ switch off timer":uiDomString} to the info object + uiDomString = "⏲ switch off timer\ +after min"; + infoArr = user.createNestedArray(uiDomString); //name + + // off timer + if (m_offTimerStart > 0) { - return USERMOD_ID_PIRSWITCH; + uiDomString = ""; + unsigned int offSeconds = (m_switchOffDelay - (millis() - m_offTimerStart)) / 1000; + if (offSeconds >= 3600) + { + uiDomString += (offSeconds / 3600); + uiDomString += " hours "; + offSeconds %= 3600; + } + if (offSeconds >= 60) + { + uiDomString += (offSeconds / 60); + offSeconds %= 60; + } + else if (uiDomString.length() > 0) + { + uiDomString += 0; + } + if (uiDomString.length() > 0) + { + uiDomString += " min "; + } + uiDomString += (offSeconds); + infoArr.add(uiDomString + " sec"); + } + else + { + infoArr.add("inactive"); + } + } + + /** + * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + * Add "PIRenabled" to json state. This can be used to disable/enable the sensor. + * Add "PIRoffSec" to json state. This can be used to adjust milliseconds. + */ + void addToJsonState(JsonObject &root) + { + root["PIRenabled"] = m_PIRenabled; + root["PIRoffSec"] = (m_switchOffDelay / 1000); + } + + /** + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + * Read "PIRenabled" from json state and switch enable/disable the PIR sensor. + * Read "PIRoffSec" from json state and adjust milliseconds. + */ + void readFromJsonState(JsonObject &root) + { + if (root["PIRoffSec"] != nullptr) + { + m_switchOffDelay = (1000 * max(60UL, min(43200UL, root["PIRoffSec"].as()))); + m_updateConfig = true; } - //More methods can be added in the future, this example will then be extended. - //Your usermod will remain compatible as it does not need to implement all methods from the Usermod base class! + if (root["PIRenabled"] != nullptr) + { + if (root["PIRenabled"] && !m_PIRenabled) + { + attachInterrupt(digitalPinToInterrupt(PIRsensorPin), ISR_PIRstateChange, CHANGE); + newPIRsensorState(true, true); + } + else if (m_PIRenabled) + { + detachInterrupt(PIRsensorPin); + } + m_PIRenabled = root["PIRenabled"]; + m_updateConfig = true; + } + } + + /** + * provide the changeable values + */ + void addToConfig(JsonObject &root) + { + JsonObject top = root.createNestedObject("PIRsensorSwitch"); + top["PIRenabled"] = m_PIRenabled; + top["PIRoffSec"] = m_switchOffDelay; + } + + /** + * restore the changeable values + */ + void readFromConfig(JsonObject &root) + { + JsonObject top = root["PIRsensorSwitch"]; + m_PIRenabled = (top["PIRenabled"] != nullptr ? top["PIRenabled"] : true); + m_switchOffDelay = top["PIRoffSec"] | m_switchOffDelay; + } + + /** + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() + { + return USERMOD_ID_PIRSWITCH; + } }; + +////////////////////////////////////////////////////// +// PIRsensorSwitch static method implementations + +volatile bool PIRsensorSwitch::newPIRsensorState(bool changeState, bool newState) +{ + static volatile bool s_PIRsensorState = false; + if (changeState) + { + s_PIRsensorState = newState; + } + return s_PIRsensorState; +} + +void IRAM_ATTR PIRsensorSwitch::ISR_PIRstateChange() +{ + newPIRsensorState(true, true); +} + +PIRsensorSwitch *PIRsensorSwitch::PIRsensorSwitchInstance(PIRsensorSwitch *pInstance, bool bRemoveInstance) +{ + static PIRsensorSwitch *s_pPIRsensorSwitch = nullptr; + if (pInstance != nullptr || bRemoveInstance) + { + s_pPIRsensorSwitch = pInstance; + } + return s_pPIRsensorSwitch; +} diff --git a/usermods/buzzer/usermod_v2_buzzer.h b/usermods/buzzer/usermod_v2_buzzer.h new file mode 100644 index 00000000..ebd8dcb1 --- /dev/null +++ b/usermods/buzzer/usermod_v2_buzzer.h @@ -0,0 +1,81 @@ +#pragma once + +#include "wled.h" +#include "Arduino.h" + +#include + +#define USERMOD_ID_BUZZER 900 +#ifndef USERMOD_BUZZER_PIN +#define USERMOD_BUZZER_PIN GPIO_NUM_32 +#endif + +/* + * Usermods allow you to add own functionality to WLED more easily + * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * + * Using a usermod: + * 1. Copy the usermod into the sketch folder (same folder as wled00.ino) + * 2. Register the usermod by adding #include "usermod_filename.h" in the top and registerUsermod(new MyUsermodClass()) in the bottom of usermods_list.cpp + */ + +class BuzzerUsermod : public Usermod { + private: + unsigned long lastTime_ = 0; + unsigned long delay_ = 0; + std::deque> sequence_ {}; + public: + /* + * setup() is called once at boot. WiFi is not yet connected at this point. + * You can use it to initialize variables, sensors or similar. + */ + void setup() { + // Setup the pin, and default to LOW + pinMode(USERMOD_BUZZER_PIN, OUTPUT); + digitalWrite(USERMOD_BUZZER_PIN, LOW); + + // Beep on startup + sequence_.push_back({ HIGH, 50 }); + sequence_.push_back({ LOW, 0 }); + } + + + /* + * connected() is called every time the WiFi is (re)connected + * Use it to initialize network interfaces + */ + void connected() { + // Double beep on WiFi + sequence_.push_back({ LOW, 100 }); + sequence_.push_back({ HIGH, 50 }); + sequence_.push_back({ LOW, 30 }); + sequence_.push_back({ HIGH, 50 }); + sequence_.push_back({ LOW, 0 }); + } + + /* + * loop() is called continuously. Here you can check for events, read sensors, etc. + */ + void loop() { + if (sequence_.size() < 1) return; // Wait until there is a sequence + if (millis() - lastTime_ <= delay_) return; // Wait until delay has elapsed + + auto event = sequence_.front(); + sequence_.pop_front(); + + digitalWrite(USERMOD_BUZZER_PIN, event.first); + delay_ = event.second; + + lastTime_ = millis(); + } + + + /* + * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() + { + return USERMOD_ID_BUZZER; + } +}; \ No newline at end of file diff --git a/usermods/esp32_multistrip/NpbWrapper.h b/usermods/esp32_multistrip/NpbWrapper.h index e468e46e..84cf8ac0 100644 --- a/usermods/esp32_multistrip/NpbWrapper.h +++ b/usermods/esp32_multistrip/NpbWrapper.h @@ -149,7 +149,6 @@ public: void Show() { - byte b; switch (_type) { case NeoPixelType_Grb: @@ -191,6 +190,51 @@ public: } } + bool CanShow() + { + bool canShow = true; + switch (_type) + { + case NeoPixelType_Grb: + { + for (uint8_t idx = 0; idx < numStrips; idx++) + { + switch (idx) + { + case 0: canShow &= pGrb0->CanShow(); break; + case 1: canShow &= pGrb1->CanShow(); break; + case 2: canShow &= pGrb2->CanShow(); break; + case 3: canShow &= pGrb3->CanShow(); break; + case 4: canShow &= pGrb4->CanShow(); break; + case 5: canShow &= pGrb5->CanShow(); break; + case 6: canShow &= pGrb6->CanShow(); break; + case 7: canShow &= pGrb7->CanShow(); break; + } + } + break; + } + case NeoPixelType_Grbw: + { + for (uint8_t idx = 0; idx < numStrips; idx++) + { + switch (idx) + { + case 0: canShow &= pGrbw0->CanShow(); break; + case 1: canShow &= pGrbw1->CanShow(); break; + case 2: canShow &= pGrbw2->CanShow(); break; + case 3: canShow &= pGrbw3->CanShow(); break; + case 4: canShow &= pGrbw4->CanShow(); break; + case 5: canShow &= pGrbw5->CanShow(); break; + case 6: canShow &= pGrbw6->CanShow(); break; + case 7: canShow &= pGrbw7->CanShow(); break; + } + } + break; + } + } + return canShow; + } + void SetPixelColorRaw(uint16_t indexPixel, RgbwColor c) { // figure out which strip this pixel index is on diff --git a/wled00/FX.cpp b/wled00/FX.cpp index cc96987a..54849dcc 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -25,6 +25,7 @@ */ #include "FX.h" +#include "tv_colors.h" #define IBN 5100 #define PALETTE_SOLID_WRAP (paletteBlend == 1 || paletteBlend == 3) @@ -233,9 +234,9 @@ uint16_t WS2812FX::mode_random_color(void) { /* * Lights every LED in a random color. Changes all LED at the same time -// * to new random colors. + * to new random colors. */ -uint16_t WS2812FX::mode_dynamic(void) { +uint16_t WS2812FX::dynamic(boolean smooth=false) { if (!SEGENV.allocateData(SEGLEN)) return mode_static(); //allocation failed if(SEGENV.call == 0) { @@ -252,12 +253,31 @@ uint16_t WS2812FX::mode_dynamic(void) { SEGENV.step = it; } - for (uint16_t i = 0; i < SEGLEN; i++) { - setPixelColor(i, color_wheel(SEGENV.data[i])); - } + if (smooth) { + for (uint16_t i = 0; i < SEGLEN; i++) { + blendPixelColor(i, color_wheel(SEGENV.data[i]),16); + } + } else { + for (uint16_t i = 0; i < SEGLEN; i++) { + setPixelColor(i, color_wheel(SEGENV.data[i])); + } + } return FRAMETIME; } +/* + * Original effect "Dynamic" + */ +uint16_t WS2812FX::mode_dynamic(void) { + return dynamic(false); +} + +/* + * effect "Dynamic" with smoth color-fading + */ +uint16_t WS2812FX::mode_dynamic_smooth(void) { + return dynamic(true); + } /* * Does the "standby-breathing" of well known i-Devices. @@ -990,6 +1010,12 @@ uint16_t WS2812FX::mode_merry_christmas(void) { return running(RED, GREEN); } +/* + * Alternating red/white pixels running. + */ +uint16_t WS2812FX::mode_candy_cane(void) { + return running(RED, WHITE); +} /* * Alternating orange/purple pixels running. @@ -1754,19 +1780,22 @@ uint16_t WS2812FX::mode_fire_2012() if (it != SEGENV.step) { + uint8_t ignition = max(7,SEGLEN/10); // ignition area: 10% of segment length or minimum 7 pixels + // Step 1. Cool down every cell a little for (uint16_t i = 0; i < SEGLEN; i++) { - SEGENV.data[i] = qsub8(heat[i], random8(0, (((20 + SEGMENT.speed /3) * 10) / SEGLEN) + 2)); + uint8_t temp = qsub8(heat[i], random8(0, (((20 + SEGMENT.speed /3) * 10) / SEGLEN) + 2)); + heat[i] = (temp==0 && i 1; k--) { - heat[k] = (heat[k - 1] + heat[k - 2] + heat[k - 2] ) / 3; + heat[k] = (heat[k - 1] + (heat[k - 2]<<1) ) / 3; // heat[k-2] multiplied by 2 } // Step 3. Randomly ignite new 'sparks' of heat near the bottom if (random8() <= SEGMENT.intensity) { - uint8_t y = random8(7); + uint8_t y = random8(ignition); if (y < SEGLEN) heat[y] = qadd8(heat[y], random8(160,255)); } SEGENV.step = it; @@ -3727,3 +3756,117 @@ uint16_t WS2812FX::mode_washing_machine(void) { return FRAMETIME; } + +/* + Blends random colors across palette + Modified, originally by Mark Kriegsman https://gist.github.com/kriegsman/1f7ccbbfa492a73c015e +*/ +uint16_t WS2812FX::mode_blends(void) { + uint16_t dataSize = sizeof(uint32_t) * SEGLEN; + if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed + uint32_t* pixels = reinterpret_cast(SEGENV.data); + uint8_t blendSpeed = map(SEGMENT.intensity, 0, UINT8_MAX, 10, 128); + uint8_t shift = (now * ((SEGMENT.speed >> 3) +1)) >> 8; + + for (int i = 0; i < SEGLEN; i++) { + pixels[i] = color_blend(pixels[i], color_from_palette(shift + quadwave8((i + 1) * 16), false, PALETTE_SOLID_WRAP, 255), blendSpeed); + setPixelColor(i, pixels[i]); + shift += 3; + } + + return FRAMETIME; +} + +#ifndef WLED_DISABLE_FX_HIGH_FLASH_USE +typedef struct TvSim { + uint32_t totalTime = 0; + uint32_t fadeTime = 0; + uint32_t startTime = 0; + uint32_t elapsed = 0; + uint32_t pixelNum = 0; + uint16_t pr = 0; // Prev R, G, B + uint16_t pg = 0; + uint16_t pb = 0; +} tvSim; + +#define numTVPixels (sizeof(tv_colors) / 2) // 2 bytes per Pixel (5/6/5) +#endif + +/* + TV Simulator + Modified and adapted to WLED by Def3nder, based on "Fake TV Light for Engineers" by Phillip Burgess https://learn.adafruit.com/fake-tv-light-for-engineers/arduino-sketch +*/ +uint16_t WS2812FX::mode_tv_simulator(void) { + #ifdef WLED_DISABLE_FX_HIGH_FLASH_USE + return mode_static(); + #else + uint16_t nr, ng, nb, r, g, b, i; + uint8_t hi, lo, r8, g8, b8; + + if (!SEGENV.allocateData(sizeof(tvSim))) return mode_static(); //allocation failed + TvSim* tvSimulator = reinterpret_cast(SEGENV.data); + + // initialize start of the TV-Colors + if (SEGENV.call == 0) { + tvSimulator->pixelNum = ((uint8_t)random(18)) * numTVPixels / 18; // Begin at random movie (18 in total) + } + + // Read next 16-bit (5/6/5) color + hi = pgm_read_byte(&tv_colors[tvSimulator->pixelNum * 2 ]); + lo = pgm_read_byte(&tv_colors[tvSimulator->pixelNum * 2 + 1]); + + // Expand to 24-bit (8/8/8) + r8 = (hi & 0xF8) | (hi >> 5); + g8 = ((hi << 5) & 0xff) | ((lo & 0xE0) >> 3) | ((hi & 0x06) >> 1); + b8 = ((lo << 3) & 0xff) | ((lo & 0x1F) >> 2); + + // Apply gamma correction, further expand to 16/16/16 + nr = (uint8_t)gamma8(r8) * 257; // New R/G/B + ng = (uint8_t)gamma8(g8) * 257; + nb = (uint8_t)gamma8(b8) * 257; + + if (SEGENV.aux0 == 0) { // initialize next iteration + SEGENV.aux0 = 1; + + // increase color-index for next loop + tvSimulator->pixelNum++; + if (tvSimulator->pixelNum >= numTVPixels) tvSimulator->pixelNum = 0; + + // randomize total duration and fade duration for the actual color + tvSimulator->totalTime = random(250, 2500); // Semi-random pixel-to-pixel time + tvSimulator->fadeTime = random(0, tvSimulator->totalTime); // Pixel-to-pixel transition time + if (random(10) < 3) tvSimulator->fadeTime = 0; // Force scene cut 30% of time + + tvSimulator->startTime = millis(); + } // end of initialization + + // how much time is elapsed ? + tvSimulator->elapsed = millis() - tvSimulator->startTime; + + // fade from prev volor to next color + if (tvSimulator->elapsed < tvSimulator->fadeTime) { + r = map(tvSimulator->elapsed, 0, tvSimulator->fadeTime, tvSimulator->pr, nr); + g = map(tvSimulator->elapsed, 0, tvSimulator->fadeTime, tvSimulator->pg, ng); + b = map(tvSimulator->elapsed, 0, tvSimulator->fadeTime, tvSimulator->pb, nb); + } else { // Avoid divide-by-zero in map() + r = nr; + g = ng; + b = nb; + } + + // set strip color + for (i = 0; i < SEGLEN; i++) { + setPixelColor(i, r >> 8, g >> 8, b >> 8); // Quantize to 8-bit + } + + // if total duration has passed, remember last color and restart the loop + if ( tvSimulator->elapsed >= tvSimulator->totalTime) { + tvSimulator->pr = nr; // Prev RGB = new RGB + tvSimulator->pg = ng; + tvSimulator->pb = nb; + SEGENV.aux0 = 0; + } + + return FRAMETIME; + #endif +} diff --git a/wled00/FX.h b/wled00/FX.h index ae9b1811..f368e567 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -52,6 +52,9 @@ #define MAX(a,b) ((a)>(b)?(a):(b)) #endif +/* Disable effects with high flash memory usage (currently TV simulator) - saves 18.5kB */ +//#define WLED_DISABLE_FX_HIGH_FLASH_USE + /* Not used in all effects yet */ #define WLED_FPS 42 #define FRAMETIME (1000/WLED_FPS) @@ -116,7 +119,8 @@ #define IS_REVERSE ((SEGMENT.options & REVERSE ) == REVERSE ) #define IS_SELECTED ((SEGMENT.options & SELECTED ) == SELECTED ) -#define MODE_COUNT 114 + +#define MODE_COUNT 118 #define FX_MODE_STATIC 0 #define FX_MODE_BLINK 1 @@ -232,6 +236,10 @@ #define FX_MODE_CHUNCHUN 111 #define FX_MODE_DANCING_SHADOWS 112 #define FX_MODE_WASHING_MACHINE 113 +#define FX_MODE_CANDY_CANE 114 +#define FX_MODE_BLENDS 115 +#define FX_MODE_TV_SIMULATOR 116 +#define FX_MODE_DYNAMIC_SMOOTH 117 class WS2812FX { typedef uint16_t (WS2812FX::*mode_ptr)(void); @@ -316,9 +324,31 @@ class WS2812FX { WS2812FX::_usedSegmentData -= _dataLen; _dataLen = 0; } - void reset(){next_time = 0; step = 0; call = 0; aux0 = 0; aux1 = 0; deallocateData();} + + /** + * If reset of this segment was request, clears runtime + * settings of this segment. + * Must not be called while an effect mode function is running + * because it could access the data buffer and this method + * may free that data buffer. + */ + void resetIfRequired() { + if (_requiresReset) { + next_time = 0; step = 0; call = 0; aux0 = 0; aux1 = 0; + deallocateData(); + _requiresReset = false; + } + } + + /** + * Flags that before the next effect is calculated, + * the internal segment state should be reset. + * Call resetIfRequired before calling the next effect function. + */ + void reset() { _requiresReset = true; } private: uint16_t _dataLen = 0; + bool _requiresReset = false; } segment_runtime; WS2812FX() { @@ -437,6 +467,10 @@ class WS2812FX { _mode[FX_MODE_CHUNCHUN] = &WS2812FX::mode_chunchun; _mode[FX_MODE_DANCING_SHADOWS] = &WS2812FX::mode_dancing_shadows; _mode[FX_MODE_WASHING_MACHINE] = &WS2812FX::mode_washing_machine; + _mode[FX_MODE_CANDY_CANE] = &WS2812FX::mode_candy_cane; + _mode[FX_MODE_BLENDS] = &WS2812FX::mode_blends; + _mode[FX_MODE_TV_SIMULATOR] = &WS2812FX::mode_tv_simulator; + _mode[FX_MODE_DYNAMIC_SMOOTH] = &WS2812FX::mode_dynamic_smooth; _brightness = DEFAULT_BRIGHTNESS; currentPalette = CRGBPalette16(CRGB::Black); @@ -478,7 +512,9 @@ class WS2812FX { gammaCorrectCol = true, applyToAllSelected = true, segmentsAreIdentical(Segment* a, Segment* b), - setEffectConfig(uint8_t m, uint8_t s, uint8_t i, uint8_t p); + setEffectConfig(uint8_t m, uint8_t s, uint8_t i, uint8_t p), + // return true if the strip is being sent pixel updates + isUpdating(void); uint8_t mainSegment = 0, @@ -643,7 +679,11 @@ class WS2812FX { mode_flow(void), mode_chunchun(void), mode_dancing_shadows(void), - mode_washing_machine(void); + mode_washing_machine(void), + mode_candy_cane(void), + mode_blends(void), + mode_tv_simulator(void), + mode_dynamic_smooth(void); private: NeoPixelWrapper *bus; @@ -676,6 +716,7 @@ class WS2812FX { blink(uint32_t, uint32_t, bool strobe, bool), candle(bool), color_wipe(bool, bool), + dynamic(bool), scan(bool), theater_chase(uint32_t, uint32_t, bool), running_base(bool), @@ -731,7 +772,7 @@ const char JSON_mode_names[] PROGMEM = R"=====([ "Twinklefox","Twinklecat","Halloween Eyes","Solid Pattern","Solid Pattern Tri","Spots","Spots Fade","Glitter","Candle","Fireworks Starburst", "Fireworks 1D","Bouncing Balls","Sinelon","Sinelon Dual","Sinelon Rainbow","Popcorn","Drip","Plasma","Percent","Ripple Rainbow", "Heartbeat","Pacifica","Candle Multi", "Solid Glitter","Sunrise","Phased","Twinkleup","Noise Pal", "Sine","Phased Noise", -"Flow","Chunchun","Dancing Shadows","Washing Machine" +"Flow","Chunchun","Dancing Shadows","Washing Machine","Candy Cane","Blends","TV Simulator","Dynamic Smooth" ])====="; diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 4418cfb7..4d66a67a 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -44,6 +44,10 @@ const uint16_t customMappingTable[] = { const uint16_t customMappingSize = sizeof(customMappingTable)/sizeof(uint16_t); //30 in example #endif +#ifndef PWM_INDEX +#define PWM_INDEX 0 +#endif + void WS2812FX::init(bool supportWhite, uint16_t countPixels, bool skipFirst) { if (supportWhite == _useRgbw && countPixels == _length && _skipFirstMode == skipFirst) return; @@ -76,6 +80,11 @@ void WS2812FX::service() { for(uint8_t i=0; i < MAX_NUM_SEGMENTS; i++) { _segment_index = i; + + // reset the segment runtime data if needed, called before isActive to ensure deleted + // segment's buffers are cleared + SEGENV.resetIfRequired(); + if (SEGMENT.isActive()) { if(nowUp > SEGENV.next_time || _triggered || (doShow && SEGMENT.mode == 0)) //last is temporary @@ -218,8 +227,11 @@ void WS2812FX::setPixelColor(uint16_t i, byte r, byte g, byte b, byte w) //you can set it to 0 if the ESP is powered by USB and the LEDs by external void WS2812FX::show(void) { - if (_callback) _callback(); - + + // avoid race condition, caputre _callback value + show_callback callback = _callback; + if (callback) callback(); + //power limit calculation //each LED can draw up 195075 "power units" (approx. 53mA) //one PU is the power it takes to have 1 channel 1 step brighter per brightness step @@ -291,10 +303,24 @@ void WS2812FX::show(void) { bus->SetBrightness(_brightness); } + // some buses send asynchronously and this method will return before + // all of the data has been sent. + // See https://github.com/Makuna/NeoPixelBus/wiki/ESP32-NeoMethods#neoesp32rmt-methods bus->Show(); _lastShow = millis(); } +/** + * Returns a true value if any of the strips are still being updated. + * On some hardware (ESP32), strip updates are done asynchronously. + */ +bool WS2812FX::isUpdating() { + return !bus->CanShow(); +} + +/** + * Forces the next frame to be computed on all active segments. + */ void WS2812FX::trigger() { _triggered = true; } @@ -378,17 +404,17 @@ void WS2812FX::setColor(uint8_t slot, uint32_t c) { } void WS2812FX::setBrightness(uint8_t b) { + if (gammaCorrectBri) b = gamma8(b); if (_brightness == b) return; - _brightness = (gammaCorrectBri) ? gamma8(b) : b; + _brightness = b; _segment_index = 0; - if (b == 0) { //unfreeze all segments on power off + if (_brightness == 0) { //unfreeze all segments on power off for (uint8_t i = 0; i < MAX_NUM_SEGMENTS; i++) { _segments[i].setOption(SEG_OPTION_FREEZE, false); } #if LEDPIN == LED_BUILTIN - if (!shouldStartBus) - shouldStartBus = true; + shouldStartBus = true; #endif } else { #if LEDPIN == LED_BUILTIN @@ -890,13 +916,24 @@ void WS2812FX::handle_palette(void) */ uint32_t WS2812FX::color_from_palette(uint16_t i, bool mapping, bool wrap, uint8_t mcol, uint8_t pbri) { - if (SEGMENT.palette == 0 && mcol < 3) return SEGCOLOR(mcol); //WS2812FX default + if (SEGMENT.palette == 0 && mcol < 3) { + uint32_t color = SEGCOLOR(mcol); + if (pbri != 255) { + CRGB crgb_color = col_to_crgb(color); + crgb_color.nscale8_video(pbri); + return crgb_to_col(crgb_color); + } else { + return color; + } + } + uint8_t paletteIndex = i; if (mapping) paletteIndex = (i*255)/(SEGLEN -1); if (!wrap) paletteIndex = scale8(paletteIndex, 240); //cut off blend at palette "end" CRGB fastled_col; fastled_col = ColorFromPalette( currentPalette, paletteIndex, pbri, (paletteBlend == 3)? NOBLEND:LINEARBLEND); - return fastled_col.r*65536 + fastled_col.g*256 + fastled_col.b; + + return crgb_to_col(fastled_col); } //@returns `true` if color, mode, speed, intensity and palette match @@ -924,7 +961,7 @@ void WS2812FX::setRgbwPwm(void) { _analogLastShow = nowUp; RgbwColor c; - uint32_t col = bus->GetPixelColorRgbw(0); + uint32_t col = bus->GetPixelColorRgbw(PWM_INDEX); c.R = col >> 16; c.G = col >> 8; c.B = col; c.W = col >> 24; byte b = getBrightness(); diff --git a/wled00/NpbWrapper.h b/wled00/NpbWrapper.h index 6e56efe5..8c1860b2 100644 --- a/wled00/NpbWrapper.h +++ b/wled00/NpbWrapper.h @@ -45,7 +45,7 @@ //This can be useful if you want to chain multiple strings with incompatible color order //#define COLOR_ORDER_OVERRIDE #define COO_MIN 0 -#define COO_MAX 27 //not inclusive, this would set the override for LEDs 0-26 +#define COO_MAX 35 //not inclusive, this would set the override for LEDs 0-26 #define COO_ORDER COL_ORDER_GRB //END CONFIGURATION @@ -296,7 +296,6 @@ public: void Show() { - byte b; switch (_type) { case NeoPixelType_Grb: _pGrb->Show(); break; @@ -304,6 +303,22 @@ public: } } + /** + * This will return true if enough time has passed since the last time Show() was called. + * This also means that calling Show() will not cause any undue waiting. If the method for + * the defined bus is hardware that sends asynchronously, then call CanShow() will let + * you know if it has finished sending the data from the last Show(). + */ + bool CanShow() + { + switch (_type) + { + case NeoPixelType_Grb: return _pGrb->CanShow(); + case NeoPixelType_Grbw: return _pGrbw->CanShow(); + default: return true; + } + } + void SetPixelColor(uint16_t indexPixel, RgbwColor c) { RgbwColor col; diff --git a/wled00/blynk.cpp b/wled00/blynk.cpp index 39b43ba8..ef53ca9b 100644 --- a/wled00/blynk.cpp +++ b/wled00/blynk.cpp @@ -8,12 +8,12 @@ uint16_t blHue = 0; byte blSat = 255; -void initBlynk(const char* auth) +void initBlynk(const char *auth, const char *host, uint16_t port) { #ifndef WLED_DISABLE_BLYNK if (!WLED_CONNECTED) return; blynkEnabled = (auth[0] != 0); - if (blynkEnabled) Blynk.config(auth); + if (blynkEnabled) Blynk.config(auth, host, port); #endif } diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 9e5c9e82..68eba7a3 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -91,6 +91,7 @@ void deserializeConfig() { CJSON(strip.ablMilliampsMax, hw_led[F("maxpwr")]); CJSON(strip.milliampsPerLed, hw_led[F("ledma")]); CJSON(strip.reverseMode, hw_led[F("rev")]); + CJSON(strip.rgbwMode, hw_led[F("rgbwm")]); JsonObject hw_led_ins_0 = hw_led[F("ins")][0]; //bool hw_led_ins_0_en = hw_led_ins_0[F("en")]; // true @@ -153,7 +154,6 @@ void deserializeConfig() { CJSON(bootPreset, def[F("ps")]); CJSON(turnOnAtBoot, def["on"]); // true CJSON(briS, def["bri"]); // 128 - if (briS == 0) briS = 255; JsonObject def_cy = def[F("cy")]; CJSON(presetCyclingEnabled, def_cy["on"]); @@ -162,7 +162,7 @@ void deserializeConfig() { CJSON(presetCycleMax, def_cy[F("range")][1]); tdd = def_cy[F("dur")] | -1; - if (tdd >= 0) presetCycleTime = tdd * 100; + if (tdd > 0) presetCycleTime = tdd; JsonObject interfaces = doc["if"]; @@ -212,6 +212,10 @@ void deserializeConfig() { if (tdd > 20 || tdd == 0) getStringFromJson(blynkApiKey, apikey, 36); //normally not present due to security + JsonObject if_blynk = interfaces[F("blynk")]; + getStringFromJson(blynkHost, if_blynk[F("host")], 33); + CJSON(blynkPort, if_blynk[F("port")]); + JsonObject if_mqtt = interfaces[F("mqtt")]; CJSON(mqttEnabled, if_mqtt[F("en")]); getStringFromJson(mqttServer, if_mqtt[F("broker")], 33); @@ -221,7 +225,7 @@ void deserializeConfig() { getStringFromJson(mqttClientID, if_mqtt[F("cid")], 41); getStringFromJson(mqttDeviceTopic, if_mqtt[F("topics")][F("device")], 33); // "wled/test" - getStringFromJson(mqttGroupTopic, if_mqtt[F("topics")][F("group")], 33); // "" + getStringFromJson(mqttGroupTopic, if_mqtt[F("topics")][F("group")], 33); // "" JsonObject if_hue = interfaces[F("hue")]; CJSON(huePollingEnabled, if_hue[F("en")]); @@ -251,7 +255,11 @@ void deserializeConfig() { CJSON(countdownMode, ol[F("cntdwn")]); overlayCurrent = overlayDefault; - JsonArray ol_cntdwn = ol[F("cntdwn")]; //[20,12,31,23,59,59] + CJSON(overlayMin, ol[F("min")]); + CJSON(overlayMax, ol[F("max")]); + CJSON(analogClock12pixel, ol[F("o12pix")]); + CJSON(analogClock5MinuteMarks, ol[F("o5m")]); + CJSON(analogClockSecondsTrail, ol[F("osec")]); //timed macro rules JsonObject tm = doc[F("timers")]; @@ -274,11 +282,13 @@ void deserializeConfig() { CJSON(timerMacro[it], timer[F("macro")]); byte dowPrev = timerWeekday[it]; - bool actPrev = timerWeekday[it] & 0x01; + //note: act is currently only 0 or 1. + //the reason we are not using bool is that the on-disk type in 0.11.0 was already int + int actPrev = timerWeekday[it] & 0x01; CJSON(timerWeekday[it], timer[F("dow")]); if (timerWeekday[it] != dowPrev) { //present in JSON timerWeekday[it] <<= 1; //add active bit - bool act = timer[F("en")] | actPrev; + int act = timer[F("en")] | actPrev; if (act) timerWeekday[it]++; } @@ -305,11 +315,11 @@ void deserializeConfig() { CJSON(DMXStart, dmx[F("start")]); CJSON(DMXStartLED,dmx[F("start-led")]); - JsonArray dmx_fixmap = dmx.createNestedArray("fixmap"); + JsonArray dmx_fixmap = dmx[F("fixmap")]; it = 0; for (int i : dmx_fixmap) { if (it > 14) break; - DMXFixtureMap[i] = i; + CJSON(DMXFixtureMap[i],dmx_fixmap[i]); it++; } #endif @@ -359,6 +369,7 @@ void serializeConfig() { ap[F("ssid")] = apSSID; ap[F("pskl")] = strlen(apPass); ap[F("chan")] = apChannel; + ap[F("hide")] = apHide; ap[F("behav")] = apBehavior; JsonArray ap_ip = ap.createNestedArray("ip"); @@ -378,6 +389,7 @@ void serializeConfig() { hw_led[F("maxpwr")] = strip.ablMilliampsMax; hw_led[F("ledma")] = strip.milliampsPerLed; hw_led[F("rev")] = strip.reverseMode; + hw_led[F("rgbwm")] = strip.rgbwMode; JsonArray hw_led_ins = hw_led.createNestedArray("ins"); @@ -478,7 +490,7 @@ void serializeConfig() { JsonArray def_cy_range = def_cy.createNestedArray("range"); def_cy_range.add(presetCycleMin); def_cy_range.add(presetCycleMax); - def_cy[F("dur")] = presetCycleTime / 100; + def_cy[F("dur")] = presetCycleTime; } JsonObject interfaces = doc.createNestedObject("if"); @@ -523,6 +535,8 @@ void serializeConfig() { if_va_macros.add(macroAlexaOff); JsonObject if_blynk = interfaces.createNestedObject("blynk"); if_blynk[F("token")] = strlen(blynkApiKey) ? "Hidden":""; + if_blynk[F("host")] = blynkHost; + if_blynk[F("port")] = blynkPort; JsonObject if_mqtt = interfaces.createNestedObject("mqtt"); if_mqtt[F("en")] = mqttEnabled; @@ -562,6 +576,12 @@ void serializeConfig() { ol[F("clock")] = overlayDefault; ol[F("cntdwn")] = countdownMode; + ol[F("min")] = overlayMin; + ol[F("max")] = overlayMax; + ol[F("o12pix")] = analogClock12pixel; + ol[F("o5m")] = analogClock5MinuteMarks; + ol[F("osec")] = analogClockSecondsTrail; + JsonObject timers = doc.createNestedObject("timers"); JsonObject cntdwn = timers.createNestedObject("cntdwn"); @@ -677,4 +697,4 @@ void serializeConfigSec() { File f = WLED_FS.open("/wsec.json", "w"); if (f) serializeJson(doc, f); f.close(); -} \ No newline at end of file +} diff --git a/wled00/data/index.css b/wled00/data/index.css new file mode 100644 index 00000000..606d6970 --- /dev/null +++ b/wled00/data/index.css @@ -0,0 +1,937 @@ +@font-face { + font-family: "WIcons"; + src: url(data:application/x-font-woff;charset=utf-8;base64,d09GRgABAAAAABsAAAsAAAAAGrQAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABPUy8yAAABCAAAAGAAAABgD50AIWNtYXAAAAFoAAABRAAAAURfBgSRZ2FzcAAAAqwAAAAIAAAACAAAABBnbHlmAAACtAAAFWQAABVkn5Xq/GhlYWQAABgYAAAANgAAADYXA6M1aGhlYQAAGFAAAAAkAAAAJAcYA19obXR4AAAYdAAAAKgAAAConAAT3mxvY2EAABkcAAAAVgAAAFZwmGsgbWF4cAAAGXQAAAAgAAAAIAA0AF1uYW1lAAAZlAAAAUoAAAFKQULQ4XBvc3QAABrgAAAAIAAAACAAAwAAAAMEAAGQAAUAAAKZAswAAACPApkCzAAAAesAMwEJAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAQAAA5BADM/80AMwDMwDMAAAAAQAAAAAAAAAAAAAAIAAAAAAAAwAAAAMAAAAcAAEAAwAAABwAAwABAAAAHAAEASgAAABGAEAABQAGAAEAIOA34DngPOBM4I/gouCp4Lvg1ODp4RbhOeE/4XzhiuIt4j3iouKm4rPixuLk4yXjM+NM45DjlePW4/HkCeQQ//3//wAAAAAAIOA34DngPOBM4I/gouCp4Lvg1ODo4RbhN+E/4XzhiuIt4j3iouKm4rPixuLj4yXjM+NL44/jlOPW4/HkCeQQ//3//wAB/+MfzR/MH8ofux95H2cfYR9QHzgfJR75Htke1B6YHosd6R3aHXYdcx1nHVUdORz5HOwc1RyTHJAcUBw2HB8cGQADAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAf//AA8AAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAgDV/8ADKwLAAAkAEgAAJREhERQGIyEiJgEVITUzNzMXMwEAAgAyI/6qIzICK/2qlirWKpYVAgD+ACMyMgKjVVUrKwADANX/wAMrAsAACQAOABYAACURIREUBiMhIiYTESERISUzFSE1MzczAQACADIj/qojMlUBVv6qAUCW/aqWKtYVAgD+ACMyMgHO/lUBq9VVVSsAAAABAJEAFQOAAlEABQAAJQEXASc3AYABxDz+AO88jQHEPP4A7zwAAAAAAgBV/7EDqwLAACQAQQAAATIXHgEXFhUUBw4BBwYPAScmJy4BJyY1NDc+ATc2MzIWFz4BMwM2Nz4BNzY1NCYjIgYHIy4BIyIGFRQXHgEXFh8BAsAxKys/EhMaG19DRFI+PlJEQ18bGhMSPysrMThlIyNlOLxMPz5YGBhVQDFWEVARVjFAVRgYWD4/TAQCwBISQCsqMjw5OHU/QEs4OEs/QHU4OTwyKitAEhIwKSkw/WlEOzpnLy8uQFY5LCw5VkAuLy9nOjtEBQACAID/wAOAAsAABAA2AAABESMRMxcWFx4BFxYVFAcOAQcGIyInLgEnJjU0Nz4BNzY3Fw4BFRQXHgEXFjMyNz4BNzY1NCYnAitWVs4fGRkjCgkeHmlGRVBQRUZpHh4JCiMZGR88MjwYF1E3Nj4+NjdRFxg8MwLA/lUBq10aICFKKSksUEVGaR4eHh5pRkVQLCkpSiEgGjwpeEY+NjdRFxgYF1E3Nj5GeCkAAAAAAgB0/6YDjALaAE4AWgAAARceAQ8BDgEvAQ4BDwEOASsBIiYvAS4BJwcGJi8BJjY/AS4BNTQ2NycuAT8BPgEfAT4BPwE+ATsBMhYfAR4BFzc2Fh8BFgYPAR4BFRQGBwUyNjU0JiMiBhUUFgMxVQYDBFIDDwdmDyMTDwELCKQICwEQEyIQZgcOBFIDAwVXAgECAVYGAwRSAw8HZg8jEw8BCwikCAsBEBMiEGYHDgRSAwMFVwIBAQH+zz9bWz8/W1sBGEQEDweNBwUCKQwUCGwICgoIbAgUDCkCBQeNBw8ERAoUCgoUCkQEDweNBwUCKQwUCGwICgoIbAgUDCkCBQeNBw8ERAoUCgoUCnJbPz9bWz8/WwAAAwAr/4QD1QMVAB4AMwBSAAABMhceARcWFSM0Jy4BJyYjIgcOAQcGFSM0Nz4BNzYzExUXBycHJzc1LgE1NDYzMhYVFAYHAzIXHgEXFhUjNCcuAScmIyIHDgEHBhUjNDc+ATc2MwIAPjY3URcYVhAROicnLCwnJzoREFYYF1E3Nj4rkTyAgDyRHCQ/LCw/JBwrYVZVgCQlVR4eaUZFUFBFRmkeHlUlJIBVVmECaxgXUTc2PiwnJzoREBAROicnLD42N1EXGP5zjZE8gIA8kY0NNCEsPz8sITQNAjclJIBVVmFQRUZpHh4eHmlGRVBhVlWAJCUAAAIAVf+VA6sC6wAcACYAAAEyFx4BFxYVFAcOAQcGIyInLgEnJjU0Nz4BNzYzEyc3LwEPARcHNwIAWE5OdCEiIiF0Tk5YWU1OdCEiIiF0Tk1ZtC+f0lJS0p8vtALrIiF0Tk5YWE5OdCEiIiF0Tk5YWE5OdCEi/VXOiRLCwhKKzW0AAwAr/5UD1QLrACAAKQAsAAAlBycHJzcuASczHgEXPgE3ITUhNTMVIRUjBgcOAQcGDwElEyMnIwcjEzMDMycCJSCF1T3ZKEAXVRQxHi5EFf4kASpWASp9CxAQJxgYHAEBXMBVMMswVcBVb4pFvVeF1j3WLWI0JkghM3Q9VVZWVSYkJUYhIh8Bbf4AgIACAP7WuAAAAAMAKwAAA9UCgAAbADcAQwAAATIXHgEXFhcGBw4BBwYjIicuAScmJzY3PgE3NhMyNz4BNzY1NCcuAScmIyIHDgEHBhUUFx4BFxYTMhYVFAYjIiY1NDYCAFBJSXovLxsbLy96SUlQUElJei8vGxsvL3pJSVAsJyc6ERAQETonJywsJyc6ERAQETonJyw1S0s1NUtLAoAYF1U7O0ZGOztVFxgYF1U7O0ZGOztVFxj96xAROicnLCwnJzoREBAROicnLCwnJzoREAFVSzU1S0s1NUsAAAAABAAr/5UD1QLAABsALgBGAEwAAAEiBgcnPgEzMhceARcWFw4BByc+ATU0Jy4BJyYlNwEHJw4BIyInLgEnJic+ATcnFw4BFRQXHgEXFjMyNjcnDgEjIiY1NDY3PwEyFh0BAgAVJxJcJ1YtUElIey4vHBdLMH0HCBAROicn/ik3AvQ2jyteMlBJSXovLxsZUTUTigoMEBE6JycsGS8WQgcOBzVLAgF2BzZKAhUIB1wPDxgXVTs7RjxmKX0SJxUsJyc6ERB1Nv0MN48RExgXVTs7Rj9tKRSLFi8ZLCcnOhEQDApCAQJLNQcOB2MBSzUHAAAAAgCr/2sDVQMVABkAMgAAATIXHgEXFhUUBgcnPgE1NCcuAScmIxUnNxURNRcHNSInLgEnJjU0NjcXDgEVFBceARcWAgBHPj5dGxocGT4PDxQURi4vNaurq6tHPj5dGxocGT4PDxQURi4vApUaG10+PkcyXCg/Gj0gNS8uRhQUgKuqgP2rgKuqgBobXT4+RzJcKD8aPSA1Ly5GFBQAAgEAAEADAAJAAAIABgAAJREBEzMRIwEAAWtAVVVAAgD/AAEA/gAAAAIBAABAAwACQAAEAAcAAAEzESMREwERAQBVVZUBawJA/gACAP8AAQD+AAAACABX/5cDqwLpAAMABwALABQAHAAlAC4ATQAAARcFER8BBREXJxElAw4BByc+ATcVBw4BByM+ATcDHgEXBy4BJzMTNx4BFxUuAScBFAcOAQcGBzU2Nz4BNzY1NCcuAScmJzUWFx4BFxYVAi1+/wCCfv8AgoIBANYuVSM9MHNA4hwkBVcHMScIBSQcPScxB1dEPSNVLkBzMAK5Hh1nRkZQPzY2UBcWFhdQNjY/UEZGZx0eAZ5ewAGAYl7AAYBiYv6AwAFSBSQcPScxB1eBI1UuQHMw/scuVCQ9MHNA/uE9HCQFVwcxJwFKU0lKcSUkCVcIHh1bOTpBQTo5Wx0eCFcJJCVxSklTAAAABQBV/+sDqwKVABAAHgAqADgARQAAATIWFREUBiMhIiY1ETQ2MyEBLgE1NDY3Jw4BFRQWFzcyNjU0JiMiBhUUFgU+ATU0JicHHgEVFAYHAzIWFRQGIyImNTQ2MwNVJDIyJP1WJDIyJAKq/fYlJiYlPDIyMjLxR2RkR0dkZAE4MjIyMjwlJiYltSMyMiMjMjIjApUyI/4AIzIyIwIAIzL99iZeMTFfJTwxfkJCfTJGZEdHZGRHR2RGMX5CQn0yPCZeMTFfJQEKMiMjMjIjIzIAAAMAq//rA0MCgwAMAB0ALgAANzQ2MzIWFRQGIyImNREyFx4BFxYVIzQnLgEnJiM1FTIXHgEXFhUjNCcuAScmIzWrNicmNzcmJzaJeXm0NDV5KyuTY2NwV01NcyEheRcYUjc3PkgmNzcmJzY2JwI7NTS0eXmJcGNjkysrefIhIXNNTVc+NzdSGBd5AAAAAQDVABUDKwJrAAsAAAEhESMRITUhETMRIQMr/wBW/wABAFYBAAEV/wABAFYBAP8AAAAAAAYAVf/rA4AClQALABEAHAAhACYAKwAANzUzFSM1MzUjNTM1AzUjNTMVBzUzFQczFSM1NyMTIRUhNRE1IRUhETUhFSFVgIBWKysrK1ZWgExMgE1N1gJV/asCVf2rAlX9q2sqqioWKhYBgIAqqoAqJloqJloBAFZW/apWVgEAVlYABQBV/5UDqwLrABwAOABEAFAAWAAAATIXHgEXFhUUBw4BBwYjIicuAScmNTQ3PgE3NjMRMjc+ATc2NTQnLgEnJiMiBw4BBwYVFBceARcWEyImNTQ2MzIWFRQGISImNTQ2MzIWFRQGEyImJyEOASMCAFhOTnQhIiIhdE5OWFlNTnQhIiIhdE5NWUc+Pl0bGhobXT4+R0c+Pl0bGhobXT4+3BomJhobJSX+uxslJRsaJiZ7S3UaAbQadUsC6yIhdE5OWFhOTnQhIiIhdE5OWFhOTnQhIv0AGhtdPj5HRz4+XRsaGhtdPj5HRz4+XRsaAYAlGxomJhobJSUbGiYmGhsl/upUQkJUAAAAAQEA/5UDKwLrACIAAAEyFx4BFxYVFAcOAQcGIyImJzY3PgE3NjU0Jy4BJyYnPgEzAYBYTk50ISIiIXROTlgiQB5BNzdPFhcXFk83N0EeQCIC6yIhdE5OWFhOTnQhIgoKFCgnakFBSEhBQWonKBQKCgAAAAADAB3/XQPjAyMADwArADgAAAEXBxUjBycjNSc3NTM3FzMBMjc+ATc2NTQnLgEnJiMiBw4BBwYVFBceARcWEzIWFRQGIyImNTQ2MwNVjo7IjY3Ijo7IjY3I/qs1Ly5GFBQUFEYuLzU1Ly5GFBQUFEYuLzVHZGRHR2RkRwHNjY3Ijo7IjY3Ijo79qxQURi4vNTUvLkYUFBQURi4vNTUvLkYUFAGrZEdHZGRHR2QABQCA/8ADgALAACgANABAAEwAWAAAATIXHgEXFhUUBw4BBwYrASIGFRQWFx4BFRQGIyInLgEnJjU0Nz4BNzYDMjY1NCYjIgYVFBY3MjY1NCYjIgYVFBYzMjY1NCYjIgYVFBYXMjY1NCYjIgYVFBYCAFBFRmkeHhEROScnLEwaJgkHCAklG1BFRmkeHh4eaUZFmxslJRsaJiaaGyUlGxomJvAaJiYaGyUlmxomJhobJSUCwBsbXD4/RiwnJzoRESUbDBYICRYMGyUeHmlGRVBQRUZpHh7+gCUbGyUlGxslqyUbGiYmGhslJRsaJiYaGyWrJRsbJSUbGyUAAAAAAgCA/8ADdAK0AAUADwAANwEXASM1AQcnNzYyHwEWFIAB2KD+KKAC9E6gTgwjDWQMYAHYoP4ooAG0TqBODAxkDSMAAAABASv/lQLVAusABwAAASEDMwERIxEBKwGqqqr+1oAC6/6q/gABgAHWAAAAAAkAgP9rA4ADFQADAAcAFgAbAB8AIwAnACsAMAAABTUzFRM1MxUlNDY7ARUjETMVIyImNRElMhYVIwERMxEBNTMVATUzFRM1MxUDNTMUBgKAVVZV/QAyI6urq6sjMgKrIzJV/qpWAQBV/wBVVlVVVTJAVVUCAFVVqyMyVf2qVTIjAlZVMiP9AAOq/FYBAFVVAgBVVf6qVlb+q1UjMgAAAAAEAID/lQOAAxUAAwAHACcARAAAARUhNRMRMxEBHgEVFAcOAQcGIyInLgEnJjU0Nz4BNzYzMhYXNx4BFwEyNz4BNzY1NCcuAScmIyIHDgEHBhUUFx4BFxYzAoD/AFVWAQEnLR4eaEZGUFBGRmgeHh4eaUZFUER6MjwRHg7+lz42N1EXGBgXUTc2Pj42N1EXGBgXUTc2PgMVVVX91gEA/wABGjJ6RE9GRmgeHx8eaEZGT1BGRmgeHiwoPA0eEf2qFxhRNjY+Pjc2URgXFxhRNjc+PjY2URgXAAAJACv/ggPVAykAAwAHAAsADwATABcAMwA3ADsAAAEHJzcDFSM1ARUjNQUHJzcDNxcHEzMVIwEyFx4BFxYVFAcOAQcGIyInLgEnJjU0Nz4BNzYTNTMVJTcXBwEgPE09KYACAFYBlE08TEw7TTwpgID+qzUvLkYUFBQURi4vNTUvLkYUFBQURi4vClb+bE08TAJxPE08/sJVVQGpfn6nTTxN/Xs8TD0BlFUBKhQURS8vNTUuL0UVFBQVRS8uNTUvL0UUFP0tfn6nTTxNAAACAID/vQOAAusABQAKAAAtARcJATcFCQIHAgABOkb+gP6ARQE7/oABgAGARin1Nv7VASs1iAErASv+1TYAAAAAAwBV/70DqwMVAAMACAAWAAAlJzcXJwcBNwEJAQcnBwE3BTcnBwE3JwNNPTM9Rmb+sHwBgP0MAx83odP+gEYBOpc9Wv6AibTAPSg9olABUGH+1QFV/OE2oaQBKzX0dT1GAStrtAAAAAACAFX/lQOrAusAHAAoAAABMhceARcWFRQHDgEHBiMiJy4BJyY1NDc+ATc2MxMnNycHJwcXBxc3FwIAWE5OdCEiIiF0Tk5YWE5OdCEiIiF0Tk5Y1ZmZPJmZPJmZPJmZAusiIXROTlhYTk50ISIiIXROTlhYTk50ISL9vJmZPJmZPJmZPJmZAAAAAQCRABUDgAJRAAUAACUBFwEnNwGAAcQ8/gDvPY4Bwzz+AO88AAAAAAEBAACvAwAB6wAFAAAJAQcnBycCAAEAPMTEPAHr/wA8w8M8AAEBAACVAwAB0QAFAAABFwkBNxcCxDz/AP8APMQB0Tz/AAEAPMMAAAABAFX/lQOrAusALAAAARUjFwcnIxUXBycVIzUHJzc1IwcnNyM1Myc3FzM1JzcXNTMVNxcHFTM3FwczA6uyijzHVcY8ilaKPMZVxzyKsrKKPMdVxjyKVoo8xlXHPIqyAWtWijzGVcc8irKyijzHVcY8ilaKPMZVxzyKsrKKPMdVxjyKAAAFAFX/lQOrAusAHAA4AEQAUABXAAABMhceARcWFRQHDgEHBiMiJy4BJyY1NDc+ATc2MxEyNz4BNzY1NCcuAScmIyIHDgEHBhUUFx4BFxYTIiY1NDYzMhYVFAYhIiY1NDYzMhYVFAYXMhYXIT4BAgBYTk50ISIiIXROTlhZTU50ISIiIXROTVlHPj5dGxoaG10+PkdHPj5dGxoaG10+PtwaJiYaGyUl/rsbJSUbGiYme0t1Gv5MGnUC6yIhdE5OWFhOTnQhIiIhdE5OWFhOTnQhIv0AGhtdPj5HRz4+XRsaGhtdPj5HRz4+XRsaAYAlGxomJhobJSUbGiYmGhslgFRCQlQAAAIAq/+VA1UDIwAmADkAAAEWFx4BFxYVFAcOAQcGIyInLgEnJjU0Nz4BNzY3BxQWMzI2NTQmMQMyNz4BNzY1NCYnDgEHDgEVFBYCQD8zM0kTFBobXT4+R0c+Pl0bGgkJJBoZIAFZQkJQIEwqJSY3EBAMDSBsOThAUQMjMj4/kVFRVkc+Pl0bGxsbXT4+RzYzNGAsLCYPQl5eQkSI/PIQETclJiotViosNwwLRjQ3TwAAAgBV/8ADqwLrAAkAEwAAAQcTJQUTJyUbAQMXJzcvAQ8BFwcDq+lG/vj++EbpATN4eHihK467SUm6jSoBtsr+1J+fASzKGgEb/uX+32G2exCtrBB7twAAAAEAAAABAABJZ54xXw889QALBAAAAAAA2gCv2QAAAADaAK/ZAAD/XQPjAykAAAAIAAIAAAAAAAAAAQAAAzP/NAAABAAAAAAAA+MAAQAAAAAAAAAAAAAAAAAAACoEAAAAAAAAAAAAAAAAAAAABAAA1QQAANUEAACRBAAAVQQAAIAEAAB0BAAAKwQAAFUEAAArBAAAKwQAACsEAACrBAABAAQAAQAEAABXBAAAVQQAAKsEAADVBAAAVQQAAFUEAAEABAAAHQQAAIAEAACABAABKwQAAIAEAACABAAAKwQAAIAEAABVBAAAVQQAAJEEAAEABAABAAQAAFUEAABVBAAAqwQAAFUAAAAAAAoAFAAeAEAAagB+AOIBOAHCAjwCfALGAzADqAP2BAoEIASkBQwFUgVsBawGMAZqBsAHPgdgB3YHxggyCJgIuAjsCTAJRAlWCWoJrAouCoYKsgAAAAEAAAAqAFsACQAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAOAK4AAQAAAAAAAQACAAAAAQAAAAAAAgAHADMAAQAAAAAAAwACACcAAQAAAAAABAACAEgAAQAAAAAABQALAAYAAQAAAAAABgACAC0AAQAAAAAACgAaAE4AAwABBAkAAQAEAAIAAwABBAkAAgAOADoAAwABBAkAAwAEACkAAwABBAkABAAEAEoAAwABBAkABQAWABEAAwABBAkABgAEAC8AAwABBAkACgA0AGh1aQB1AGlWZXJzaW9uIDEuMABWAGUAcgBzAGkAbwBuACAAMQAuADB1aQB1AGl1aQB1AGlSZWd1bGFyAFIAZQBnAHUAbABhAHJ1aQB1AGlGb250IGdlbmVyYXRlZCBieSBJY29Nb29uLgBGAG8AbgB0ACAAZwBlAG4AZQByAGEAdABlAGQAIABiAHkAIABJAGMAbwBNAG8AbwBuAC4AAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) format('woff'); +} + +:root { + --c-1: #111; + --c-f: #fff; + --c-2: #222; + --c-3: #333; + --c-4: #444; + --c-5: #555; + --c-6: #666; + --c-8: #888; + --c-b: #bbb; + --c-c: #ccc; + --c-e: #eee; + --c-d: #ddd; + --c-r: #831; + --t-b: 0.5; + --c-o: rgba(34, 34, 34, 0.9); + --c-tb : rgba(34, 34, 34, var(--t-b)); + --c-tba: rgba(102, 102, 102, var(--t-b)); + --c-tbh: rgba(51, 51, 51, var(--t-b)); + /*following are internal*/ + --th: 70px; + --tp: 70px; + --bh: 63px; + --tbp: 14px 14px 10px 14px; + --bbp: 9px 0 7px 0; + --bhd: none; + --bmt: 0px; +} + +html { + touch-action: manipulation; +} + +body { + margin: 0; + background-color: var(--c-1); + font-family: Helvetica, Verdana, sans-serif; + font-size: 17px; + color: var(--c-f); + text-align: center; + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-tap-highlight-color: transparent; + scrollbar-width: 6px; + scrollbar-color: var(--c-sb) transparent; +} + +html, +body { + height: 100%; + width: 100%; + position: fixed; + overscroll-behavior: none; +} + +#bg { + height: 100vh; + width: 100vw; + position: fixed; + z-index: -10; + background-position: center; + background-repeat: no-repeat; + background-size: cover; + opacity: 0; + transition: opacity 2s; +} + +p { + margin: 10px 0 2px 0; + color: var(--c-d); +} + +button { + outline: none; + cursor: pointer; +} + +.labels { + margin: 0; + padding: 8px 0 2px 0; +} + +#namelabel { + position: fixed; + bottom: calc(var(--bh) + 5px); + right: 4px; + color: var(--c-6); + writing-mode: vertical-rl; +} + +.bri { + padding: 4px; +} + +.wrapper { + position: fixed; + top: 0; + left: 0; + right: 0; + background: var(--c-tb); + z-index: 1; +} + +.icons { + font-family: 'WIcons'; + font-style: normal; + font-size: 24px; + line-height: 1; + display: inline-block; + margin: -2px 0 4px 0; +} + +.huge { + font-size: 42px; +} + +.infot { + table-layout: fixed; + width: 100%; +} + +.segt { + table-layout: fixed; + width: 76%; +} + +.segtd { + text-align: center; + text-transform: uppercase; + font-size: 14px; +} + +.keytd { + text-align: left; + padding-bottom: 8px; +} + +.valtd { + text-align: right; + padding-bottom: 8px; +} + +.slider-icon +{ + transform: translate(6px,3px); + color: var(--c-d); +} + +.e-icon +{ + transform: translateY(3px); + color: var(--c-d); +} + +.sel-icon { + transform: translateX(3px); + color: var(--c-d); +} + +.flr { + float: right; + cursor: pointer; + margin: 0; + color: var(--c-f); + transform: rotate(0deg); + transition: transform 0.3s; +} + +.exp { + transform: rotate(180deg); +} + +.il { + display: inline-block; + vertical-align: middle; +} + +#liveview { + height: 4px; + display: none; + width: 100%; + border: 0px; +} + +.tab { + background-color: transparent; + color: var(--c-d); +} + +.bot { + position: fixed; + bottom: 0; + left: 0; + width: 100%; + background-color: var(--c-tb); +} + +.tab button { + background-color: transparent; + float: left; + border: none; + transition: color 0.3s, background-color 0.3s; + font-size: 17px; + color: var(--c-c); +} + +.top button { + padding: var(--tbp); +} + +.bot button { + padding: var(--bbp); + width:25%; +} + +.tab button:hover { + background-color: var(--c-tbh); + color: var(--c-e); +} + +.tab button.active { + background-color: var(--c-tba) !important; + color: var(--c-f); +} + +.active { + background-color: var(--c-6) !important; + color: var(--c-f); +} + +.container { + --n: 1; + width: 100%; + width: calc(var(--n)*100%); + height: calc(100% - var(--tp) - var(--bh)); + margin-top: var(--tp); + transform: translate(calc(var(--i, 0)/var(--n)*-100%)); + overscroll-behavior: none; +} + +.tabcontent { + float: left; + position: relative; + width: 100%; + width: calc(100%/var(--n)); + padding: 11px 0; + box-sizing: border-box; + border: 0px; + overflow: auto; + height: 100%; + overscroll-behavior: none; +} + +#Effects { + padding-top: 0; + margin-top: 11px; + height: calc(100% - 11px); + -webkit-overflow-scrolling: touch; +} + +.smooth { transition: transform calc(var(--f, 1)*.5s) ease-out } + +.tab-label { + margin: 0 0 -5px 0; + padding-bottom: 4px; +} + +.overlay { + position: fixed; + height: 100%; + width: 100%; + top: 0; + left: 0; + background-color: var(--c-3); + font-size: 24px; + display: flex; + align-items: center; + justify-content: center; + z-index: 11; + opacity: 0.95; + transition: 0.7s; + pointer-events: none; +} + +.staytop { + display: block; + position: -webkit-sticky; + position: sticky; + background: var(--c-1); + top: -1px; + z-index: 1; + margin-top: 1px; + width: 274px; + margin: auto; + border-radius: 25px; +} + +#staytop1 { + top: 28px; +} + +#staytop2 { + top: 56px; +} + +#fxb0 { + margin-bottom: 2px; + filter: drop-shadow(0 0 1px #000); +} + +.first { + margin-top: 18px !important; +} + +#toast { + opacity: 0; + background-color: var(--c-5); + max-width: 90%; + color: var(--c-f); + text-align: center; + border-radius: 5px; + padding: 16px; + position: fixed; + z-index: 5; + left: 50%; + transform: translateX(-50%); + bottom: calc(var(--bh) + 22px); + font-size: 17px; + pointer-events: none; +} + +#toast.show { + opacity: 1; + animation: fadein 0.5s, fadein 0.5s 2.5s reverse; +} + +#toast.error { + opacity: 1; + background-color: #b21; + animation: fadein 0.5s; +} + +.modal { + position:fixed; + left: 0px; + bottom: 0px; + right: 0px; + top: calc(var(--th) - 1px); + background-color: var(--c-o); + transform: translateY(100%); + transition: transform 0.4s; + padding: 8px; + font-size: 20px; + overflow: auto; +} + +#info { + z-index: 3; +} + +#rover { + z-index: 2; +} + +#roverstar { + position: fixed; + top: calc(var(--th) + 5px); + left: 1px; + display: none; + cursor: pointer; +} + +#connind { + position: fixed; + bottom: calc(var(--bh) + 5px); + left: 4px; + padding: 5px; + border-radius: 5px; + background-color: #a90; + z-index: -2; +} + +#imgw { + width: 200px; + height: 55px; + display: inline-block; +} + +#kv { + max-width: 490px; + display: inline-block; +} + +#lv { + max-width: 600px; + display: inline-block; +} + +#heart { + transition: color 0.9s; + font-size: 16px; + color: #f00; +} + +img { + max-width: 100%; + max-height: 100%; +} + +@keyframes fadein { + from {bottom: 0; opacity: 0;} + to {bottom: calc(var(--bh) + 22px); opacity: 1;} +} + +.sliderdisplay { + content:''; + position: absolute; + top: 13px; bottom: 13px; + left: 10px; right: 10px; + background: var(--c-4); + border-radius: 17px; + pointer-events: none; + z-index: -1; +} + +.sliderbubble { + width: 36px; + line-height: 24px; + background: var(--c-3); + position: absolute; + transform: translateX(-50%); + border-radius: 12px; + margin-left: 12px; + margin-top: 3px; + padding: 0px; + display: inline; +} + +.hidden { + display: none; +} + +input[type=range] { + -webkit-appearance: none; + width: 220px; + padding: 0px; + margin: 0px 10px 0px 10px; + background-color: transparent; + cursor: pointer; +} +input[type=range]:focus { + outline: none; +} +input[type=range]::-webkit-slider-runnable-track { + width: 100%; + height: 30px; + cursor: pointer; + background: transparent; +} +input[type=range]::-webkit-slider-thumb { + height: 16px; + width: 16px; + border-radius: 17px; + background: var(--c-f); + cursor: pointer; + -webkit-appearance: none; + margin-top: 7px; +} +input[type=range]::-moz-range-track { + width: 100%; + height: 30px; + background-color: rgba(0, 0, 0, 0); +} +input[type=range]::-moz-range-thumb { + border: 0px solid rgba(0, 0, 0, 0); + height: 16px; + width: 16px; + border-radius: 17px; + background: var(--c-f); + transform: translateY(7px); +} +input[type=range]:active + .sliderbubble { + display: inline; + transform: translateX(-50%); +} + +#wwrap { + display: none; +} + +.sliderwrap { + height: 30px; + width: 240px; + position: relative; +} + +.sws { + width: 212px; +} + +.sis { + width: 192px !important; +} + +.hd { + display: var(--bhd); +} + +#briwrap { + float: right; + margin-top: var(--bmt); +} + +#picker { + margin: 10px auto; + width: 260px; +} + +#rgbwrap { + display: none; +} + +.btn { + padding: 8px; + margin: 10px; + width: 230px; + font-size: 19px; + background-color: var(--c-3); + color: var(--c-f); + border: 0px solid white; + border-radius: 25px; + transition-duration: 0.5s; + -webkit-backface-visibility: hidden; + -webkit-transform:translate3d(0,0,0); +} + +.btn-s { + padding: 9px; + width: 276px; + background-color: var(--c-2); +} +.btn-i { + padding-bottom: 3px; +} +.btn-icon { + margin: 0px 8px 4px 0; + vertical-align: middle; +} +.btna-icon { + margin: 0px; +} +.btn-p { + width: 216px; +} + +#qcs-w { + margin-top: 10px; +} +.qcs { + padding: 14px; + margin: 2px; + border-radius: 14px; + display: inline-block; +} +.qcsb { + padding: 13px; + border: 1px solid var(--c-f); +} +#hexw { + margin-top: 5px; + display: none; +} + +.cl { + width: 42px; +} + +select { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + background: url("data:image/svg+xml;utf8,") no-repeat; + background-size: 12px; + background-position: 206px 16px; + padding-left: 12px !important; + background-repeat: no-repeat; + outline: none; +} +select:-moz-focusring { + transition-duration: 0s; + color: transparent; + text-shadow: 0 0 0 #fff; +} +option { + background-color: var(--c-3); + color: var(--c-f); +} + +input[type=number], input[type=text] { + background: var(--c-3); + color: var(--c-f); + border: 0px solid white; + border-radius: 5px; + padding: 8px; + margin: 6px 6px 6px 0; + font-size: 19px; + transition: background-color 0.2s; + outline: none; + width: 50px; + -webkit-appearance: textfield; + -moz-appearance: textfield; + appearance: textfield; +} + +textarea { + background: var(--c-2); + color: var(--c-f); + width: 236px; + height: 90px; + border-radius: 5px; + border: 2px solid #555; + outline: none; + resize: none; + font-size: 19px; +} + +::selection { + background: var(--c-b); +} + +input[type=text] { + width: 100px; + border-radius: 25px; + text-align: center; +} + +.ptxt { + width: 200px !important; + margin: 26px 0 6px 12px !important; +} + +.stxt { + width: 50px !important; +} + +input[type=number]:focus, input[type=text]:focus { + background: var(--c-6); +} + +input[type=number]::-webkit-inner-spin-button, +input[type=number]::-webkit-outer-spin-button { + -webkit-appearance: none; +} + +.segn { + margin: 3px 0 6px 0 !important; +} + +.segname { + position: absolute; + top: 0px; + left: 50%; + padding: 9px 0; + transform: translateX(-50%); + white-space: nowrap; + cursor: pointer; +} + +.pname { + width: 208px; + padding: 8px 0; + text-align: center; + overflow: hidden; + text-overflow: clip; +} + +.pid { + position: absolute; + top: 0px; + left: 0px; + padding: 11px 0px 0px 11px; + font-size: 16px; + width: 20px; + text-align: center; + color: var(--c-b); +} + +.newseg { + cursor: default; +} + +.ic { + padding: 6px 0 0 0; +} + +.xxs { + width: 40px; + margin: 6px; +} + +.psts { + background-color: var(--c-3); + color: var(--c-f); + cursor: pointer; + padding: 2px 0 0 0; + height: 40px; +} + +.cnf { + color: var(--c-f); + cursor: pointer; + background: var(--c-3); + border-radius: 5px; +} + +.cnf-s { + position: absolute; + top: 66px; + right: 28px; + padding: 43px 6px; +} + +.pwr { + color: var(--c-6); + transform: translate(2px, 3px); + cursor: pointer; +} + +.act { + color: var(--c-f); +} + +.half { + padding: 7.5px; + top: 64px; +} + +.del { + position: absolute; + bottom: 8px; + right: 8px; + color: var(--c-f); + cursor: pointer; +} + +.check { + display: inline-block; + position: relative; + padding-bottom: 32px; + margin-bottom: 14px; + cursor: pointer; + text-align: center; +} + +.schkl { + padding: 2px 22px 0px 35px; + margin: 0 0 0 2px; +} + +.revchkl { + padding: 2px 0px 0px 35px; + margin-bottom: 0px; + margin-top: 8px; +} + +.check input { + position: absolute; + opacity: 0; + cursor: pointer; + height: 0; + width: 0; +} + +.checkmark { + position: absolute; + bottom: 0; + left: 0; + height: 25px; + width: 25px; + background-color: var(--c-3); + border-radius: 10px; +} + +.schk { + top: 0; +} + +.psv { + left: initial; + bottom: initial; + top: 0; + right: 0; +} + +.psvl { + padding: 2px 35px 10px 0px; + margin-top: 10px; + margin-bottom: 0px; +} + + +.check:hover input ~ .checkmark { + background-color: var(--c-4); +} + +.check input:checked ~ .checkmark { + background-color: var(--c-6); +} + +.checkmark:after { + content: ""; + position: absolute; + display: none; +} + +.check input:checked ~ .checkmark:after { + display: block; +} + +.check .checkmark:after { + left: 9px; + top: 5px; + width: 5px; + height: 10px; + border: solid var(--c-f); + border-width: 0 3px 3px 0; + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); +} + +.h { + font-size: 13px; + text-align: center; + color: var(--c-b); +} + +.bp { + margin-bottom: 5px; +} + +.seg { + position: relative; + display: inline-block; + padding: 8px; + margin: 10px; + width: 260px; + font-size: 19px; + background-color: var(--c-2); + color: var(--c-f); + border: 0px solid white; + border-radius: 20px; + text-align: left; + transition: background-color 0.5s; + filter: brightness(1); +} + +.pres { + margin-bottom: 6px; +} + +.segin { + padding: 8px 8px 4px 8px; + display: none; +} + +.expanded { + display: block; +} + +.c { + text-align: center; +} + +.po2 { + display: none; + margin-top: 8px; +} + +.pwarn { + color: red; +} + +::-webkit-scrollbar { + width: 6px; +} +::-webkit-scrollbar-track { + background: transparent; +} +::-webkit-scrollbar-thumb { + background: var(--c-sb); + opacity: 0.2; + border-radius: 5px; +} +::-webkit-scrollbar-thumb:hover { + background: var(--c-sbh); +} + +@media all and (max-width: 335px) { + .sliderbubble { + display: none; + } +} + +@media all and (max-width: 550px) and (min-width: 374px) { + .infobtn { + width: 155px; + } +} + +@media all and (max-width: 685px) { + .top button { + width: 16.6%; + padding: 8px 0 4px 0; + } + .hd { + display: none !important; + } + #briwrap { + margin-top: 0px !important; + float: none; + } +} + +@media all and (max-width: 1249px) { + #buttonPcm { + display: none; + } +} \ No newline at end of file diff --git a/wled00/data/index.htm b/wled00/data/index.htm index c07ac747..9bdaae86 100644 --- a/wled00/data/index.htm +++ b/wled00/data/index.htm @@ -1,5 +1,5 @@ - + @@ -8,933 +8,7 @@ WLED - + @@ -1118,1419 +192,8 @@ input[type=number]::-webkit-outer-spin-button { For best performance, it is recommended to turn off the streaming source when not in use.
- + + + \ No newline at end of file diff --git a/wled00/data/index.js b/wled00/data/index.js new file mode 100644 index 00000000..f3fb72af --- /dev/null +++ b/wled00/data/index.js @@ -0,0 +1,1414 @@ +//page js +var loc = false, locip; +var noNewSegs = false; +var isOn = false, nlA = false, isLv = false, isInfo = false, syncSend = false, syncTglRecv = true, isRgbw = false; +var whites = [0,0,0]; +var expanded = [false]; +var powered = [true]; +var nlDur = 60, nlTar = 0; +var nlFade = false; +var selectedFx = 0; +var csel = 0; +var currentPreset = -1; +var lastUpdate = 0; +var segCount = 0, ledCount = 0, lowestUnused = 0, maxSeg = 0, lSeg = 0; +var pcMode = false, pcModeA = false, lastw = 0; +var d = document; +const ranges = RangeTouch.setup('input[type="range"]', {}); +var pJson = {}; +var pN = "", pI = 0; +var pmt = 1, pmtLS = 0, pmtLast = 0; +var lastinfo = {}; +var cfg = { + theme:{base:"dark", bg:{url:""}, alpha:{bg:0.6,tab:0.8}, color:{bg:""}}, + comp :{colors:{picker: true, rgb: false, quick: true, hex: false}, labels:true, pcmbot:false, pid:true} +}; + +var cpick = new iro.ColorPicker("#picker", { + width: 260, + wheelLightness: false, + wheelAngle: 90, + layout: [ + { + component: iro.ui.Wheel, + options: {} + }, + { + component: iro.ui.Slider, + options: { + sliderType: 'value' + } + }, + { + component: iro.ui.Slider, + options: { + sliderType: 'kelvin', + minTemperature: 2100, + maxTemperature: 10000 + } + } + ] +}); + +function handleVisibilityChange() { + if (!document.hidden && new Date () - lastUpdate > 3000) { + requestJson(null); + } +} + +function sCol(na, col) { + d.documentElement.style.setProperty(na, col); +} + +function applyCfg() +{ + cTheme(cfg.theme.base === "light"); + var bg = cfg.theme.color.bg; + if (bg) sCol('--c-1', bg); + var ccfg = cfg.comp.colors; + d.getElementById('hexw').style.display = ccfg.hex ? "block":"none"; + d.getElementById('picker').style.display = ccfg.picker ? "block":"none"; + d.getElementById('rgbwrap').style.display = ccfg.rgb ? "block":"none"; + d.getElementById('qcs-w').style.display = ccfg.quick ? "block":"none"; + var l = cfg.comp.labels; + var e = d.querySelectorAll('.tab-label'); + for (var i=0; i 23 && today.getDate() < 28)) img.src = "https://aircoookie.github.io/xmas.png"; + } + img.addEventListener('load', (event) => { + var a = parseFloat(cfg.theme.alpha.bg); + d.getElementById('staytop2').style.background = "transparent"; + if (isNaN(a)) a = 0.6; + bg.style.opacity = a; + bg.style.backgroundImage = `url(${img.src})`; + img = null; + }); +} + +function onLoad() { + if (window.location.protocol == "file:") { + loc = true; + locip = localStorage.getItem('locIp'); + if (!locip) + { + locip = prompt("File Mode. Please enter WLED IP!"); + localStorage.setItem('locIp', locip); + } + } + var sett = localStorage.getItem('wledUiCfg'); + if (sett) cfg = mergeDeep(cfg, JSON.parse(sett)); + + resetPUtil(); + + applyCfg(); + loadBg(cfg.theme.bg.url); + + var cd = d.getElementById('csl').children; + for (var i = 0; i < cd.length; i++) { + cd[i].style.backgroundColor = "rgb(0, 0, 0)"; + } + selectSlot(0); + updateTablinks(0); + resetUtil(); + cpick.on("input:end", function() { + setColor(1); + }); + pmtLS = localStorage.getItem('wledPmt'); + setTimeout(function(){requestJson(null, false);}, 25); + d.addEventListener("visibilitychange", handleVisibilityChange, false); + size(); + d.getElementById("cv").style.opacity=0; + if (localStorage.getItem('pcm') == "true") togglePcMode(true); + var sls = d.querySelectorAll('input[type="range"]'); + for (var sl of sls) { + sl.addEventListener('input', updateBubble, true); + sl.addEventListener('touchstart', toggleBubble); + sl.addEventListener('touchend', toggleBubble); + } +} + +function updateTablinks(tabI) +{ + var tablinks = d.getElementsByClassName("tablinks"); + for (var i of tablinks) { + i.className = i.className.replace(" active", ""); + } + if (pcMode) return; + tablinks[tabI].className += " active"; +} + +function openTab(tabI, force = false) { + if (pcMode && !force) return; + iSlide = tabI; + _C.classList.toggle('smooth', false); + _C.style.setProperty('--i', iSlide); + updateTablinks(tabI); +} + +var timeout; +function showToast(text, error = false) { + if (error) d.getElementById('connind').style.backgroundColor = "#831"; + var x = d.getElementById("toast"); + x.innerHTML = text; + x.className = error ? "error":"show"; + clearTimeout(timeout); + x.style.animation = 'none'; + x.style.animation = null; + timeout = setTimeout(function(){ x.className = x.className.replace("show", ""); }, 2900); +} + +function showErrorToast() { + showToast('Connection to light failed!', true); +} +function clearErrorToast() { + d.getElementById("toast").className = d.getElementById("toast").className.replace("error", ""); +} + +function getRuntimeStr(rt) +{ + var t = parseInt(rt); + var days = Math.floor(t/86400); + var hrs = Math.floor((t - days*86400)/3600); + var mins = Math.floor((t - days*86400 - hrs*3600)/60); + var str = days ? (days + " " + (days == 1 ? "day" : "days") + ", ") : ""; + str += (hrs || days) ? (hrs + " " + (hrs == 1 ? "hour" : "hours")) : ""; + if (!days && hrs) str += ", "; + if (t > 59 && !days) str += mins + " min"; + if (t < 3600 && t > 59) str += ", "; + if (t < 3600) str += (t - mins*60) + " sec"; + return str; +} + +function inforow(key, val, unit = "") +{ + return `${key}${val}${unit}`; +} + +function getLowestUnusedP() +{ + var l = 1; + for (var key in pJson) + { + if (key == l) l++; + } + if (l > 250) l = 250; + return l; +} + +function checkUsed(i) { + var id = d.getElementById(`p${i}id`).value; + if (pJson[id] && (i == 0 || id != i)) { + d.getElementById(`p${i}warn`).innerHTML = `⚠ Overwriting ${pName(id)}!`; + } else { + d.getElementById(`p${i}warn`).innerHTML = ""; + } +} + +function pName(i) { + var n = "Preset " + i; + if (pJson[i].n) n = pJson[i].n; + return n; +} + +function papiVal(i) { + if (!pJson[i]) return ""; + var o = Object.assign({},pJson[i]); + if (o.win) return o.win; + delete o.n; delete o.p; delete o.ql; + return JSON.stringify(o); +} + +function qlName(i) { + if (!pJson[i]) return ""; + if (!pJson[i].ql) return ""; + return pJson[i].ql; +} + +function cpBck() { + var copyText = document.getElementById("bck"); + + copyText.select(); + copyText.setSelectionRange(0, 999999); + + document.execCommand("copy"); + + showToast("Copied to clipboard!"); +} + +function presetError(empty) +{ + var hasBackup = false; var bckstr = ""; + try { + bckstr = localStorage.getItem("wledP"); + if (bckstr.length > 10) hasBackup = true; + } catch (e) { + + } + var cn = `
`; + if (empty) + cn += `You have no presets yet!`; + else + cn += `Sorry, there was an issue loading your presets!`; + + if (hasBackup) { + cn += `

`; + if (empty) + cn += `However, there is backup preset data of a previous installation available.
+ (Saving a preset will hide this and overwrite the backup)`; + else + cn += `Here is a backup of the last known good state:`; + cn += `
+ `; + } + cn += `
`; + d.getElementById('pcont').innerHTML = cn; + if (hasBackup) d.getElementById('bck').value = bckstr; +} + +function loadPresets() +{ + var url = '/presets.json'; + if (loc) { + url = `http://${locip}/presets.json`; + } + + fetch + (url, { + method: 'get' + }) + .then(res => { + if (!res.ok) { + showErrorToast(); + } + return res.json(); + }) + .then(json => { + pJson = json; + populatePresets(); + }) + .catch(function (error) { + showToast(error, true); + console.log(error); + presetError(false); + }); +} + +var pQL = []; + +function populateQL() +{ + var cn = ""; + if (pQL.length > 0) { + cn += `

Quick load

`; + + var it = 0; + for (var key of (pQL||[])) + { + cn += ``; + it++; + if (it > 4) { + it = 0; + cn += '
'; + } + } + if (it != 0) cn+= '
'; + + cn += `

All presets

`; + } + d.getElementById('pql').innerHTML = cn; +} + +function populatePresets(fromls) +{ + if (fromls) pJson = JSON.parse(localStorage.getItem("wledP")); + delete pJson["0"]; + var cn = ""; + var arr = Object.entries(pJson); + arr.sort(cmpP); + var added = false; + pQL = []; + var is = []; + + for (var key of (arr||[])) + { + if (!isObject(key[1])) continue; + let i = parseInt(key[0]); + var qll = key[1].ql; + if (qll) pQL.push([i, qll]); + is.push(i); + + cn += `
`; + if (cfg.comp.pid) cn += `
${i}
`; + cn += `
${pName(i)}
+ +
+

`; + added = true; + } + + d.getElementById('pcont').innerHTML = cn; + if (added) { + if (pmtLS != pmt && pmt != 0) { + localStorage.setItem("wledPmt", pmt); + pJson["0"] = {}; + localStorage.setItem("wledP", JSON.stringify(pJson)); + } + pmtLS = pmt; + for (var a = 0; a < is.length; a++) { + let i = is[a]; + if (expanded[i+100]) expand(i+100, true); + } + } else { presetError(true); } + updatePA(); + populateQL(); +} + +function populateInfo(i) +{ + var cn=""; + var heap = i.freeheap/1000; + heap = heap.toFixed(1); + var pwr = i.leds.pwr; + var pwru = "Not calculated"; + if (pwr > 1000) {pwr /= 1000; pwr = pwr.toFixed((pwr > 10) ? 0 : 1); pwru = pwr + " A";} + else if (pwr > 0) {pwr = 50 * Math.round(pwr/50); pwru = pwr + " mA";} + var urows=""; + if (i.u) { + for (var k = 0; k < i.u.length; k++) + { + var val = i.u[k]; + if (val[1]) { + urows += inforow(k,val[0],val[1]); + } else { + urows += inforow(k,val); + } + } + } + var vcn = "Kuuhaku"; + if (i.ver.startsWith("0.11.")) vcn = "Mirai"; + if (i.cn) vcn = i.cn; + + cn += `v${i.ver} "${vcn}"

+ ${urows} + ${inforow("Build",i.vid)} + ${inforow("Signal strength",i.wifi.signal +"% ("+ i.wifi.rssi, " dBm)")} + ${inforow("Uptime",getRuntimeStr(i.uptime))} + ${inforow("Free heap",heap," kB")} + ${inforow("Estimated current",pwru)} + ${inforow("MAC address",i.mac)} + ${inforow("Filesystem",i.fs.u + "/" + i.fs.t + " kB (" +Math.round(i.fs.u*100/i.fs.t) + "%)")} + ${inforow("Environment",i.arch + " " + i.core + " (" + i.lwip + ")")} +
`; + d.getElementById('kv').innerHTML = cn; +} + +function populateSegments(s) +{ + var cn = ""; + segCount = 0; lowestUnused = 0; lSeg = 0; + + for (var y = 0; y < (s.seg||[]).length; y++) + { + segCount++; + + var inst=s.seg[y]; + let i = parseInt(inst.id); + powered[i] = inst.on; + if (i == lowestUnused) lowestUnused = i+1; + if (i > lSeg) lSeg = i; + + cn += `
+ +
+ Segment ${i} +
+ +
+ + + + + + + + + +
Start LEDStop LED
+ + + + + + + + + +
GroupingSpacing
+
+ +
+ +
+
+ + + + +
+

`; + } + + d.getElementById('segcont').innerHTML = cn; + if (lowestUnused >= maxSeg) { + d.getElementById('segutil').innerHTML = 'Maximum number of segments reached.'; + noNewSegs = true; + } else if (noNewSegs) { + resetUtil(); + noNewSegs = false; + } + for (var i = 0; i <= lSeg; i++) { + updateLen(i); + updateTrail(d.getElementById(`seg${i}bri`)); + if (segCount < 2) d.getElementById(`segd${lSeg}`).style.display = "none"; + } + d.getElementById('rsbtn').style.display = (segCount > 1) ? "inline":"none"; +} + +function updateTrail(e, slidercol) +{ + if (e==null) return; + var max = e.hasAttribute('max') ? e.attributes.max.value : 255; + var progress = e.value * 100 / max; + progress = parseInt(progress); + var scol; + switch (slidercol) { + case 1: scol = "#f00"; break; + case 2: scol = "#0f0"; break; + case 3: scol = "#00f"; break; + default: scol = "var(--c-f)"; + } + var val = `linear-gradient(90deg, ${scol} ${progress}%, var(--c-4) ${progress}%)`; + e.parentNode.getElementsByClassName('sliderdisplay')[0].style.background = val; +} + +function updateBubble(e) +{ + var bubble = e.target.parentNode.getElementsByTagName('output')[0]; + + if (bubble) { + bubble.innerHTML = e.target.value; + } +} + +function toggleBubble(e) +{ + e.target.parentNode.querySelector('output').classList.toggle('hidden'); +} + +function updateLen(s) +{ + if (!d.getElementById(`seg${s}s`)) return; + var start = parseInt(d.getElementById(`seg${s}s`).value); + var stop = parseInt(d.getElementById(`seg${s}e`).value); + var len = stop - start; + var out = "(delete)"; + if (len > 1) { + out = `${len} LEDs`; + } else if (len == 1) { + out = "1 LED"; + } + + if (d.getElementById(`seg${s}grp`) != null) + { + var grp = parseInt(d.getElementById(`seg${s}grp`).value); + var spc = parseInt(d.getElementById(`seg${s}spc`).value); + if (grp == 0) grp = 1; + var virt = Math.ceil(len/(grp + spc)); + if (!isNaN(virt) && (grp > 1 || spc > 0)) out += ` (${virt} virtual)`; + } + + d.getElementById(`seg${s}len`).innerHTML = out; +} + +function updatePA() +{ + var ps = d.getElementsByClassName("seg"); + for (let i = 0; i < ps.length; i++) { + ps[i].style.backgroundColor = "var(--c-2)"; + } + ps = d.getElementsByClassName("psts"); + for (let i = 0; i < ps.length; i++) { + ps[i].style.backgroundColor = "var(--c-2)"; + } + if (currentPreset > 0) { + var acv = d.getElementById(`p${currentPreset}o`); + if (acv && !expanded[currentPreset+100]) + acv.style.background = "var(--c-6)"; + acv = d.getElementById(`p${currentPreset}qlb`); + if (acv) acv.style.background = "var(--c-6)"; + } +} + +function updateUI() +{ + d.getElementById('buttonPower').className = (isOn) ? "active":""; + d.getElementById('buttonNl').className = (nlA) ? "active":""; + d.getElementById('buttonSync').className = (syncSend) ? "active":""; + + d.getElementById('fxb' + selectedFx).style.backgroundColor = "var(--c-6)"; + updateTrail(d.getElementById('sliderBri')); + updateTrail(d.getElementById('sliderSpeed')); + updateTrail(d.getElementById('sliderIntensity')); + updateTrail(d.getElementById('sliderW')); + if (isRgbw) d.getElementById('wwrap').style.display = "block"; + + var spal = d.getElementById("selectPalette"); + spal.style.backgroundColor = (spal.selectedIndex > 0) ? "var(--c-6)":"var(--c-3)"; + updatePA(); + updateHex(); + updateRgb(); +} + +function displayRover(i,s) +{ + d.getElementById('rover').style.transform = (i.live && s.lor == 0) ? "translateY(0px)":"translateY(100%)"; + var sour = i.lip ? i.lip:""; if (sour.length > 2) sour = " from " + sour; + d.getElementById('lv').innerHTML = `WLED is receiving live ${i.lm} data${sour}`; + d.getElementById('roverstar').style.display = (i.live && s.lor) ? "block":"none"; +} + +function compare(a, b) { + if (a.name < b.name) return -1; + return 1; +} +function cmpP(a, b) { + if (!a[1].n) return (a[0] > b[0]); + return a[1].n.localeCompare(b[1].n,undefined, {numeric: true}); +} + +var jsonTimeout; +function requestJson(command, rinfo = true, verbose = true) { + d.getElementById('connind').style.backgroundColor = "#a90"; + lastUpdate = new Date(); + if (!jsonTimeout) jsonTimeout = setTimeout(showErrorToast, 3000); + var req = null; + var e1 = d.getElementById('fxlist'); + var e2 = d.getElementById('selectPalette'); + + var url = rinfo ? '/json/si': (command ? '/json/state':'/json'); + if (loc) { + url = `http://${locip}${url}`; + } + + var type = command ? 'post':'get'; + if (command) + { + command.v = verbose; + command.time = Math.floor(Date.now() / 1000); + req = JSON.stringify(command); + //console.log(req); + } + fetch + (url, { + method: type, + headers: { + "Content-type": "application/json; charset=UTF-8" + }, + body: req + }) + .then(res => { + if (!res.ok) { + showErrorToast(); + } + return res.json(); + }) + .then(json => { + clearTimeout(jsonTimeout); + jsonTimeout = null; + clearErrorToast(); + d.getElementById('connind').style.backgroundColor = "#070"; + if (!json) showToast('Empty response', true); + if (json.success) return; + var s = json; + if (!command || rinfo) { + if (!rinfo) { + pmt = json.info.fs.pmt; + if (pmt != pmtLS || pmt == 0) { + setTimeout(loadPresets,99); + } + else populatePresets(true); + pmtLast = pmt; + var x='',y=''; + json.effects.shift(); //remove solid + for (let i = 0; i < json.effects.length; i++) json.effects[i] = {id: parseInt(i)+1, name:json.effects[i]}; + json.effects.sort(compare); + for (let i = 0; i < json.effects.length; i++) { + x += `
`; + } + + json.palettes.shift(); //remove default + for (let i = 0; i < json.palettes.length; i++) json.palettes[i] = {"id": parseInt(i)+1, "name":json.palettes[i]}; + json.palettes.sort(compare); + for (let i = 0; i < json.palettes.length; i++) { + y += ``; + } + e1.innerHTML=x; e2.innerHTML=y; + } + + var info = json.info; + var name = info.name; + d.getElementById('namelabel').innerHTML = name; + if (name === "Dinnerbone") d.documentElement.style.transform = "rotate(180deg)"; + if (info.live) name = "(Live) " + name; + if (loc) name = "(L) " + name; + d.title = name; + isRgbw = info.leds.wv; + ledCount = info.leds.count; + syncTglRecv = info.str; + maxSeg = info.leds.maxseg; + pmt = info.fs.pmt; + if (!command && pmt != pmtLast) setTimeout(loadPresets,99); + pmtLast = pmt; + lastinfo = info; + if (isInfo) populateInfo(info); + s = json.state; + displayRover(info, s); + } + isOn = s.on; + d.getElementById('sliderBri').value= s.bri; + nlA = s.nl.on; + nlDur = s.nl.dur; + nlTar = s.nl.tbri; + nlFade = s.nl.fade; + syncSend = s.udpn.send; + currentPreset = s.ps; + d.getElementById('cyToggle').checked = (s.pl < 0) ? false : true; + d.getElementById('cycs').value = s.ccnf.min; + d.getElementById('cyce').value = s.ccnf.max; + d.getElementById('cyct').value = s.ccnf.time /10; + d.getElementById('cyctt').value = s.transition /10; + + var selc=0; var ind=0; + populateSegments(s); + for (let i = 0; i < (s.seg||[]).length; i++) + { + if(s.seg[i].sel) {selc = ind; break;} ind++; + } + var i=s.seg[selc]; + if (!i) { + showToast('No Segments!', true); + updateUI(); + return; + } + var cd = d.getElementById('csl').children; + for (let e = 2; e >= 0; e--) + { + cd[e].style.backgroundColor = "rgb(" + i.col[e][0] + "," + i.col[e][1] + "," + i.col[e][2] + ")"; + if (isRgbw) whites[e] = parseInt(i.col[e][3]); + selectSlot(csel); + } + d.getElementById('sliderSpeed').value = whites[csel]; + + d.getElementById('sliderSpeed').value = i.sx; + d.getElementById('sliderIntensity').value = i.ix; + + d.getElementById('fxb' + selectedFx).style.backgroundColor = "var(--c-3)"; + selectedFx = i.fx; + e2.value = i.pal; + if (!command) d.getElementById('Effects').scrollTop = d.getElementById('fxb' + selectedFx).offsetTop - d.getElementById('Effects').clientHeight/1.8; + + if (s.error && s.error != 0) { + var errstr = ""; + switch (s.error) { + case 10: errstr = "Could not mount filesystem!"; break; + case 11: errstr = "Not enough space to save preset!"; break; + case 12: errstr = "The requested preset does not exist."; break; + case 19: errstr = "A filesystem error has occured."; break; + } + showToast('Error ' + s.error + ": " + errstr, true); + } + updateUI(); + }) + .catch(function (error) { + showToast(error, true); + console.log(error); + }); +} + +function togglePower() { + isOn = !isOn; + var obj = {"on": isOn}; + obj.transition = parseInt(d.getElementById('cyctt').value*10); + requestJson(obj); +} + +function toggleNl() { + nlA = !nlA; + if (nlA) + { + showToast(`Timer active. Your light will turn ${nlTar > 0 ? "on":"off"} ${nlFade ? "over":"after"} ${nlDur} minutes.`); + } else { + showToast('Timer deactivated.'); + } + var obj = {"nl": {"on": nlA}}; + requestJson(obj); +} + +function toggleSync() { + syncSend = !syncSend; + if (syncSend) + { + showToast('Other lights in the network will now sync to this one.'); + } else { + showToast('This light and other lights in the network will no longer sync.'); + } + var obj = {"udpn": {"send": syncSend}}; + if (syncTglRecv) obj.udpn.recv = syncSend; + requestJson(obj); +} + +function toggleLiveview() { + isLv = !isLv; + d.getElementById('liveview').style.display = (isLv) ? "block":"none"; + var url = loc ? `http://${locip}/liveview`:"/liveview"; + d.getElementById('liveview').src = (isLv) ? url:"about:blank"; + d.getElementById('buttonSr').className = (isLv) ? "active":""; + size(); +} + +function toggleInfo() { + isInfo = !isInfo; + if (isInfo) populateInfo(lastinfo); + d.getElementById('info').style.transform = (isInfo) ? "translateY(0px)":"translateY(100%)"; + d.getElementById('buttonI').className = (isInfo) ? "active":""; +} + +function makeSeg() { + var ns = 0; + if (lowestUnused > 0) { + var pend = d.getElementById(`seg${lowestUnused -1}e`).value; + if (pend < ledCount) ns = pend; + } + var cn = `
+
+ New segment ${lowestUnused} +
+
+
+ + + + + + + + + +
Start LEDStop LED
+
${ledCount - ns} LEDs
+ +
+
`; + d.getElementById('segutil').innerHTML = cn; +} + +function resetUtil() { + var cn = `
`; + d.getElementById('segutil').innerHTML = cn; +} + +function makeP(i) { + return ` +
+
Quick load label:
+
(leave empty for no Quick load button)
+
+
+ API command
+ +
+
+ + +
+
Save to ID 0)?i:getLowestUnusedP()}>
+
+ + ${(i>0)?'': + ''} +
+
+ +
+ ${(i>0)? ('
ID ' +i+ '
'):""}`; +} + +function makePUtil() { + d.getElementById('putil').innerHTML = `
+
+ New preset
+
+ ${makeP(0)}
`; + updateTrail(d.getElementById('p0p')); +} + +function resetPUtil() { + var cn = `
`; + d.getElementById('putil').innerHTML = cn; +} + +function tglCs(i){ + var pss = d.getElementById(`p${i}cstgl`).checked; + d.getElementById(`p${i}o1`).style.display = pss? "block" : "none"; + d.getElementById(`p${i}o2`).style.display = !pss? "block" : "none"; +} + +function selSegEx(s) +{ + var obj = {"seg":[]}; + for (let i=0; i<=lSeg; i++){ + obj.seg.push({"sel":(i==s)?true:false}); + } + requestJson(obj); +} + +function selSeg(s){ + var sel = d.getElementById(`seg${s}sel`).checked; + var obj = {"seg": {"id": s, "sel": sel}}; + requestJson(obj, false); +} + +function setSeg(s){ + var start = parseInt(d.getElementById(`seg${s}s`).value); + var stop = parseInt(d.getElementById(`seg${s}e`).value); + if (stop <= start) {delSeg(s); return;} + var obj = {"seg": {"id": s, "start": start, "stop": stop}}; + if (d.getElementById(`seg${s}grp`)) + { + var grp = parseInt(d.getElementById(`seg${s}grp`).value); + var spc = parseInt(d.getElementById(`seg${s}spc`).value); + obj.seg.grp = grp; + obj.seg.spc = spc; + } + requestJson(obj); +} + +function delSeg(s){ + if (segCount < 2) { + showToast("You need to have multiple segments to delete one!"); + return; + } + expanded[s] = false; + segCount--; + var obj = {"seg": {"id": s, "stop": 0}}; + requestJson(obj, false); +} + +function setRev(s){ + var rev = d.getElementById(`seg${s}rev`).checked; + var obj = {"seg": {"id": s, "rev": rev}}; + requestJson(obj, false); +} + +function setMi(s){ + var mi = d.getElementById(`seg${s}mi`).checked; + var obj = {"seg": {"id": s, "mi": mi}}; + requestJson(obj, false); +} + +function setSegPwr(s){ + var obj = {"seg": {"id": s, "on": !powered[s]}}; + requestJson(obj); +} + +function setSegBri(s){ + var obj = {"seg": {"id": s, "bri": parseInt(d.getElementById(`seg${s}bri`).value)}}; + requestJson(obj); +} + +function setX(ind) { + var obj = {"seg": {"fx": parseInt(ind)}}; + requestJson(obj); +} + +function setPalette() +{ + var obj = {"seg": {"pal": parseInt(d.getElementById('selectPalette').value)}}; + requestJson(obj); +} + +function setBri() { + var obj = {"bri": parseInt(d.getElementById('sliderBri').value)}; + obj.transition = parseInt(d.getElementById('cyctt').value*10); + requestJson(obj); +} + +function setSpeed() { + var obj = {"seg": {"sx": parseInt(d.getElementById('sliderSpeed').value)}}; + requestJson(obj, false); +} + +function setIntensity() { + var obj = {"seg": {"ix": parseInt(d.getElementById('sliderIntensity').value)}}; + requestJson(obj, false); +} + +function setLor(i) { + var obj = {"lor": i}; + requestJson(obj); +} + +function toggleCY() { + var obj = {"pl" : -1}; + if (d.getElementById('cyToggle').checked) + { + obj = {"pl": 0, "ccnf": {"min": parseInt(d.getElementById('cycs').value), "max": parseInt(d.getElementById('cyce').value), "time": parseInt(d.getElementById('cyct').value*10)}}; + obj.transition = parseInt(d.getElementById('cyctt').value*10); + } + + requestJson(obj); +} + +function setPreset(i) { + var obj = {"ps": i}; + + showToast("Loading preset " + pName(i) +" (" + i + ")"); + + requestJson(obj); +} + +function saveP(i) { + pI = parseInt(d.getElementById(`p${i}id`).value); + if (!pI || pI < 1) pI = (i>0) ? i : getLowestUnusedP(); + pN = d.getElementById(`p${i}txt`).value; + if (pN == "") pN = "Preset " + pI; + var obj = {}; + if (!d.getElementById(`p${i}cstgl`).checked) { + var raw = d.getElementById(`p${i}api`).value; + try { + obj = JSON.parse(raw); + } catch (e) { + obj.win = raw; + if (raw.length < 2) { + d.getElementById(`p${i}warn`).innerHTML = "⚠ Please enter your API command first"; + return; + } else if (raw.indexOf('{') > -1) { + d.getElementById(`p${i}warn`).innerHTML = "⚠ Syntax error in custom JSON API command"; + return; + } else if (raw.indexOf("Please") == 0) { + d.getElementById(`p${i}warn`).innerHTML = "⚠ Please refresh the page before modifying this preset"; + return; + } + } + obj.o = true; + } else { + obj.ib = d.getElementById(`p${i}ibtgl`).checked; + obj.sb = d.getElementById(`p${i}sbtgl`).checked; + } + obj.psave = pI; obj.n = pN; + var pQN = d.getElementById(`p${i}ql`).value; + if (pQN.length > 0) obj.ql = pQN; + + showToast("Saving " + pN +" (" + pI + ")"); + requestJson(obj); + if (obj.o) { + pJson[pI] = obj; + delete pJson[pI].psave; + delete pJson[pI].o; + delete pJson[pI].v; + delete pJson[pI].time; + } else { + pJson[pI] = {"n":pN, "win":"Please refresh the page to see this newly saved command."}; + if (obj.win) pJson[pI].win = obj.win; + if (obj.ql) pJson[pI].ql = obj.ql; + } + populatePresets(); + resetPUtil(); +} + +function delP(i) { + var obj = {"pdel": i}; + requestJson(obj); + delete pJson[i]; + populatePresets(); +} + +function selectSlot(b) { + csel = b; + var cd = d.getElementById('csl').children; + for (let i = 0; i < cd.length; i++) { + cd[i].style.border="2px solid white"; + cd[i].style.margin="5px"; + cd[i].style.width="42px"; + } + cd[csel].style.border="5px solid white"; + cd[csel].style.margin="2px"; + cd[csel].style.width="50px"; + cpick.color.set(cd[csel].style.backgroundColor); + d.getElementById('sliderW').value = whites[csel]; + updateTrail(d.getElementById('sliderW')); + updateHex(); + updateRgb(); +} + +var lasth = 0; +function pC(col) +{ + if (col == "rnd") + { + col = {h: 0, s: 0, v: 100}; + col.s = Math.floor((Math.random() * 50) + 50); + do { + col.h = Math.floor(Math.random() * 360); + } while (Math.abs(col.h - lasth) < 50); + lasth = col.h; + } + cpick.color.set(col); + setColor(0); +} + +function updateRgb() +{ + var col = cpick.color.rgb; + var s = d.getElementById('sliderR'); + s.value = col.r; updateTrail(s,1); + s = d.getElementById('sliderG'); + s.value = col.g; updateTrail(s,2); + s = d.getElementById('sliderB'); + s.value = col.b; updateTrail(s,3); +} + +function updateHex() +{ + var str = cpick.color.hexString; + str = str.substring(1); + var w = whites[csel]; + if (w > 0) str += w.toString(16); + d.getElementById('hexc').value = str; + d.getElementById('hexcnf').style.backgroundColor = "var(--c-3)"; +} + +function hexEnter() { + d.getElementById('hexcnf').style.backgroundColor = "var(--c-6)"; + if(event.keyCode == 13) fromHex(); +} + +function fromHex() +{ + var str = d.getElementById('hexc').value; + whites[csel] = parseInt(str.substring(6), 16); + try { + cpick.color.set("#" + str.substring(0,6)); + } catch (e) { + cpick.color.set("#ffaa00"); + } + if (isNaN(whites[csel])) whites[csel] = 0; + setColor(2); +} + +function fromRgb() +{ + var r = d.getElementById('sliderR').value; + var g = d.getElementById('sliderG').value; + var b = d.getElementById('sliderB').value; + cpick.color.set(`rgb(${r},${g},${b})`); + setColor(0); +} + +function setColor(sr) { + var cd = d.getElementById('csl').children; + if (sr == 1 && cd[csel].style.backgroundColor == 'rgb(0, 0, 0)') cpick.color.setChannel('hsv', 'v', 100); + cd[csel].style.backgroundColor = cpick.color.rgbString; + if (sr != 2) whites[csel] = d.getElementById('sliderW').value; + var col = cpick.color.rgb; + var obj = {"seg": {"col": [[col.r, col.g, col.b, whites[csel]],[],[]]}}; + if (csel == 1) { + obj = {"seg": {"col": [[],[col.r, col.g, col.b, whites[csel]],[]]}}; + } else if (csel == 2) { + obj = {"seg": {"col": [[],[],[col.r, col.g, col.b, whites[csel]]]}}; + } + updateHex(); + updateRgb(); + obj.transition = parseInt(d.getElementById('cyctt').value*10); + requestJson(obj); +} + +var hc = 0; +setInterval(function(){if (!isInfo) return; hc+=18; if (hc>300) hc=0; if (hc>200)hc=306; if (hc==144) hc+=36; if (hc==108) hc+=18; +d.getElementById('heart').style.color = `hsl(${hc}, 100%, 50%)`;}, 910); + +function openGH() +{ + window.open("https://github.com/Aircoookie/WLED/wiki"); +} + +var cnfr = false; +function cnfReset() +{ + if (!cnfr) + { + var bt = d.getElementById('resetbtn'); + bt.style.color = "#f00"; + bt.innerHTML = "Confirm Reboot"; + cnfr = true; return; + } + window.location.href = "/reset"; +} + +var cnfrS = false; +function rSegs() +{ + var bt = d.getElementById('rsbtn'); + if (!cnfrS) + { + bt.style.color = "#f00"; + bt.innerHTML = "Confirm reset"; + cnfrS = true; return; + } + cnfrS = false; + bt.style.color = "#fff"; + bt.innerHTML = "Reset segments"; + var obj = {"seg":[{"start":0,"stop":ledCount,"sel":true}]}; + for (let i=1; i<=lSeg; i++){ + obj.seg.push({"stop":0}); + } + requestJson(obj); +} + +function expand(i,a) +{ + if (!a) expanded[i] = !expanded[i]; + d.getElementById('seg' +i).style.display = (expanded[i]) ? "block":"none"; + d.getElementById('sege' +i).style.transform = (expanded[i]) ? "rotate(180deg)":"rotate(0deg)"; + if (i > 100) { //presets + var p = i-100; + d.getElementById(`p${p}o`).style.background = (expanded[i] || p != currentPreset)?"var(--c-2)":"var(--c-6)"; + if (d.getElementById('seg' +i).innerHTML == "") { + d.getElementById('seg' +i).innerHTML = makeP(p); + var papi = papiVal(p); + d.getElementById(`p${p}api`).value = papi; + if (papi.indexOf("Please") == 0) d.getElementById(`p${p}cstgl`).checked = true; + tglCs(p); + } + } +} + +function unfocusSliders() { + d.getElementById("sliderBri").blur(); + d.getElementById("sliderSpeed").blur(); + d.getElementById("sliderIntensity").blur(); +} + +//sliding UI +const _C = document.querySelector('.container'), N = 4; + +let iSlide = 0, x0 = null, scrollS = 0, locked = false, w; + +function unify(e) { return e.changedTouches ? e.changedTouches[0] : e; } + +function hasIroClass(classList) { + for (var i = 0; i < classList.length; i++) { + var element = classList[i]; + if (element.startsWith('Iro')) return true; + } + + return false; +} + + +function lock(e) { + if (pcMode) return; + var l = e.target.classList; + var pl = e.target.parentElement.classList; + + if (l.contains('noslide') || hasIroClass(l) || hasIroClass(pl)) return; + + x0 = unify(e).clientX; + scrollS = d.getElementsByClassName("tabcontent")[iSlide].scrollTop; + + _C.classList.toggle('smooth', !(locked = true)); +} + +function move(e) { + if(!locked || pcMode) return; + var dx = unify(e).clientX - x0, s = Math.sign(dx), + f = +(s*dx/w).toFixed(2); + + if((iSlide > 0 || s < 0) && (iSlide < N - 1 || s > 0) && + f > 0.12 && + d.getElementsByClassName("tabcontent")[iSlide].scrollTop == scrollS) { + _C.style.setProperty('--i', iSlide -= s); + f = 1 - f; + updateTablinks(iSlide); + } + _C.style.setProperty('--f', f); + _C.classList.toggle('smooth', !(locked = false)); + x0 = null; +} + +function size() { + w = window.innerWidth; + var h = d.getElementById('top').clientHeight; + sCol('--th', h + "px"); + sCol('--bh', d.getElementById('bot').clientHeight + "px"); + if (isLv) h -= 4; + sCol('--tp', h + "px"); + togglePcMode(); +} + +function togglePcMode(fromB = false) +{ + if (fromB) { + pcModeA = !pcModeA; + localStorage.setItem('pcm', pcModeA); + pcMode = pcModeA; + } + if (w < 1250 && !pcMode) return; + if (!fromB && ((w < 1250 && lastw < 1250) || (w >= 1250 && lastw >= 1250))) return; + openTab(0, true); + if (w < 1250) {pcMode = false;} + else if (pcModeA && !fromB) pcMode = pcModeA; + updateTablinks(0); + d.getElementById('buttonPcm').className = (pcMode) ? "active":""; + d.getElementById('bot').style.height = (pcMode && !cfg.comp.pcmbot) ? "0":"auto"; + sCol('--bh', d.getElementById('bot').clientHeight + "px"); + _C.style.width = (pcMode)?'100%':'400%'; + lastw = w; +} + +function isObject(item) { + return (item && typeof item === 'object' && !Array.isArray(item)); +} + +function mergeDeep(target, ...sources) { + if (!sources.length) return target; + const source = sources.shift(); + + if (isObject(target) && isObject(source)) { + for (const key in source) { + if (isObject(source[key])) { + if (!target[key]) Object.assign(target, { [key]: {} }); + mergeDeep(target[key], source[key]); + } else { + Object.assign(target, { [key]: source[key] }); + } + } + } + return mergeDeep(target, ...sources); +} + +size(); +_C.style.setProperty('--n', N); + +window.addEventListener('resize', size, false); + +_C.addEventListener('mousedown', lock, false); +_C.addEventListener('touchstart', lock, false); + +_C.addEventListener('mouseout', move, false); +_C.addEventListener('mouseup', move, false); +_C.addEventListener('touchend', move, false); \ No newline at end of file diff --git a/wled00/data/iro.js b/wled00/data/iro.js new file mode 100644 index 00000000..f459e417 --- /dev/null +++ b/wled00/data/iro.js @@ -0,0 +1,7 @@ +/*! + * iro.js v5.3.1 + * 2016-2020 James Daniel + * Licensed under MPL 2.0 + * github.com/jaames/iro.js + */ +!function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(t=t||self).iro=n()}(this,function(){"use strict";var k,s,n,i,o,m={},M=[],r=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|^--/i;function j(t,n){for(var i in n)t[i]=n[i];return t}function b(t){var n=t.parentNode;n&&n.removeChild(t)}function d(t,n,i){var r,e,u,o,l=arguments;if(n=j({},n),3=r/i?u=n:e=n}return n},function(t,n,i){n&&g(t.prototype,n),i&&g(t,i)}(l,[{key:"hsv",get:function(){var t=this.$;return{h:t.h,s:t.s,v:t.v}},set:function(t){var n=this.$;if(t=p({},n,t),this.onChange){var i={h:!1,v:!1,s:!1,a:!1};for(var r in n)i[r]=t[r]!=n[r];this.$=t,(i.h||i.s||i.v||i.a)&&this.onChange(this,i)}else this.$=t}},{key:"hsva",get:function(){return p({},this.$)},set:function(t){this.hsv=t}},{key:"hue",get:function(){return this.$.h},set:function(t){this.hsv={h:t}}},{key:"saturation",get:function(){return this.$.s},set:function(t){this.hsv={s:t}}},{key:"value",get:function(){return this.$.v},set:function(t){this.hsv={v:t}}},{key:"alpha",get:function(){return this.$.a},set:function(t){this.hsv=p({},this.hsv,{a:t})}},{key:"kelvin",get:function(){return l.rgbToKelvin(this.rgb)},set:function(t){this.rgb=l.kelvinToRgb(t)}},{key:"red",get:function(){return this.rgb.r},set:function(t){this.rgb=p({},this.rgb,{r:t})}},{key:"green",get:function(){return this.rgb.g},set:function(t){this.rgb=p({},this.rgb,{g:t})}},{key:"blue",get:function(){return this.rgb.b},set:function(t){this.rgb=p({},this.rgb,{b:t})}},{key:"rgb",get:function(){var t=l.hsvToRgb(this.$),n=t.r,i=t.g,r=t.b;return{r:U(n),g:U(i),b:U(r)}},set:function(t){this.hsv=p({},l.rgbToHsv(t),{a:void 0===t.a?1:t.a})}},{key:"rgba",get:function(){return p({},this.rgb,{a:this.alpha})},set:function(t){this.rgb=t}},{key:"hsl",get:function(){var t=l.hsvToHsl(this.$),n=t.h,i=t.s,r=t.l;return{h:U(n),s:U(i),l:U(r)}},set:function(t){this.hsv=p({},l.hslToHsv(t),{a:void 0===t.a?1:t.a})}},{key:"hsla",get:function(){return p({},this.hsl,{a:this.alpha})},set:function(t){this.hsl=t}},{key:"rgbString",get:function(){var t=this.rgb;return"rgb("+t.r+", "+t.g+", "+t.b+")"},set:function(t){var n,i,r,e,u=1;if((n=P.exec(t))?(i=K(n[1],255),r=K(n[2],255),e=K(n[3],255)):(n=z.exec(t))&&(i=K(n[1],255),r=K(n[2],255),e=K(n[3],255),u=K(n[4],1)),!n)throw new Error("Invalid rgb string");this.rgb={r:i,g:r,b:e,a:u}}},{key:"rgbaString",get:function(){var t=this.rgba;return"rgba("+t.r+", "+t.g+", "+t.b+", "+t.a+")"},set:function(t){this.rgbString=t}},{key:"hexString",get:function(){var t=this.rgb;return"#"+V(t.r)+V(t.g)+V(t.b)},set:function(t){var n,i,r,e,u=255;if((n=C.exec(t))?(i=17*Q(n[1]),r=17*Q(n[2]),e=17*Q(n[3])):(n=D.exec(t))?(i=17*Q(n[1]),r=17*Q(n[2]),e=17*Q(n[3]),u=17*Q(n[4])):(n=F.exec(t))?(i=Q(n[1]),r=Q(n[2]),e=Q(n[3])):(n=G.exec(t))&&(i=Q(n[1]),r=Q(n[2]),e=Q(n[3]),u=Q(n[4])),!n)throw new Error("Invalid hex string");this.rgb={r:i,g:r,b:e,a:u/255}}},{key:"hex8String",get:function(){var t=this.rgba;return"#"+V(t.r)+V(t.g)+V(t.b)+V(q(255*t.a))},set:function(t){this.hexString=t}},{key:"hslString",get:function(){var t=this.hsl;return"hsl("+t.h+", "+t.s+"%, "+t.l+"%)"},set:function(t){var n,i,r,e,u=1;if((n=H.exec(t))?(i=K(n[1],360),r=K(n[2],100),e=K(n[3],100)):(n=$.exec(t))&&(i=K(n[1],360),r=K(n[2],100),e=K(n[3],100),u=K(n[4],1)),!n)throw new Error("Invalid hsl string");this.hsl={h:i,s:r,l:e,a:u}}},{key:"hslaString",get:function(){var t=this.hsla;return"hsl("+t.h+", "+t.s+"%, "+t.l+"%, "+t.a+")"},set:function(t){this.hslString=t}}]),l}();function Z(t){var n,i=t.width,r=t.sliderSize,e=t.borderWidth,u=t.handleRadius,o=t.padding,l=t.sliderShape,s="horizontal"===t.layoutDirection;return r=null!=(n=r)?n:2*o+2*u+2*e,"circle"===l?{handleStart:t.padding+t.handleRadius,handleRange:i-2*o-2*u-2*e,width:i,height:i,cx:i/2,cy:i/2,radius:i/2-e/2}:{handleStart:r/2,handleRange:i-r,radius:r/2,x:0,y:0,width:s?r:i,height:s?i:r}}function tt(t,n){var i=Z(t),r=i.width,e=i.height,u=i.handleRange,o=i.handleStart,l="horizontal"===t.layoutDirection,s=l?r/2:e/2,c=o+function(t,n){var i=n.hsva,r=n.rgb;switch(t.sliderType){case"red":return r.r/2.55;case"green":return r.g/2.55;case"blue":return r.b/2.55;case"alpha":return 100*i.a;case"kelvin":var e=t.minTemperature,u=t.maxTemperature-e,o=(n.kelvin-e)/u*100;return Math.max(0,Math.min(o,100));case"hue":return i.h/=3.6;case"saturation":return i.s;case"value":default:return i.v}}(t,n)/100*u;return l&&(c=-1*c+u+2*o),{x:l?s:c,y:l?c:s}}function nt(t){var n=t.width/2;return{width:t.width,radius:n-t.borderWidth,cx:n,cy:n}}function it(t,n,i){var r=t.wheelAngle,e=t.wheelDirection;return((n=!i&&"clockwise"===e||i&&"anticlockwise"===e?(i?180:360)-(r-n):r+n)%360+360)%360}function rt(t,n,i){var r=nt(t),e=r.cx,u=r.cy,o=t.width/2-t.padding-t.handleRadius-t.borderWidth;n=e-n,i=u-i;var l=it(t,Math.atan2(-i,-n)*(180/Math.PI)),s=Math.min(Math.sqrt(n*n+i*i),o);return{h:Math.round(l),s:Math.round(100/o*s)}}function et(t){var n=t.width,i=t.boxHeight;return{width:n,height:null!=i?i:n,radius:t.padding+t.handleRadius}}function ut(t,n,i){var r=et(t),e=r.width,u=r.height,o=r.radius,l=(n-o)/(e-2*o)*100,s=(i-o)/(u-2*o)*100;return{s:Math.max(0,Math.min(l,100)),v:Math.max(0,Math.min(100-s,100))}}function ot(t){X=X||document.getElementsByTagName("base");var n=window.navigator.userAgent,i=/^((?!chrome|android).)*safari/i.test(n),r=/iPhone|iPod|iPad/i.test(n),e=window.location;return(i||r)&&0 work on touch devices +// https://github.com/sampotts/rangetouch +// License: The MIT License (MIT) +// ========================================================================== +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define("RangeTouch",t):(e=e||self).RangeTouch=t()}(this,(function(){"use strict";function e(e,t){for(var n=0;nt){var n=function(e){var t="".concat(e).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/);return t?Math.max(0,(t[1]?t[1].length:0)-(t[2]?+t[2]:0)):0}(t);return parseFloat(e.toFixed(n))}return Math.round(e/t)*t}return function(){function t(e,n){(function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")})(this,t),m(e)?this.element=e:d(e)&&(this.element=document.querySelector(e)),m(this.element)&&p(this.element.rangeTouch)&&(this.config=r({},i,{},n),this.init())}return n=t,c=[{key:"setup",value:function(e){var n=1(n=100/l.width*(i.clientX-l.left))?n=0:100n?n-=(100-2*n)*a:50 - - + + WLED Settings -

WLED Software Update

Installed version: ##VERSION##
Download the latest binary:
-

+

WLED Software Update

+
+ Installed version: ##VERSION##
+ Download the latest binary: +
+
+
+
+
Updating...
Please do not close or refresh the page :)
\ No newline at end of file diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index bbf228d8..d52d5edf 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -15,7 +15,7 @@ void handleAlexa(); void onAlexaChange(EspalexaDevice* dev); //blynk.cpp -void initBlynk(const char* auth); +void initBlynk(const char* auth, const char* host, uint16_t port); void handleBlynk(); void updateBlynk(); @@ -240,6 +240,7 @@ void userLoop(); void applyMacro(byte index); void deEEP(); void deEEPSettings(); +void clearEEPROM(); //wled_serial.cpp void handleSerial(); diff --git a/wled00/html_other.h b/wled00/html_other.h index c363f6b3..5cf7ec64 100644 --- a/wled00/html_other.h +++ b/wled00/html_other.h @@ -36,17 +36,19 @@ const char PAGE_dmxmap[] PROGMEM = R"=====()====="; // Autogenerated from wled00/data/update.htm, do not edit!! const char PAGE_update[] PROGMEM = R"=====( -WLED Update -

WLED Software Update

Installed version: 0.11.0
-Download the latest binary: WLED Update

WLED Software Update

+Installed version: 0.11.1
Download the latest binary:
-
-
)====="; +

+
Updating... +
Please do not close or refresh the page :)
)====="; // Autogenerated from wled00/data/welcome.htm, do not edit!! diff --git a/wled00/html_settings.h b/wled00/html_settings.h index 23b840ca..d6d72cb1 100644 --- a/wled00/html_settings.h +++ b/wled00/html_settings.h @@ -10,9 +10,9 @@ const char PAGE_settingsCss[] PROGMEM = R"=====(
WiFi Settings