diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index ac2a12149e..7ec12365e8 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -45,7 +45,7 @@ body: attributes: label: What version of WLED? description: You can find this in by going to Config -> Security & Updates -> Scroll to Bottom. Copy and paste the entire line after "Server message" - placeholder: "e.g. WLED 0.13.1 (build 2203150)" + placeholder: "e.g. SR WLED 0.13.2 (build 2207281)" validations: required: true - type: dropdown diff --git a/.gitignore b/.gitignore index 47c1f0feab..02e648b88b 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,5 @@ .gitignore .clang-format node_modules -.cproject -.project .idea .direnv diff --git a/CHANGELOG.md b/CHANGELOG.md index f22224fce3..3b7c2149f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ ## WLED changelog +### Builds after release 0.13.1 + +#### Build 2203191 + +- Fixed sunrise/set calculation (once again) + +#### Build 2203190 + +- Fixed `/json/cfg` unable to set busses (#2589) +- Fixed Peek with odd LED counts > 255 (#2586) + +#### Build 2203160 + +- Version bump to v0.13.2-a0 "Toki" +- Add ability to skip up to 255 LEDs +- Dependency version bumps + ### WLED release 0.13.1 #### Build 2203150 @@ -27,7 +44,7 @@ #### Build 2203080 - Disabled auto white mode in segments with no RGB bus -- Fixed hostname string not 0-terminated +- Fixed hostname string not 0-terminated - Fixed Popcorn mode not lighting first LED on pop #### Build 2203060 @@ -80,7 +97,7 @@ - Initial ESP32-C3 and ESP32-S2 support (PRs #2452, #2454, #2502) - Full segment sync (PR #2427) -- Allow overriding of color order by ranges (PR #2463) +- Allow overriding of color order by ranges (PR #2463) - Added white channel to Peek #### Build 2112080 diff --git a/package-lock.json b/package-lock.json index ed4c55bb02..6fba6589a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wled", - "version": "0.13.1", + "version": "0.13.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "wled", - "version": "0.13.1", + "version": "0.13.2", "license": "ISC", "dependencies": { "clean-css": "^4.2.3", diff --git a/package.json b/package.json index 44b10185ed..7c8f4e128b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wled", - "version": "0.13.1", + "version": "0.13.2", "description": "Tools for WLED project", "main": "tools/cdata.js", "directories": { diff --git a/platformio.ini b/platformio.ini index 7d49c07826..50004f3dba 100644 --- a/platformio.ini +++ b/platformio.ini @@ -165,7 +165,7 @@ upload_speed = 115200 lib_compat_mode = strict lib_deps = fastled/FastLED @ 3.5.0 - IRremoteESP8266 @ 2.8.1 + IRremoteESP8266 @ 2.8.2 https://github.com/Aircoookie/ESPAsyncWebServer.git @ ~2.0.4 #For use of the TTGO T-Display ESP32 Module with integrated TFT display uncomment the following line #TFT_eSPI @@ -203,9 +203,9 @@ build_flags = lib_deps = ${env.lib_deps} #https://github.com/lorol/LITTLEFS.git - # ESPAsyncTCP @ 1.2.0 + ESPAsyncTCP @ 1.2.2 ESPAsyncUDP - makuna/NeoPixelBus @ 2.6.7 # 2.6.5/2.6.6 and newer do not compile on ESP core < 3.0.0 + makuna/NeoPixelBus @ 2.6.9 [esp32] #platform = https://github.com/tasmota/platform-espressif32/releases/download/v2.0.2.3/platform-espressif32-2.0.2.3.zip @@ -224,8 +224,10 @@ default_partitions = tools/WLED_ESP32_4MB_1MB_FS.csv lib_deps = ${env.lib_deps} - https://github.com/lorol/LITTLEFS.git - makuna/NeoPixelBus @ 2.6.7 + ; https://github.com/lorol/LITTLEFS.git + ; WLEDSR specific: use patched version of lorol LittleFS + https://github.com/softhack007/LITTLEFS-threadsafe.git#master + makuna/NeoPixelBus @ 2.6.9 https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 arduinoFFT @ 1.5.6 @@ -239,7 +241,7 @@ build_flags = -g lib_deps = ${env.lib_deps} - makuna/NeoPixelBus @ 2.6.7 + makuna/NeoPixelBus @ 2.6.9 https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 [esp32c3] @@ -252,7 +254,7 @@ build_flags = -g lib_deps = ${env.lib_deps} - makuna/NeoPixelBus @ 2.6.7 + makuna/NeoPixelBus @ 2.6.9 https://github.com/pbolduc/AsyncTCP.git @ 1.2.0 # ------------------------------------------------------------------------------ @@ -332,6 +334,8 @@ build_unflags = ${common.build_unflags} build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32 #-D WLED_DISABLE_BLYNK #-D WLED_DISABLE_BROWNOUT_DET upload_speed = 921600 lib_deps = ${esp32.lib_deps} +monitor_filters = esp32_exception_decoder +board_build.partitions = ${esp32.default_partitions} [env:elementv2] board = esp32dev @@ -588,6 +592,15 @@ board_build.ldscript = ${common.ldscript_2m512k} build_flags = ${common.build_flags_esp8266} -D WLED_USE_IC_CCT -D BTNPIN=-1 -D IRPIN=-1 -D WLED_DISABLE_INFRARED lib_deps = ${esp8266.lib_deps} +[env:MY9291] +board = esp01_1m +platform = ${common.platform_wled_default} +platform_packages = ${common.platform_packages} +board_build.ldscript = ${common.ldscript_1m128k} +build_unflags = ${common.build_unflags} +build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP01 -D WLED_DISABLE_OTA -D USERMOD_MY9291 +lib_deps = ${esp8266.lib_deps} + # ------------------------------------------------------------------------------ # travis test board configurations # ------------------------------------------------------------------------------ diff --git a/platformio_override.ini.sample b/platformio_override.ini.sample index 422a5f7e96..0556aa7add 100644 --- a/platformio_override.ini.sample +++ b/platformio_override.ini.sample @@ -22,7 +22,7 @@ build_flags = ${common.build_flags_esp8266} ; ; ; *** To use the below defines/overrides, copy and paste each onto it's own line just below build_flags in the section above. -; +; ; disable specific features ; -D WLED_DISABLE_OTA ; -D WLED_DISABLE_ALEXA @@ -49,15 +49,18 @@ build_flags = ${common.build_flags_esp8266} ; for the Magic Home LED Controller use PWM pins 5,12,13,15 ; for the H801 controller use PINs 15,13,12,14 (W2 = 04) ; for the BW-LT11 controller use PINs 12,4,14,5 -; +; ; set the name of the module - make sure there is a quote-backslash-quote before the name and a backslash-quote-quote after the name ; -D SERVERNAME="\"WLED\"" -; +; ; set the number of LEDs ; -D DEFAULT_LED_COUNT=30 -; +; ; set milliampere limit when using ESP pin to power leds -; -D ABL_MILLIAMPS_DEFAULT =850 +; -D ABL_MILLIAMPS_DEFAULT=850 ; ; enable IR by setting remote type -; -D IRTYPE=0 //0 Remote disabled | 1 24-key RGB | 2 24-key with CT | 3 40-key blue | 4 40-key RGB | 5 21-key RGB | 6 6-key black | 7 9-key red | 8 JSON remote +; -D IRTYPE=0 ;0 Remote disabled | 1 24-key RGB | 2 24-key with CT | 3 40-key blue | 4 40-key RGB | 5 21-key RGB | 6 6-key black | 7 9-key red | 8 JSON remote +; +; set default color order of your led strip +; -D DEFAULT_LED_COLOR_ORDER=COL_ORDER_GRB diff --git a/requirements.txt b/requirements.txt index c3ed11fc16..06b2d535e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ aiofiles==0.6.0 # via platformio ajsonrpc==1.1.0 # via platformio -bottle==0.12.19 +bottle==0.12.20 # via platformio certifi==2020.12.5 # via requests diff --git a/tools/cdata.js b/tools/cdata.js index 13994bc801..6508772e72 100644 --- a/tools/cdata.js +++ b/tools/cdata.js @@ -438,6 +438,14 @@ const char PAGE_dmxmap[] PROGMEM = R"=====()====="; method: "plaintext", filter: "html-minify", }, + { + file: "liveviewws2D.htm", + name: "PAGE_liveviewws2D", + prepend: "=====(", + append: ")=====", + method: "plaintext", + filter: "html-minify", + }, { file: "404.htm", name: "PAGE_404", diff --git a/usermods/EXAMPLE_v2/usermod_v2_example.h b/usermods/EXAMPLE_v2/usermod_v2_example.h index 48fb7cd8e8..a4fe938931 100644 --- a/usermods/EXAMPLE_v2/usermod_v2_example.h +++ b/usermods/EXAMPLE_v2/usermod_v2_example.h @@ -207,6 +207,17 @@ class MyExampleUsermod : public Usermod { return configComplete; } + + /* + * handleOverlayDraw() is called just before every show() (LED strip update frame) after effects have set the colors. + * Use this to blank out some LEDs or set them to a different color regardless of the set effect mode. + * Commonly used for custom clocks (Cronixie, 7 segment) + */ + void handleOverlayDraw() + { + //strip.setPixelColor(0, RGBW32(0,0,0,0)) // set the first pixel to black + } + /* * getId() allows you to optionally give your V2 usermod an unique ID (please define it in const.h!). diff --git a/usermods/MY9291/MY92xx.h b/usermods/MY9291/MY92xx.h new file mode 100644 index 0000000000..658852b446 --- /dev/null +++ b/usermods/MY9291/MY92xx.h @@ -0,0 +1,321 @@ +/* + +MY92XX LED Driver for Arduino +Based on the C driver by MaiKe Labs + +Copyright (c) 2016 - 2026 MaiKe Labs +Copyright (C) 2017 - 2018 Xose Pérez for the Arduino compatible library + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +*/ + +#ifndef _my92xx_h +#define _my92xx_h + +#include + +#ifdef DEBUG_MY92XX +#if ARDUINO_ARCH_ESP8266 +#define DEBUG_MSG_MY92XX(...) DEBUG_MY92XX.printf( __VA_ARGS__ ) +#elif ARDUINO_ARCH_AVR +#define DEBUG_MSG_MY92XX(...) { char buffer[80]; snprintf(buffer, sizeof(buffer), __VA_ARGS__ ); DEBUG_MY92XX.print(buffer); } +#endif +#else +#define DEBUG_MSG_MY92XX(...) +#endif + +typedef enum my92xx_model_t { + MY92XX_MODEL_MY9291 = 0X00, + MY92XX_MODEL_MY9231 = 0X01, +} my92xx_model_t; + +typedef enum my92xx_cmd_one_shot_t { + MY92XX_CMD_ONE_SHOT_DISABLE = 0X00, + MY92XX_CMD_ONE_SHOT_ENFORCE = 0X01, +} my92xx_cmd_one_shot_t; + +typedef enum my92xx_cmd_reaction_t { + MY92XX_CMD_REACTION_FAST = 0X00, + MY92XX_CMD_REACTION_SLOW = 0X01, +} my92xx_cmd_reaction_t; + +typedef enum my92xx_cmd_bit_width_t { + MY92XX_CMD_BIT_WIDTH_16 = 0X00, + MY92XX_CMD_BIT_WIDTH_14 = 0X01, + MY92XX_CMD_BIT_WIDTH_12 = 0X02, + MY92XX_CMD_BIT_WIDTH_8 = 0X03, +} my92xx_cmd_bit_width_t; + +typedef enum my92xx_cmd_frequency_t { + MY92XX_CMD_FREQUENCY_DIVIDE_1 = 0X00, + MY92XX_CMD_FREQUENCY_DIVIDE_4 = 0X01, + MY92XX_CMD_FREQUENCY_DIVIDE_16 = 0X02, + MY92XX_CMD_FREQUENCY_DIVIDE_64 = 0X03, +} my92xx_cmd_frequency_t; + +typedef enum my92xx_cmd_scatter_t { + MY92XX_CMD_SCATTER_APDM = 0X00, + MY92XX_CMD_SCATTER_PWM = 0X01, +} my92xx_cmd_scatter_t; + +typedef struct { + my92xx_cmd_scatter_t scatter : 1; + my92xx_cmd_frequency_t frequency : 2; + my92xx_cmd_bit_width_t bit_width : 2; + my92xx_cmd_reaction_t reaction : 1; + my92xx_cmd_one_shot_t one_shot : 1; + unsigned char resv : 1; +} __attribute__((aligned(1), packed)) my92xx_cmd_t; + +#define MY92XX_COMMAND_DEFAULT { \ + .scatter = MY92XX_CMD_SCATTER_APDM, \ + .frequency = MY92XX_CMD_FREQUENCY_DIVIDE_1, \ + .bit_width = MY92XX_CMD_BIT_WIDTH_8, \ + .reaction = MY92XX_CMD_REACTION_FAST, \ + .one_shot = MY92XX_CMD_ONE_SHOT_DISABLE, \ + .resv = 0 \ +} + +class my92xx { + +public: + + my92xx(my92xx_model_t model, unsigned char chips, unsigned char di, unsigned char dcki, my92xx_cmd_t command); + unsigned char getChannels(); + void setChannel(unsigned char channel, unsigned int value); + unsigned int getChannel(unsigned char channel); + void setState(bool state); + bool getState(); + void update(); + +private: + + void _di_pulse(unsigned int times); + void _dcki_pulse(unsigned int times); + void _set_cmd(my92xx_cmd_t command); + void _send(); + void _write(unsigned int data, unsigned char bit_length); + + my92xx_cmd_t _command; + my92xx_model_t _model = MY92XX_MODEL_MY9291; + unsigned char _chips = 1; + unsigned char _channels; + uint16_t* _value; + bool _state = false; + unsigned char _pin_di; + unsigned char _pin_dcki; + + +}; + + +#if ARDUINO_ARCH_ESP8266 + +extern "C" { + void os_delay_us(unsigned int); +} + +#elif ARDUINO_ARCH_AVR + +#define os_delay_us delayMicroseconds + +#endif + +void my92xx::_di_pulse(unsigned int times) { + for (unsigned int i = 0; i < times; i++) { + digitalWrite(_pin_di, HIGH); + digitalWrite(_pin_di, LOW); + } +} + +void my92xx::_dcki_pulse(unsigned int times) { + for (unsigned int i = 0; i < times; i++) { + digitalWrite(_pin_dcki, HIGH); + digitalWrite(_pin_dcki, LOW); + } +} + +void my92xx::_write(unsigned int data, unsigned char bit_length) { + + unsigned int mask = (0x01 << (bit_length - 1)); + + for (unsigned int i = 0; i < bit_length / 2; i++) { + digitalWrite(_pin_dcki, LOW); + digitalWrite(_pin_di, (data & mask) ? HIGH : LOW); + digitalWrite(_pin_dcki, HIGH); + data = data << 1; + digitalWrite(_pin_di, (data & mask) ? HIGH : LOW); + digitalWrite(_pin_dcki, LOW); + digitalWrite(_pin_di, LOW); + data = data << 1; + } + +} + +void my92xx::_set_cmd(my92xx_cmd_t command) { + + // ets_intr_lock(); + + // TStop > 12us. + os_delay_us(12); + + // Send 12 DI pulse, after 6 pulse's falling edge store duty data, and 12 + // pulse's rising edge convert to command mode. + _di_pulse(12); + + // Delay >12us, begin send CMD data + os_delay_us(12); + + // Send CMD data + unsigned char command_data = *(unsigned char*)(&command); + for (unsigned char i = 0; i < _chips; i++) { + _write(command_data, 8); + } + + // TStart > 12us. Delay 12 us. + os_delay_us(12); + + // Send 16 DI pulse,at 14 pulse's falling edge store CMD data, and + // at 16 pulse's falling edge convert to duty mode. + _di_pulse(16); + + // TStop > 12us. + os_delay_us(12); + + // ets_intr_unlock(); + +} + +void my92xx::_send() { + +#ifdef DEBUG_MY92XX + DEBUG_MSG_MY92XX("[MY92XX] Refresh: %s (", _state ? "ON" : "OFF"); + for (unsigned char channel = 0; channel < _channels; channel++) { + DEBUG_MSG_MY92XX(" %d", _value[channel]); + } + DEBUG_MSG_MY92XX(" )\n"); +#endif + + unsigned char bit_length = 8; + switch (_command.bit_width) { + case MY92XX_CMD_BIT_WIDTH_16: + bit_length = 16; + break; + case MY92XX_CMD_BIT_WIDTH_14: + bit_length = 14; + break; + case MY92XX_CMD_BIT_WIDTH_12: + bit_length = 12; + break; + case MY92XX_CMD_BIT_WIDTH_8: + bit_length = 8; + break; + default: + bit_length = 8; + break; + } + + // ets_intr_lock(); + + // TStop > 12us. + os_delay_us(12); + + // Send color data + for (unsigned char channel = 0; channel < _channels; channel++) { + _write(_state ? _value[channel] : 0, bit_length); + } + + // TStart > 12us. Ready for send DI pulse. + os_delay_us(12); + + // Send 8 DI pulse. After 8 pulse falling edge, store old data. + _di_pulse(8); + + // TStop > 12us. + os_delay_us(12); + + // ets_intr_unlock(); + +} + +// ----------------------------------------------------------------------------- + +unsigned char my92xx::getChannels() { + return _channels; +} + +void my92xx::setChannel(unsigned char channel, unsigned int value) { + if (channel < _channels) { + _value[channel] = value; + } +} + +unsigned int my92xx::getChannel(unsigned char channel) { + if (channel < _channels) { + return _value[channel]; + } + return 0; +} + +bool my92xx::getState() { + return _state; +} + +void my92xx::setState(bool state) { + _state = state; +} + +void my92xx::update() { + _send(); +} + +// ----------------------------------------------------------------------------- + +my92xx::my92xx(my92xx_model_t model, unsigned char chips, unsigned char di, unsigned char dcki, my92xx_cmd_t command) : _command(command) { + + _model = model; + _chips = chips; + _pin_di = di; + _pin_dcki = dcki; + + // Init channels + if (_model == MY92XX_MODEL_MY9291) { + _channels = 4 * _chips; + } + else if (_model == MY92XX_MODEL_MY9231) { + _channels = 3 * _chips; + } + _value = new uint16_t[_channels]; + for (unsigned char i = 0; i < _channels; i++) { + _value[i] = 0; + } + + // Init GPIO + pinMode(_pin_di, OUTPUT); + pinMode(_pin_dcki, OUTPUT); + digitalWrite(_pin_di, LOW); + digitalWrite(_pin_dcki, LOW); + + // Clear all duty register + _dcki_pulse(32 * _chips); + + // Send init command + _set_cmd(command); + + DEBUG_MSG_MY92XX("[MY92XX] Initialized\n"); + +} + +#endif \ No newline at end of file diff --git a/usermods/MY9291/usermode_MY9291.h b/usermods/MY9291/usermode_MY9291.h new file mode 100644 index 0000000000..66bbc34cbc --- /dev/null +++ b/usermods/MY9291/usermode_MY9291.h @@ -0,0 +1,45 @@ +#pragma once + +#include "wled.h" +#include "MY92xx.h" + +#define MY92XX_MODEL MY92XX_MODEL_MY9291 +#define MY92XX_CHIPS 1 +#define MY92XX_DI_PIN 13 +#define MY92XX_DCKI_PIN 15 + +#define MY92XX_RED 0 +#define MY92XX_GREEN 1 +#define MY92XX_BLUE 2 +#define MY92XX_WHITE 3 + +class MY9291Usermod : public Usermod { + private: + my92xx _my92xx = my92xx(MY92XX_MODEL, MY92XX_CHIPS, MY92XX_DI_PIN, MY92XX_DCKI_PIN, MY92XX_COMMAND_DEFAULT); + + public: + + void setup() { + _my92xx.setState(true); + } + + void connected() { + } + + void loop() { + uint32_t c = strip.getPixelColor(0); + int w = ((c >> 24) & 0xff) * bri / 255.0; + int r = ((c >> 16) & 0xff) * bri / 255.0; + int g = ((c >> 8) & 0xff) * bri / 255.0; + int b = (c & 0xff) * bri / 255.0; + _my92xx.setChannel(MY92XX_RED, r); + _my92xx.setChannel(MY92XX_GREEN, g); + _my92xx.setChannel(MY92XX_BLUE, b); + _my92xx.setChannel(MY92XX_WHITE, w); + _my92xx.update(); + } + + uint16_t getId() { + return USERMOD_ID_MY9291; + } +}; \ No newline at end of file diff --git a/usermods/PWM_fan/readme.md b/usermods/PWM_fan/readme.md index a40098c159..976d6b24c2 100644 --- a/usermods/PWM_fan/readme.md +++ b/usermods/PWM_fan/readme.md @@ -19,8 +19,8 @@ You will also need `-D USERMOD_DALLASTEMPERATURE`. All of the parameters are configured during run-time using Usermods settings page. This includes: -* PWM output pin -* tacho input pin +* PWM output pin (can be configured at compile time `-D PWM_PIN=xx`) +* tacho input pin (can be configured at compile time `-D TACHO_PIN=xx`) * sampling frequency in seconds * threshold temperature in degees C diff --git a/usermods/PWM_fan/usermod_PWM_fan.h b/usermods/PWM_fan/usermod_PWM_fan.h index 82aa917bbd..943ba1ae66 100644 --- a/usermods/PWM_fan/usermod_PWM_fan.h +++ b/usermods/PWM_fan/usermod_PWM_fan.h @@ -10,6 +10,13 @@ // https://github.com/KlausMu/esp32-fan-controller/tree/main/src // adapted for WLED usermod by @blazoncek +#ifndef TACHO_PIN + #define TACHO_PIN -1 +#endif + +#ifndef PWM_PIN + #define PWM_PIN -1 +#endif // tacho counter static volatile unsigned long counter_rpm = 0; @@ -37,8 +44,8 @@ class PWMFanUsermod : public Usermod { #endif // configurable parameters - int8_t tachoPin = -1; - int8_t pwmPin = -1; + int8_t tachoPin = TACHO_PIN; + int8_t pwmPin = PWM_PIN; uint8_t tachoUpdateSec = 30; float targetTemperature = 25.0; uint8_t minPWMValuePct = 50; diff --git a/usermods/Si7021_MQTT_HA/readme.md b/usermods/Si7021_MQTT_HA/readme.md new file mode 100644 index 0000000000..d9a96f7a70 --- /dev/null +++ b/usermods/Si7021_MQTT_HA/readme.md @@ -0,0 +1,69 @@ +# Si7021 to MQTT (with Home Assistant Auto Discovery) usermod + +This usermod implements support for [Si7021 I²C temperature and humidity sensors](https://www.silabs.com/documents/public/data-sheets/Si7021-A20.pdf). + +The sensor data will *not* be shown on the WLED UI (so far) but published via MQTT to WLED's "build in" MQTT device topic. + +``` +temperature: $mqttDeviceTopic/si7021_temperature +humidity: $mqttDeviceTopic/si7021_humidity +``` + +Additionally the following sensors can be published: + +``` +heat_index: $mqttDeviceTopic/si7021_heat_index +dew_point: $mqttDeviceTopic/si7021_dew_point +absolute_humidity: $mqttDeviceTopic/si7021_absolute_humidity +``` + +Sensor data will be updated/send every 60 seconds. + +This usermod also supports Home Assistant Auto Discovery. + +## Settings via Usermod Setup + +- `enabled`: Enables this usermod +- `Send Dew Point, Abs. Humidity and Heat Index`: Enables additional sensors +- `Home Assistant MQTT Auto-Discovery`: Enables Home Assistant Auto Discovery + +# Installation + +## Hardware + +Attach the Si7021 sensor to the I²C interface. + +Default PINs ESP32: + +``` +SCL_PIN = 22; +SDA_PIN = 21; +``` + +Default PINs ESP8266: + +``` +SCL_PIN = 5; +SDA_PIN = 4; +``` + +## Software + +Add to `build_flags` in platformio.ini: + +``` + -D USERMOD_SI7021_MQTT_HA +``` + +Add to `lib_deps` in platformio.ini: + +``` + adafruit/Adafruit Si7021 Library @ 1.4.0 + BME280@~3.0.0 +``` + +# Credits + +- Aircoookie for making WLED +- Other usermod creators for example code (`sensors_to_mqtt` and `multi_relay` especially) +- You, for reading this \ No newline at end of file diff --git a/usermods/Si7021_MQTT_HA/usermod_si7021_mqtt_ha.h b/usermods/Si7021_MQTT_HA/usermod_si7021_mqtt_ha.h new file mode 100644 index 0000000000..71c22da16a --- /dev/null +++ b/usermods/Si7021_MQTT_HA/usermod_si7021_mqtt_ha.h @@ -0,0 +1,236 @@ +#pragma once + +// this is remixed from usermod_v2_SensorsToMqtt.h (sensors_to_mqtt usermod) +// and usermod_multi_relay.h (multi_relay usermod) + +#include "wled.h" +#include +#include // EnvironmentCalculations::HeatIndex(), ::DewPoint(), ::AbsoluteHumidity() + +Adafruit_Si7021 si7021; + +#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; +#endif + +class Si7021_MQTT_HA : public Usermod +{ + private: + bool sensorInitialized = false; + bool mqttInitialized = false; + float sensorTemperature = 0; + float sensorHumidity = 0; + float sensorHeatIndex = 0; + float sensorDewPoint = 0; + float sensorAbsoluteHumidity= 0; + String mqttTemperatureTopic = ""; + String mqttHumidityTopic = ""; + String mqttHeatIndexTopic = ""; + String mqttDewPointTopic = ""; + String mqttAbsoluteHumidityTopic = ""; + unsigned long nextMeasure = 0; + bool enabled = false; + bool haAutoDiscovery = true; + bool sendAdditionalSensors = true; + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + static const char _sendAdditionalSensors[]; + static const char _haAutoDiscovery[]; + + void _initializeSensor() + { + sensorInitialized = si7021.begin(); + Serial.printf("Si7021_MQTT_HA: sensorInitialized = %d\n", sensorInitialized); + } + + void _initializeMqtt() + { + mqttTemperatureTopic = String(mqttDeviceTopic) + "/si7021_temperature"; + mqttHumidityTopic = String(mqttDeviceTopic) + "/si7021_humidity"; + mqttHeatIndexTopic = String(mqttDeviceTopic) + "/si7021_heat_index"; + mqttDewPointTopic = String(mqttDeviceTopic) + "/si7021_dew_point"; + mqttAbsoluteHumidityTopic = String(mqttDeviceTopic) + "/si7021_absolute_humidity"; + + // Update and publish sensor data + _updateSensorData(); + _publishSensorData(); + + if (haAutoDiscovery) { + _publishHAMqttSensor("temperature", "Temperature", mqttTemperatureTopic, "temperature", "°C"); + _publishHAMqttSensor("humidity", "Humidity", mqttHumidityTopic, "humidity", "%"); + if (sendAdditionalSensors) { + _publishHAMqttSensor("heat_index", "Heat Index", mqttHeatIndexTopic, "temperature", "°C"); + _publishHAMqttSensor("dew_point", "Dew Point", mqttDewPointTopic, "", "°C"); + _publishHAMqttSensor("absolute_humidity", "Absolute Humidity", mqttAbsoluteHumidityTopic, "", "g/m³"); + } + } + + mqttInitialized = true; + } + + void _publishHAMqttSensor( + const String &name, + const String &friendly_name, + const String &state_topic, + const String &deviceClass, + const String &unitOfMeasurement) + { + if (WLED_MQTT_CONNECTED) { + String topic = String("homeassistant/sensor/") + mqttClientID + "/" + name + "/config"; + + StaticJsonDocument<300> doc; + + doc["name"] = String(serverDescription) + " " + friendly_name; + doc["state_topic"] = state_topic; + doc["unique_id"] = String(mqttClientID) + name; + if (unitOfMeasurement != "") + doc["unit_of_measurement"] = unitOfMeasurement; + if (deviceClass != "") + doc["device_class"] = deviceClass; + doc["expire_after"] = 1800; + + JsonObject device = doc.createNestedObject("device"); // attach the sensor to the same device + device["name"] = String(serverDescription); + device["model"] = "WLED"; + device["manufacturer"] = "Aircoookie"; + device["identifiers"] = String("wled-") + String(serverDescription); + device["sw_version"] = VERSION; + + String payload; + serializeJson(doc, payload); + + mqtt->publish(topic.c_str(), 0, true, payload.c_str()); + } + } + + void _updateSensorData() + { + sensorTemperature = si7021.readTemperature(); + sensorHumidity = si7021.readHumidity(); + + // Serial.print("Si7021_MQTT_HA: Temperature: "); + // Serial.print(sensorTemperature, 2); + // Serial.print("\tHumidity: "); + // Serial.print(sensorHumidity, 2); + + if (sendAdditionalSensors) { + EnvironmentCalculations::TempUnit envTempUnit(EnvironmentCalculations::TempUnit_Celsius); + sensorHeatIndex = EnvironmentCalculations::HeatIndex(sensorTemperature, sensorHumidity, envTempUnit); + sensorDewPoint = EnvironmentCalculations::DewPoint(sensorTemperature, sensorHumidity, envTempUnit); + sensorAbsoluteHumidity = EnvironmentCalculations::AbsoluteHumidity(sensorTemperature, sensorHumidity, envTempUnit); + + // Serial.print("\tHeat Index: "); + // Serial.print(sensorHeatIndex, 2); + // Serial.print("\tDew Point: "); + // Serial.print(sensorDewPoint, 2); + // Serial.print("\tAbsolute Humidity: "); + // Serial.println(sensorAbsoluteHumidity, 2); + } + // else + // Serial.println(""); + } + + void _publishSensorData() + { + if (WLED_MQTT_CONNECTED) { + mqtt->publish(mqttTemperatureTopic.c_str(), 0, false, String(sensorTemperature).c_str()); + mqtt->publish(mqttHumidityTopic.c_str(), 0, false, String(sensorHumidity).c_str()); + if (sendAdditionalSensors) { + mqtt->publish(mqttHeatIndexTopic.c_str(), 0, false, String(sensorHeatIndex).c_str()); + mqtt->publish(mqttDewPointTopic.c_str(), 0, false, String(sensorDewPoint).c_str()); + mqtt->publish(mqttAbsoluteHumidityTopic.c_str(), 0, false, String(sensorAbsoluteHumidity).c_str()); + } + } + } + + public: + void addToConfig(JsonObject& root) + { + JsonObject top = root.createNestedObject(FPSTR(_name)); + + top[FPSTR(_enabled)] = enabled; + top[FPSTR(_sendAdditionalSensors)] = sendAdditionalSensors; + top[FPSTR(_haAutoDiscovery)] = haAutoDiscovery; + } + + bool readFromConfig(JsonObject& root) + { + JsonObject top = root[FPSTR(_name)]; + + bool configComplete = !top.isNull(); + configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled); + configComplete &= getJsonValue(top[FPSTR(_sendAdditionalSensors)], sendAdditionalSensors); + configComplete &= getJsonValue(top[FPSTR(_haAutoDiscovery)], haAutoDiscovery); + + return configComplete; + } + + void onMqttConnect(bool sessionPresent) { + if (mqttDeviceTopic[0] != 0) + _initializeMqtt(); + } + + void setup() + { + if (enabled) { + Serial.println("Si7021_MQTT_HA: Starting!"); + Wire.begin(SDA_PIN, SCL_PIN); + Serial.println("Si7021_MQTT_HA: Initializing sensors.. "); + _initializeSensor(); + } + } + + // gets called every time WiFi is (re-)connected. + void connected() + { + nextMeasure = millis() + 5000; // Schedule next measure in 5 seconds + } + + void loop() + { + yield(); + if (!enabled || strip.isUpdating()) return; // !sensorFound || + + unsigned long tempTimer = millis(); + + if (tempTimer > nextMeasure) { + nextMeasure = tempTimer + 60000; // Schedule next measure in 60 seconds + + if (!sensorInitialized) { + Serial.println("Si7021_MQTT_HA: Error! Sensors not initialized in loop()!"); + _initializeSensor(); + return; // lets try again next loop + } + + if (WLED_MQTT_CONNECTED) { + if (!mqttInitialized) + _initializeMqtt(); + + // Update and publish sensor data + _updateSensorData(); + _publishSensorData(); + } + else { + Serial.println("Si7021_MQTT_HA: Missing MQTT connection. Not publishing data"); + mqttInitialized = false; + } + } + } + + uint16_t getId() + { + return USERMOD_ID_SI7021_MQTT_HA; + } +}; + +// strings to reduce flash memory usage (used more than twice) +const char Si7021_MQTT_HA::_name[] PROGMEM = "Si7021 MQTT (Home Assistant)"; +const char Si7021_MQTT_HA::_enabled[] PROGMEM = "enabled"; +const char Si7021_MQTT_HA::_sendAdditionalSensors[] PROGMEM = "Send Dew Point, Abs. Humidity and Heat Index"; +const char Si7021_MQTT_HA::_haAutoDiscovery[] PROGMEM = "Home Assistant MQTT Auto-Discovery"; diff --git a/usermods/multi_relay/readme.md b/usermods/multi_relay/readme.md index 2d933cdabe..663a9f0093 100644 --- a/usermods/multi_relay/readme.md +++ b/usermods/multi_relay/readme.md @@ -3,12 +3,12 @@ This usermod-v2 modification allows the connection of multiple relays each with individual delay and on/off mode. ## HTTP API -All responses are returned as JSON. +All responses are returned as JSON. * Status Request: `http://[device-ip]/relays` * Switch Command: `http://[device-ip]/relays?switch=1,0,1,1` -The number of numbers behind the switch parameter must correspond to the number of relays. The number 1 switches the relay on. The number 0 switches the relay off. +The number of numbers behind the switch parameter must correspond to the number of relays. The number 1 switches the relay on. The number 0 switches the relay off. * Toggle Command: `http://[device-ip]/relays?toggle=1,0,1,1` @@ -81,13 +81,15 @@ void registerUsermods() Usermod can be configured in Usermods settings page. * `enabled` - enable/disable usermod -* `pin` - GPIO pin where relay is attached to ESP +* `pin` - GPIO pin where relay is attached to ESP (can be configured at compile time `-D MULTI_RELAY_PINS=xx,xx,...`) * `delay-s` - delay in seconds after on/off command is received * `active-high` - toggle high/low activation of relay (can be used to reverse relay states) * `external` - if enabled WLED does not control relay, it can only be triggered by external command (MQTT, HTTP, JSON or button) * `button` - button (from LED Settings) that controls this relay +* `broadcast`- time in seconds between state broadcasts using MQTT +* `HA-discovery`- enable Home Assistant auto discovery -If there is no MultiRelay section, just save current configuration and re-open Usermods settings page. +If there is no MultiRelay section, just save current configuration and re-open Usermods settings page. Have fun - @blazoncek diff --git a/usermods/multi_relay/usermod_multi_relay.h b/usermods/multi_relay/usermod_multi_relay.h index 6143a6b997..72d140d183 100644 --- a/usermods/multi_relay/usermod_multi_relay.h +++ b/usermods/multi_relay/usermod_multi_relay.h @@ -6,6 +6,10 @@ #define MULTI_RELAY_MAX_RELAYS 4 #endif +#ifndef MULTI_RELAY_PINS + #define MULTI_RELAY_PINS -1 +#endif + #define WLED_DEBOUNCE_THRESHOLD 50 //only consider button input of at least 50ms as valid (debouncing) #define ON true @@ -71,7 +75,7 @@ class MultiRelay : public Usermod { } /** - * switch off the strip if the delay has elapsed + * switch off the strip if the delay has elapsed */ void handleOffTimer() { unsigned long now = millis(); @@ -177,8 +181,9 @@ class MultiRelay : public Usermod { * constructor */ MultiRelay() { + const int8_t defPins[] = {MULTI_RELAY_PINS}; for (uint8_t i=0; i WLED_DEBOUNCE_THRESHOLD) { //fire edge event only after 50ms without change (debounce) for (uint8_t i=0; i=0 && _relay[i].button == b) { @@ -453,7 +458,7 @@ class MultiRelay : public Usermod { } return handled; } - + /** * addToJsonInfo() can be used to add custom entries to the /json/info part of the JSON API. */ @@ -557,7 +562,7 @@ class MultiRelay : public Usermod { /** * restore the changeable values * readFromConfig() is called before setup() to populate properties from values stored in cfg.json - * + * * The function should return true if configuration was successfully loaded or false if there was no configuration. */ bool readFromConfig(JsonObject &root) { @@ -619,7 +624,7 @@ class MultiRelay : public Usermod { DEBUG_PRINTLN(F(" config (re)loaded.")); } // use "return !top["newestParameter"].isNull();" when updating Usermod with new features - return !top[FPSTR(_broadcast)].isNull(); + return !top[FPSTR(_HAautodiscovery)].isNull(); } /** diff --git a/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h b/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h index c289cc32ad..4926469d6d 100644 --- a/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h +++ b/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h @@ -40,6 +40,7 @@ class AutoSaveUsermod : public Usermod { // If we've detected the need to auto save, this will be non zero. unsigned long autoSaveAfter = 0; + uint8_t knownInputLevel = 0; //WLEDSR uint8_t knownBrightness = 0; uint8_t knownEffectSpeed = 0; uint8_t knownEffectIntensity = 0; @@ -88,6 +89,7 @@ class AutoSaveUsermod : public Usermod { #endif initDone = true; if (enabled && applyAutoSaveOnBoot) applyPreset(autoSavePreset); + knownInputLevel = inputLevel; //WLEDSR knownBrightness = bri; knownEffectSpeed = effectSpeed; knownEffectIntensity = effectIntensity; @@ -110,7 +112,10 @@ class AutoSaveUsermod : public Usermod { uint8_t currentPalette = strip.getMainSegment().palette; unsigned long wouldAutoSaveAfter = now + autoSaveAfterSec*1000; - if (knownBrightness != bri) { + if ((soundAgc == 0) && (knownInputLevel != inputLevel)) { //begin WLEDSR + knownInputLevel = inputLevel; + autoSaveAfter = wouldAutoSaveAfter; //end WLEDSR + } else if (knownBrightness != bri) { knownBrightness = bri; autoSaveAfter = wouldAutoSaveAfter; } else if (knownEffectSpeed != effectSpeed) { diff --git a/usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h b/usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h index 88b18b35ed..a0299bc94f 100644 --- a/usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h +++ b/usermods/usermod_v2_four_line_display/usermod_v2_four_line_display.h @@ -137,7 +137,6 @@ class FourLineDisplayUsermod : public Usermod { private: bool initDone = false; - bool enabled = true; unsigned long lastTime = 0; // HW interface & configuration @@ -159,6 +158,7 @@ class FourLineDisplayUsermod : public Usermod { bool clockMode = false; // display clock bool forceAutoRedraw = false; // WLEDSR: force rotating of variables in display, even if strip.isUpdating, this can cause led stutter, this should not be necessary if display is fast enough... bool noAutoRedraw = false; // WLEDSR: never do auto Redraw, only when variable changes or rotary is pressed (in case redraw causes stutter on leds, should not be needed with spi displays) + bool enabled = true; // Next variables hold the previous known values to determine if redraw is // required. @@ -1048,7 +1048,7 @@ class FourLineDisplayUsermod : public Usermod { setFlipMode(flip); if (needsRedraw && !wakeDisplay()) redraw(true); } - // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features (or return false during development) return !top[FPSTR(_enabled)].isNull(); } diff --git a/usermods/usermod_v2_mode_sort/usermod_v2_mode_sort.h b/usermods/usermod_v2_mode_sort/usermod_v2_mode_sort.h index b923270285..b6ab1fa0b4 100644 --- a/usermods/usermod_v2_mode_sort/usermod_v2_mode_sort.h +++ b/usermods/usermod_v2_mode_sort/usermod_v2_mode_sort.h @@ -189,6 +189,7 @@ class ModeSortUsermod : public Usermod { bool complete = false; for (size_t i = 0; i < strlen_P(json); i++) { singleJsonSymbol = pgm_read_byte_near(json + i); + if (singleJsonSymbol == '\0') break; switch (singleJsonSymbol) { case '"': insideQuotes = !insideQuotes; @@ -200,20 +201,14 @@ class ModeSortUsermod : public Usermod { case '[': break; case ']': - complete = true; + if (!insideQuotes) complete = true; break; case ',': - if (!insideQuotes) { //WLEDSR(HarryB) added condition to differentiate between comma in mode name or as seperator - modeIndex++; - } + if (!insideQuotes) modeIndex++; default: - if (!insideQuotes) { - break; - } - } - if (complete) { - break; + if (!insideQuotes) break; } + if (complete) break; } return modeStrings; } diff --git a/usermods/usermod_v2_word_clock/readme.md b/usermods/usermod_v2_word_clock/readme.md new file mode 100644 index 0000000000..9c4d1ac04e --- /dev/null +++ b/usermods/usermod_v2_word_clock/readme.md @@ -0,0 +1,26 @@ +# Word Clock Usermod V2 + +This usermod can be used to drive a wordclock with a 11x10 pixel matrix with WLED. There are also 4 additional dots for the minutes. +The visualisation is desribed in 4 mask with LED numbers (single dots for minutes, minutes, hours and "clock/Uhr"). +There are 2 parameters to chnage the behaviour: + +active: enable/disable usermod +diplayItIs: enable/disable display of "Es ist" on the clock. + +## Installation + +Copy and update the example `platformio_override.ini.sample` +from the Rotary Encoder UI usermode folder to the root directory of your particular build. +This file should be placed in the same directory as `platformio.ini`. + +### Define Your Options + +* `USERMOD_WORDCLOCK` - define this to have this the Auto Save usermod included wled00\usermods_list.cpp + +### PlatformIO requirements + +No special requirements. + +## Change Log + +2022/03/30 initial commit \ No newline at end of file diff --git a/usermods/usermod_v2_word_clock/usermod_v2_word_clock.h b/usermods/usermod_v2_word_clock/usermod_v2_word_clock.h new file mode 100644 index 0000000000..10b83dd01a --- /dev/null +++ b/usermods/usermod_v2_word_clock/usermod_v2_word_clock.h @@ -0,0 +1,427 @@ +#pragma once + +#include "wled.h" + +/* + * Usermods allow you to add own functionality to WLED more easily + * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * + * This usermod can be used to drive a wordclock with a 11x10 pixel matrix with WLED. There are also 4 additional dots for the minutes. + * The visualisation is desribed in 4 mask with LED numbers (single dots for minutes, minutes, hours and "clock/Uhr"). + * There are 2 parameters to chnage the behaviour: + * + * active: enable/disable usermod + * diplayItIs: enable/disable display of "Es ist" on the clock. + */ + +class WordClockUsermod : public Usermod +{ + private: + unsigned long lastTime = 0; + int lastTimeMinutes = -1; + + // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer) + bool usermodActive = false; + bool displayItIs = false; + + // defines for mask sizes + #define maskSizeLeds 114 + #define maskSizeMinutes 12 + #define maskSizeHours 6 + #define maskSizeItIs 5 + #define maskSizeMinuteDots 4 + + // "minute" masks + const int maskMinutes[12][maskSizeMinutes] = + { + {107, 108, 109, -1, -1, -1, -1, -1, -1, -1, -1, -1}, // :00 + { 7, 8, 9, 10, 40, 41, 42, 43, -1, -1, -1, -1}, // :05 fünf nach + { 11, 12, 13, 14, 40, 41, 42, 43, -1, -1, -1, -1}, // :10 zehn nach + { 26, 27, 28, 29, 30, 31, 32, -1, -1, -1, -1, -1}, // :15 viertel + { 15, 16, 17, 18, 19, 20, 21, 40, 41, 42, 43, -1}, // :20 zwanzig nach + { 7, 8, 9, 10, 33, 34, 35, 44, 45, 46, 47, -1}, // :25 fünf vor halb + { 44, 45, 46, 47, -1, -1, -1, -1, -1, -1, -1, -1}, // :30 halb + { 7, 8, 9, 10, 40, 41, 42, 43, 44, 45, 46, 47}, // :35 fünf nach halb + { 15, 16, 17, 18, 19, 20, 21, 33, 34, 35, -1, -1}, // :40 zwanzig vor + { 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, -1}, // :45 dreiviertel + { 11, 12, 13, 14, 33, 34, 35, -1, -1, -1, -1, -1}, // :50 zehn vor + { 7, 8, 9, 10, 33, 34, 35, -1, -1, -1, -1, -1} // :55 fünf vor + }; + + // hour masks + const int maskHours[13][maskSizeHours] = + { + { 55, 56, 57, -1, -1, -1}, // 01: ein + { 55, 56, 57, 58, -1, -1}, // 01: eins + { 62, 63, 64, 65, -1, -1}, // 02: zwei + { 66, 67, 68, 69, -1, -1}, // 03: drei + { 73, 74, 75, 76, -1, -1}, // 04: vier + { 51, 52, 53, 54, -1, -1}, // 05: fünf + { 77, 78, 79, 80, 81, -1}, // 06: sechs + { 88, 89, 90, 91, 92, 93}, // 07: sieben + { 84, 85, 86, 87, -1, -1}, // 08: acht + {102, 103, 104, 105, -1, -1}, // 09: neun + { 99, 100, 101, 102, -1, -1}, // 10: zehn + { 49, 50, 51, -1, -1, -1}, // 11: elf + { 94, 95, 96, 97, 98, -1} // 12: zwölf and 00: null + }; + + // mask "it is" + const int maskItIs[maskSizeItIs] = {0, 1, 3, 4, 5}; + + // mask minute dots + const int maskMinuteDots[maskSizeMinuteDots] = {110, 111, 112, 113}; + + // overall mask to define which LEDs are on + int maskLedsOn[maskSizeLeds] = + { + 0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0 + }; + + // update led mask + void updateLedMask(const int wordMask[], int arraySize) + { + // loop over array + for (int x=0; x < arraySize; x++) + { + // check if mask has a valid LED number + if (wordMask[x] >= 0 && wordMask[x] < maskSizeLeds) + { + // turn LED on + maskLedsOn[wordMask[x]] = 1; + } + } + } + + // set hours + void setHours(int hours, bool fullClock) + { + int index = hours; + + // handle 00:xx as 12:xx + if (hours == 0) + { + index = 12; + } + + // check if we get an overrun of 12 o´clock + if (hours == 13) + { + index = 1; + } + + // special handling for "ein Uhr" instead of "eins Uhr" + if (hours == 1 && fullClock == true) + { + index = 0; + } + + // update led mask + updateLedMask(maskHours[index], maskSizeHours); + } + + // set minutes + void setMinutes(int index) + { + // update led mask + updateLedMask(maskMinutes[index], maskSizeMinutes); + } + + // set minutes dot + void setSingleMinuteDots(int minutes) + { + // modulo to get minute dots + int minutesDotCount = minutes % 5; + + // check if minute dots are active + if (minutesDotCount > 0) + { + // activate all minute dots until number is reached + for (int i = 0; i < minutesDotCount; i++) + { + // activate LED + maskLedsOn[maskMinuteDots[i]] = 1; + } + } + } + + // update the display + void updateDisplay(uint8_t hours, uint8_t minutes) + { + // disable complete matrix at the bigging + for (int x = 0; x < maskSizeLeds; x++) + { + maskLedsOn[x] = 0; + } + + // display it is/es ist if activated + if (displayItIs) + { + updateLedMask(maskItIs, maskSizeItIs); + } + + // set single minute dots + setSingleMinuteDots(minutes); + + // switch minutes + switch (minutes / 5) + { + case 0: + // full hour + setMinutes(0); + setHours(hours, true); + break; + case 1: + // 5 nach + setMinutes(1); + setHours(hours, false); + break; + case 2: + // 10 nach + setMinutes(2); + setHours(hours, false); + break; + case 3: + // viertel + setMinutes(3); + setHours(hours + 1, false); + break; + case 4: + // 20 nach + setMinutes(4); + setHours(hours, false); + break; + case 5: + // 5 vor halb + setMinutes(5); + setHours(hours + 1, false); + break; + case 6: + // halb + setMinutes(6); + setHours(hours + 1, false); + break; + case 7: + // 5 nach halb + setMinutes(7); + setHours(hours + 1, false); + break; + case 8: + // 20 vor + setMinutes(8); + setHours(hours + 1, false); + break; + case 9: + // viertel vor + setMinutes(9); + setHours(hours + 1, false); + break; + case 10: + // 10 vor + setMinutes(10); + setHours(hours + 1, false); + break; + case 11: + // 5 vor + setMinutes(11); + setHours(hours + 1, false); + break; + } + } + + 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() + { + } + + /* + * 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. + * + * 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() { + + // do it every 5 seconds + if (millis() - lastTime > 5000) + { + // check the time + int minutes = minute(localTime); + + // check if we already updated this minute + if (lastTimeMinutes != minutes) + { + // update the display with new time + updateDisplay(hourFormat12(localTime), minute(localTime)); + + // remember last update time + lastTimeMinutes = minutes; + } + + // remember last update + lastTime = millis(); + } + } + + /* + * 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) + { + } + */ + + /* + * 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) + { + } + + /* + * 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) + { + } + + /* + * addToConfig() can be used to add custom persistent settings to the cfg.json file in the "um" (usermod) object. + * It will be called by WLED when settings are actually saved (for example, LED settings are saved) + * If you want to force saving the current state, use serializeConfig() in your loop(). + * + * CAUTION: serializeConfig() will initiate a filesystem write operation. + * It might cause the LEDs to stutter and will cause flash wear if called too often. + * Use it sparingly and always in the loop, never in network callbacks! + * + * addToConfig() will make your settings editable through the Usermod Settings page automatically. + * + * Usermod Settings Overview: + * - Numeric values are treated as floats in the browser. + * - If the numeric value entered into the browser contains a decimal point, it will be parsed as a C float + * before being returned to the Usermod. The float data type has only 6-7 decimal digits of precision, and + * doubles are not supported, numbers will be rounded to the nearest float value when being parsed. + * The range accepted by the input field is +/- 1.175494351e-38 to +/- 3.402823466e+38. + * - If the numeric value entered into the browser doesn't contain a decimal point, it will be parsed as a + * C int32_t (range: -2147483648 to 2147483647) before being returned to the usermod. + * Overflows or underflows are truncated to the max/min value for an int32_t, and again truncated to the type + * used in the Usermod when reading the value from ArduinoJson. + * - Pin values can be treated differently from an integer value by using the key name "pin" + * - "pin" can contain a single or array of integer values + * - On the Usermod Settings page there is simple checking for pin conflicts and warnings for special pins + * - Red color indicates a conflict. Yellow color indicates a pin with a warning (e.g. an input-only pin) + * - Tip: use int8_t to store the pin value in the Usermod, so a -1 value (pin not set) can be used + * + * See usermod_v2_auto_save.h for an example that saves Flash space by reusing ArduinoJson key name strings + * + * If you need a dedicated settings page with custom layout for your Usermod, that takes a lot more work. + * You will have to add the setting to the HTML, xml.cpp and set.cpp manually. + * See the WLED Soundreactive fork (code and wiki) for reference. https://github.com/atuline/WLED + * + * I highly recommend checking out the basics of ArduinoJson serialization and deserialization in order to use custom settings! + */ + void addToConfig(JsonObject& root) + { + JsonObject top = root.createNestedObject("WordClockUsermod"); + top["active"] = usermodActive; + top["displayItIs"] = displayItIs; + } + + /* + * readFromConfig() can be used to read back the custom settings you added with addToConfig(). + * This is called by WLED when settings are loaded (currently this only happens immediately after boot, or after saving on the Usermod Settings page) + * + * readFromConfig() is called BEFORE setup(). This means you can use your persistent values in setup() (e.g. pin assignments, buffer sizes), + * but also that if you want to write persistent values to a dynamic buffer, you'd need to allocate it here instead of in setup. + * If you don't know what that is, don't fret. It most likely doesn't affect your use case :) + * + * Return true in case the config values returned from Usermod Settings were complete, or false if you'd like WLED to save your defaults to disk (so any missing values are editable in Usermod Settings) + * + * getJsonValue() returns false if the value is missing, or copies the value into the variable provided and returns true if the value is present + * The configComplete variable is true only if the "exampleUsermod" object and all values are present. If any values are missing, WLED will know to call addToConfig() to save them + * + * This function is guaranteed to be called on boot, but could also be called every time settings are updated + */ + bool readFromConfig(JsonObject& root) + { + // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor + // setting them inside readFromConfig() is slightly more robust, handling the rare but plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed) + + JsonObject top = root["WordClockUsermod"]; + + bool configComplete = !top.isNull(); + + configComplete &= getJsonValue(top["active"], usermodActive); + configComplete &= getJsonValue(top["displayItIs"], displayItIs); + + return configComplete; + } + + /* + * handleOverlayDraw() is called just before every show() (LED strip update frame) after effects have set the colors. + * Use this to blank out some LEDs or set them to a different color regardless of the set effect mode. + * Commonly used for custom clocks (Cronixie, 7 segment) + */ + void handleOverlayDraw() + { + // check if usermod is active + if (usermodActive == true) + { + // loop over all leds + for (int x = 0; x < maskSizeLeds; x++) + { + // check mask + if (maskLedsOn[x] == 0) + { + // set pixel off + strip.setPixelColor(x, RGBW32(0,0,0,0)); + } + } + } + } + + /* + * 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_WORDCLOCK; + } + + //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! +}; \ No newline at end of file diff --git a/usermods/wizlights/readme.md b/usermods/wizlights/readme.md new file mode 100644 index 0000000000..802a3798c4 --- /dev/null +++ b/usermods/wizlights/readme.md @@ -0,0 +1,35 @@ +# Controlling Wiz lights + +This usermod allows the control of [WiZ](https://www.wizconnected.com/en/consumer/) lights that are in the same network as the WLED controller. + +The mod takes the colors from the first few pixels and sends them to the lights. + +## Configuration + +- Interval (ms) + - How frequently to update the WiZ lights, in milliseconds. + - Setting too low may causse ESP to become unresponsive. +- Send Delay (ms) + - An optional millisecond delay after updating each WiZ light. + - Can help smooth out effects when using a larger number of WiZ lights +- Use Enhanced White + - Enables using the WiZ lights onboard white LEDs instead of sending maximum RGB values. + - Tunable with warm and cool LEDs as supported by WiZ bulbs + - Note: Only sent when max RGB value is set, need to have automatic brightness limiter disabled + - ToDo: Have better logic for white value mixing to better take advantage of the lights capabilities +- Always Force Update + - Can be enabled to always send update message to light, even when color matches what was previously sent. +- Force update every x minutes + - Configuration option to allow adjusting the default force update timeout of 5 minutes. + - Setting to 0 has the same impact as enabling Always Force Update + - +Then enter the IPs for the lights to be controlled, in order. There is currently a limit of 15 devices that can be controled, but that number +can be easily changed by updating _MAX_WIZ_LIGHTS_. + + + + +## Related project + +If you use these lights and python, make sure to check out the [pywizlight](https://github.com/sbidy/pywizlight) project. I learned how to +format the messages to control the lights from that project. diff --git a/usermods/wizlights/wizlights.h b/usermods/wizlights/wizlights.h new file mode 100644 index 0000000000..c588f00ea8 --- /dev/null +++ b/usermods/wizlights/wizlights.h @@ -0,0 +1,154 @@ +#pragma once + +#include "wled.h" +#include + +// Maximum number of lights supported +#define MAX_WIZ_LIGHTS 15 + +WiFiUDP UDP; + + + + +class WizLightsUsermod : public Usermod { + + private: + unsigned long lastTime = 0; + long updateInterval; + long sendDelay; + + long forceUpdateMinutes; + bool forceUpdate; + + bool useEnhancedWhite; + long warmWhite; + long coldWhite; + + IPAddress lightsIP[MAX_WIZ_LIGHTS]; // Stores Light IP addresses + bool lightsValid[MAX_WIZ_LIGHTS]; // Stores Light IP address validity + uint32_t colorsSent[MAX_WIZ_LIGHTS]; // Stores last color sent for each light + + + + public: + + + + // Send JSON blob to WiZ Light over UDP + // RGB or C/W white + // TODO: + // Better utilize WLED existing white mixing logic + void wizSendColor(IPAddress ip, uint32_t color) { + UDP.beginPacket(ip, 38899); + + // If no LED color, turn light off. Note wiz light setting for "Off fade-out" will be applied by the light itself. + if (color == 0) { + UDP.print("{\"method\":\"setPilot\",\"params\":{\"state\":false}}"); + + // If color is WHITE, try and use the lights WHITE LEDs instead of mixing RGB LEDs + } else if (color == 16777215 && useEnhancedWhite){ + + // set cold white light only + if (coldWhite > 0 && warmWhite == 0){ + UDP.print("{\"method\":\"setPilot\",\"params\":{\"c\":"); UDP.print(coldWhite) ;UDP.print("}}");} + + // set warm white light only + if (warmWhite > 0 && coldWhite == 0){ + UDP.print("{\"method\":\"setPilot\",\"params\":{\"w\":"); UDP.print(warmWhite) ;UDP.print("}}");} + + // set combination of warm and cold white light + if (coldWhite > 0 && warmWhite > 0){ + UDP.print("{\"method\":\"setPilot\",\"params\":{\"c\":"); UDP.print(coldWhite) ;UDP.print(",\"w\":"); UDP.print(warmWhite); UDP.print("}}");} + + // Send color as RGB + } else { + UDP.print("{\"method\":\"setPilot\",\"params\":{\"r\":"); + UDP.print(R(color)); + UDP.print(",\"g\":"); + UDP.print(G(color)); + UDP.print(",\"b\":"); + UDP.print(B(color)); + UDP.print("}}"); + } + + UDP.endPacket(); + } + + + + // TODO: Check millis() rollover + void loop() { + + // Make sure we are connected first + if (!WLED_CONNECTED) return; + + unsigned long ellapsedTime = millis() - lastTime; + if (ellapsedTime > updateInterval) { + bool update = false; + for (uint8_t i = 0; i < MAX_WIZ_LIGHTS; i++) { + if (!lightsValid[i]) { continue; } + uint32_t newColor = strip.getPixelColor(i); + if (forceUpdate || (newColor != colorsSent[i]) || (ellapsedTime > forceUpdateMinutes*60000)){ + wizSendColor(lightsIP[i], newColor); + colorsSent[i] = newColor; + update = true; + delay(sendDelay); + } + } + if (update) lastTime = millis(); + } + } + + + + void addToConfig(JsonObject& root) + { + JsonObject top = root.createNestedObject("wizLightsUsermod"); + top["Interval (ms)"] = updateInterval; + top["Send Delay (ms)"] = sendDelay; + top["Use Enhanced White *"] = useEnhancedWhite; + top["* Warm White Value (0-255)"] = warmWhite; + top["* Cold White Value (0-255)"] = coldWhite; + top["Always Force Update"] = forceUpdate; + top["Force Update Every x Minutes"] = forceUpdateMinutes; + + for (uint8_t i = 0; i < MAX_WIZ_LIGHTS; i++) { + top[getJsonLabel(i)] = lightsIP[i].toString(); + } + } + + + + bool readFromConfig(JsonObject& root) + { + JsonObject top = root["wizLightsUsermod"]; + bool configComplete = !top.isNull(); + + configComplete &= getJsonValue(top["Interval (ms)"], updateInterval, 1000); // How frequently to update the wiz lights + configComplete &= getJsonValue(top["Send Delay (ms)"], sendDelay, 0); // Optional delay after sending each UDP message + configComplete &= getJsonValue(top["Use Enhanced White *"], useEnhancedWhite, false); // When color is white use wiz white LEDs instead of mixing RGB + configComplete &= getJsonValue(top["* Warm White Value (0-255)"], warmWhite, 0); // Warm White LED value for Enhanced White + configComplete &= getJsonValue(top["* Cold White Value (0-255)"], coldWhite, 50); // Cold White LED value for Enhanced White + configComplete &= getJsonValue(top["Always Force Update"], forceUpdate, false); // Update wiz light every loop, even if color value has not changed + configComplete &= getJsonValue(top["Force Update Every x Minutes"], forceUpdateMinutes, 5); // Update wiz light if color value has not changed, every x minutes + + // Read list of IPs + String tempIp; + for (uint8_t i = 0; i < MAX_WIZ_LIGHTS; i++) { + configComplete &= getJsonValue(top[getJsonLabel(i)], tempIp, "0.0.0.0"); + lightsValid[i] = lightsIP[i].fromString(tempIp); + + // If the IP is not valid, force the value to be empty + if (!lightsValid[i]){lightsIP[i].fromString("0.0.0.0");} + } + + return configComplete; + } + + + // Create label for the usermod page (I cannot make it work with JSON arrays...) + String getJsonLabel(uint8_t i) {return "WiZ Light IP #" + String(i+1);} + + uint16_t getId(){return USERMOD_ID_WIZLIGHTS;} +}; diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 019196c912..60f57f373d 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -2816,7 +2816,6 @@ uint16_t WS2812FX::mode_popcorn(void) { if (popcorn[i].pos >= 0.0f) { // draw now active popcorn (either active before or just popped) uint32_t col = color_wheel(popcorn[i].colIndex); if (!SEGMENT.palette && popcorn[i].colIndex < NUM_COLORS) col = SEGCOLOR(popcorn[i].colIndex); - uint16_t ledIndex = popcorn[i].pos; if (ledIndex < SEGLEN) setPixelColor(ledIndex, col); } @@ -4265,17 +4264,21 @@ uint16_t WS2812FX::mode_aurora(void) { // Sound reactive external variables. -extern int sample; +extern int sampleRaw; extern float sampleAvg; extern bool samplePeak; extern uint8_t myVals[32]; -extern int sampleAgc; +//extern int sampleAgc; +extern int rawSampleAgc; +extern float sampleAgc; extern uint8_t squelch; extern byte soundSquelch; extern byte soundAgc; extern uint8_t maxVol; extern uint8_t binNum; +extern float sampleReal; // "sample" as float, to provide bits that are lost otherwise. Needed for AGC. +extern float multAgc; // sampleReal * multAgc = sampleAgc. Our multiplier // FFT based variables extern double FFT_MajorPeak; @@ -4289,13 +4292,15 @@ extern float fftAvg[]; // Helper function(s) // /////////////////////////////////////// +double mapf(double x, double in_min, double in_max, double out_min, double out_max); + //////////////////////////// // set Pixels // //////////////////////////// -void WS2812FX::setPixels(CRGB* leds) { // ewowi20210703: use realPixelIndex (rotated and mirrored) to find the right led +void WS2812FX::setPixels(CRGB* leds) { // ewowi20210703: use segmentToLogical (rotated and mirrored) to find the right led for (int i=0; i < SEGLEN; i++) { - setPixelColor(i, leds[realPixelIndex(i)].red, leds[realPixelIndex(i)].green, leds[realPixelIndex(i)].blue); + setPixelColor(i, leds[segmentToLogical(i)].red, leds[segmentToLogical(i)].green, leds[segmentToLogical(i)].blue); } } @@ -4333,7 +4338,7 @@ uint16_t WS2812FX::mode_wavesins(void) { // Uses beatsi for (int i = 0; i < SEGLEN; i++) { uint8_t bri = sin8(millis()/4+i* (int)SEGMENT.intensity); // leds[i] = CHSV(beatsin8(SEGMENT.speed, SEGMENT.custom1, SEGMENT.custom1+SEGMENT.custom2, 0, i * SEGMENT.custom3), 255, bri); - leds[realPixelIndex(i)] = ColorFromPalette(currentPalette, beatsin8(SEGMENT.speed, SEGMENT.custom1, SEGMENT.custom1+SEGMENT.custom2, 0, i * SEGMENT.custom3), bri, LINEARBLEND); + leds[segmentToLogical(i)] = ColorFromPalette(currentPalette, beatsin8(SEGMENT.speed, SEGMENT.custom1, SEGMENT.custom1+SEGMENT.custom2, 0, i * SEGMENT.custom3), bri, LINEARBLEND); } setPixels(leds); @@ -4356,7 +4361,7 @@ uint16_t WS2812FX::mode_FlowStripe(void) { // By: ldirko c = sin8(c); c = sin8(c / 2 + t); byte b = sin8(c + t/8); - leds[realPixelIndex(i)] = CHSV(b + hue, 255, 255); + leds[segmentToLogical(i)] = CHSV(b + hue, 255, 255); } setPixels(leds); @@ -4478,7 +4483,7 @@ void WS2812FX::nscale8( CRGB* leds, uint8_t scale) uint16_t WS2812FX::XY(uint16_t x, uint16_t y) { // ewowi20210703: new XY: segmentToReal: Maps XY in 2D segment to to rotated and mirrored logical index. Works for 1D strips and 2D panels - return realPixelIndex(x%SEGMENT.width + y%SEGMENT.height * SEGMENT.width); + return segmentToLogical(x%SEGMENT.width + y%SEGMENT.height * SEGMENT.width); } //Use https://wokwi.com/arduino/projects/300565972972995085 to create layout examples @@ -5454,7 +5459,7 @@ uint16_t WS2812FX::mode_2DSwirl(void) { // By: Mark Kriegsman https: uint8_t nj = (SEGMENT.width - 1) - j; uint16_t ms = millis(); - uint8_t tmpSound = (soundAgc) ? sampleAgc : sample; + int tmpSound = (soundAgc) ? rawSampleAgc : sampleRaw; leds[XY( i, j)] += ColorFromPalette(currentPalette, (ms / 11 + sampleAvg*4), tmpSound * SEGMENT.intensity / 64, LINEARBLEND); //CHSV( ms / 11, 200, 255); leds[XY( j, i)] += ColorFromPalette(currentPalette, (ms / 13 + sampleAvg*4), tmpSound * SEGMENT.intensity / 64, LINEARBLEND); //CHSV( ms / 13, 200, 255); @@ -5506,7 +5511,7 @@ uint16_t WS2812FX::mode_2DWaverly(void) { // byte thisVal = inoise8(i * 45 , t , t); // byte thisMax = map(thisVal, 0, 255, 0, SEGMENT.height); - uint8_t tmpSound = (soundAgc) ? sampleAgc : sampleAvg; + int tmpSound = (soundAgc) ? sampleAgc : sampleAvg; uint16_t thisVal = tmpSound*SEGMENT.intensity/64 * inoise8(i * 45 , t , t)/64; uint16_t thisMax = map(thisVal, 0, 512, 0, SEGMENT.height); @@ -5546,9 +5551,12 @@ uint16_t WS2812FX::mode_gravcenter(void) { // Gravcenter. By Andr fade_out(240); - float segmentSampleAvg = sampleAvg * SEGMENT.intensity / 255; + float tmpSound = (soundAgc) ? sampleAgc : sampleAvg; + float segmentSampleAvg = tmpSound * (float)SEGMENT.intensity / 255.0; + segmentSampleAvg *= 0.125; // divide by 8, to compensate for later "sensitivty" upscaling - int tempsamp = constrain(segmentSampleAvg*2,0,SEGLEN/2); // Keep the sample from overflowing. + float mySampleAvg = mapf(segmentSampleAvg*2.0, 0, 32, 0, (float)SEGLEN/2.0); // map to pixels availeable in current segment + int tempsamp = constrain(mySampleAvg,0,SEGLEN/2); // Keep the sample from overflowing. uint8_t gravity = 8 - SEGMENT.speed/32; for (int i=0; i(SEGENV.data); + + fade_out(240); + + float tmpSound = multAgc; // AGC gain + if (soundAgc == 0) { + if ((sampleAvg> 1.0) && (sampleReal > 0.05)) + tmpSound = (float)sample / sampleReal; // current non-AGC gain + else + tmpSound = ((float)sampleGain/40.0 * (float)inputLevel/128.0) + 1.0/16.0; // non-AGC gain from presets + } + + if (tmpSound > 2) tmpSound = ((tmpSound -2.0) / 2) +2; //compress ranges > 2 + if (tmpSound > 1) tmpSound = ((tmpSound -1.0) / 2) +1; //compress ranges > 1 + + float segmentSampleAvg = 64.0 * tmpSound * (float)SEGMENT.intensity / 128.0; + float mySampleAvg = mapf(segmentSampleAvg, 0, 128, 0, (SEGLEN-1)); // map to pixels availeable in current segment + int tempsamp = constrain(mySampleAvg,0,SEGLEN-1); // Keep the sample from overflowing. + + //tempsamp = SEGLEN - tempsamp; // uncomment to invert direction + segmentSampleAvg=fmax(64.0 - fmin(segmentSampleAvg,63),8); // inverted brightness + + uint8_t gravity = 8 - SEGMENT.speed/32; + + if (sampleAvg > 1) // disable bar "body" if below squelch + { + for (int i=0; i= gravcen->topLED) + gravcen->topLED = tempsamp; + else if (gravcen->gravityCounter % gravity == 0) + gravcen->topLED--; + + if (gravcen->topLED > 0) { + setPixelColor(gravcen->topLED, color_from_palette(millis(), false, PALETTE_SOLID_WRAP, 0)); + } + gravcen->gravityCounter = (gravcen->gravityCounter + 1) % gravity; + + return FRAMETIME; +} // mode_gravimeter() +#endif ////////////////////// // * JUGGLES // @@ -5654,8 +5719,10 @@ uint16_t WS2812FX::mode_gravimeter(void) { // Gravmeter. By Andre uint16_t WS2812FX::mode_juggles(void) { // Juggles. By Andrew Tuline. fade_out(224); + int my_sampleAgc = fmax(fmin(sampleAgc, 255.0), 0); + for (int i=0; iSEGLEN/2) maxLen = SEGLEN/2; for (int i=(SEGLEN/2-maxLen); i<(SEGLEN/2+maxLen); i++) { @@ -5732,7 +5800,7 @@ uint16_t WS2812FX::mode_noisefire(void) { // Noisefire. By Andre uint8_t tmpSound = (soundAgc) ? sampleAgc : sampleAvg; CRGB color = ColorFromPalette(currentPalette, index, tmpSound*2, LINEARBLEND); // Use the my own palette. - leds[realPixelIndex(i)] = color; + leds[segmentToLogical(i)] = color; } setPixels(leds); @@ -5749,14 +5817,14 @@ uint16_t WS2812FX::mode_noisemeter(void) { // Noisemeter. By Andr uint8_t fadeRate = map(SEGMENT.speed,0,255,224,255); fade_out(fadeRate); - int maxLen = sample*8; - - maxLen = maxLen * SEGMENT.intensity / 256; // Still a bit too sensitive. - + float tmpSound = (soundAgc) ? rawSampleAgc : sampleRaw; + float tmpSound2 = tmpSound * 2.0 * (float)SEGMENT.intensity / 255.0; + int maxLen = mapf(tmpSound2, 0, 255, 0, SEGLEN); // map to pixels availeable in current segment // Still a bit too sensitive. if (maxLen >SEGLEN) maxLen = SEGLEN; + tmpSound = soundAgc ? sampleAgc : sampleAvg; // now use smoothed value (sampleAvg or sampleAgc) for (int i=0; iSEGLEN/2; i--) { // Move to the right. - leds[realPixelIndex(i)] = leds[realPixelIndex(i-1)]; + leds[segmentToLogical(i)] = leds[segmentToLogical(i-1)]; } for (int i=0; ithatphase += beatsin8(7,-4,4); // Two phase values to make a complex pattern. By Andrew Tuline. for (int i=0; ithisphase)/2; - thisbright += cos8((i*117)+plasmoip->thatphase)/2; // Let's munge the brightness a bit and animate it all with the phases. - uint8_t colorIndex=thisbright; + // updated, similar to "plasma" effect - softhack007 + uint8_t thisbright = cubicwave8(((i*(1 + (3*SEGMENT.speed/32)))+plasmoip->thisphase) & 0xFF)/2; + thisbright += cos8(((i*(97 +(5*SEGMENT.speed/32)))+plasmoip->thatphase) & 0xFF)/2; // Let's munge the brightness a bit and animate it all with the phases. - uint8_t tmpSound = (soundAgc) ? sampleAgc : sampleAvg; - if (tmpSound * SEGMENT.intensity / 32 < thisbright) {thisbright = 0;} + uint8_t colorIndex=thisbright; + int tmpSound = (soundAgc) ? sampleAgc : sampleAvg; + if (tmpSound * SEGMENT.intensity / 64 < thisbright) {thisbright = 0;} - leds[realPixelIndex(i)] += color_blend(SEGCOLOR(1), color_from_palette(colorIndex, false, PALETTE_SOLID_WRAP, 0), thisbright); + leds[segmentToLogical(i)] += color_blend(SEGCOLOR(1), color_from_palette(colorIndex, false, PALETTE_SOLID_WRAP, 0), thisbright); } setPixels(leds); @@ -5862,7 +5931,7 @@ uint16_t WS2812FX::mode_puddlepeak(void) { // Puddlepeak. By Andr uint16_t pos = random(SEGLEN); // Set a random starting position. binNum = SEGMENT.custom2; // Select a bin. - maxVol = SEGMENT.custom3/2; // Our volume comparator. + maxVol = SEGMENT.custom3/4; // Our volume comparator. fade_out(fadeVal); @@ -5892,8 +5961,10 @@ uint16_t WS2812FX::mode_puddles(void) { // Puddles. By Andrew fade_out(fadeVal); - if (sample>0 ) { - size = sample * SEGMENT.intensity /256 /8 + 1; // Determine size of the flash based on the volume. + float tmpSound = (soundAgc) ? rawSampleAgc : sampleRaw; + + if (tmpSound>1 ) { + size = tmpSound * SEGMENT.intensity /256 /8 + 1; // Determine size of the flash based on the volume. if (pos+size>= SEGLEN) size=SEGLEN-pos; } @@ -6004,21 +6075,34 @@ uint16_t WS2812FX::mode_binmap(void) { // Binmap. Scale raw f float maxVal = 512; // Kind of a guess as to the maximum output value per combined logarithmic bins. + float binScale = (((float)sampleGain / 40.0) + 1.0/16) * ((float)inputLevel/128.0); // non-AGC gain multiplier + if (soundAgc) binScale = multAgc; // AGC gain + if (sampleAvg < 1) binScale = 0.001; // silentium! + +#if 0 + //The next lines are good for debugging, however too much flickering for non-developers ;-) + float my_magnitude = FFT_Magnitude / 16.0; // scale magnitude to be aligned with scaling of FFT bins + my_magnitude *= binScale; // apply gain + maxVal = fmax(64, my_magnitude); // set maxVal = max FFT result +#endif + for (int i=0; i startBin) endBin --; // avoid overlapping double sumBin = 0; for (int j=startBin; j<=endBin; j++) { - sumBin += (fftBin[j] < soundSquelch*6) ? 0 : fftBin[j]; // We need some sound temporary squelch for fftBin, because we didn't do it for the raw bins in audio_reactive.h + sumBin += (fftBin[j] < soundSquelch*1.75) ? 0 : fftBin[j]; // We need some sound temporary squelch for fftBin, because we didn't do it for the raw bins in audio_reactive.h } sumBin = sumBin/(endBin-startBin+1); // Normalize it. sumBin = sumBin * (i+5) / (endBin-startBin+5); // Disgusting frequency adjustment calculation. Lows were too bright. Am open to quick 'n dirty alternatives. - sumBin = sumBin * 8; // Need to use the 'log' version for this. + sumBin = sumBin * 8; // Need to use the 'log' version for this. Why " * 8" ?? + sumBin *= binScale; // apply gain if (sumBin > maxVal) sumBin = maxVal; // Make sure our bin isn't higher than the max . . which we capped earlier. @@ -6045,7 +6129,7 @@ uint16_t WS2812FX::mode_blurz(void) { // Blurz. By Andrew Tul fade_out(SEGMENT.speed); uint16_t segLoc = random(SEGLEN); - leds[realPixelIndex(segLoc)] = color_blend(SEGCOLOR(1), color_from_palette(fftResult[SEGENV.aux0]*240/(SEGLEN-1), false, PALETTE_SOLID_WRAP, 0), fftResult[SEGENV.aux0]); + leds[segmentToLogical(segLoc)] = color_blend(SEGCOLOR(1), color_from_palette(2*fftResult[SEGENV.aux0 % 16]*240/(SEGLEN-1), false, PALETTE_SOLID_WRAP, 0), 2*fftResult[SEGENV.aux0 % 16]); SEGENV.aux0++; SEGENV.aux0 = SEGENV.aux0 % 16; @@ -6069,16 +6153,16 @@ uint16_t WS2812FX::mode_DJLight(void) { // Written by ??? Adap if (SEGENV.aux0 != secondHand) { // Triggered millis timing. SEGENV.aux0 = secondHand; - leds[realPixelIndex(mid)] = CRGB(fftResult[15]/2, fftResult[5]/2, fftResult[0]/2); // 16-> 15 as 16 is out of bounds - leds[realPixelIndex(mid)].fadeToBlackBy(map(fftResult[1*4], 0, 255, 255, 10)); // TODO - Update + leds[segmentToLogical(mid)] = CRGB(fftResult[15]/2, fftResult[5]/2, fftResult[0]/2); // 16-> 15 as 16 is out of bounds + leds[segmentToLogical(mid)].fadeToBlackBy(map(fftResult[1*4], 0, 255, 255, 10)); // TODO - Update //move to the left for (int i = NUM_LEDS - 1; i > mid; i--) { - leds[realPixelIndex(i)] = leds[realPixelIndex(i - 1)]; + leds[segmentToLogical(i)] = leds[segmentToLogical(i - 1)]; } // move to the right for (int i = 0; i < mid; i++) { - leds[realPixelIndex(i)] = leds[realPixelIndex(i + 1)]; + leds[segmentToLogical(i)] = leds[segmentToLogical(i + 1)]; } } @@ -6095,13 +6179,18 @@ uint16_t WS2812FX::mode_freqmap(void) { // Map FFT_MajorPeak t // Start frequency = 60 Hz and log10(60) = 1.78 // End frequency = 5120 Hz and lo10(5120) = 3.71 + float my_magnitude = FFT_Magnitude / 4.0; + if (soundAgc) my_magnitude *= multAgc; + if (sampleAvg < 1 ) my_magnitude = 0.001; // noise gate closed - mute + fade_out(SEGMENT.speed); - uint16_t locn = (log10(FFT_MajorPeak) - 1.78) * (float)SEGLEN/(3.71-1.78); // log10 frequency range is from 1.78 to 3.71. Let's scale to SEGLEN. + int locn = (log10f(FFT_MajorPeak) - 1.78) * (float)SEGLEN/(3.71-1.78); // log10 frequency range is from 1.78 to 3.71. Let's scale to SEGLEN. if (locn >=SEGLEN) locn = SEGLEN-1; - uint16_t pixCol = (log10((int)FFT_MajorPeak) - 1.78) * 255.0/(3.71-1.78); // Scale log10 of frequency values to the 255 colour index. - uint16_t bright = (int)FFT_Magnitude>>7; + if (locn < 1) locn = 0; + uint16_t pixCol = (log10f(FFT_MajorPeak) - 1.78) * 255.0/(3.71-1.78); // Scale log10 of frequency values to the 255 colour index. + uint16_t bright = (int)my_magnitude; setPixelColor(locn, color_blend(SEGCOLOR(1), color_from_palette(SEGMENT.intensity+pixCol, false, PALETTE_SOLID_WRAP, 0), bright)); @@ -6147,11 +6236,11 @@ uint16_t WS2812FX::mode_freqmatrix(void) { // Freqmatrix. By Andr } // Serial.println(color); - leds[realPixelIndex(0)] = color; + leds[segmentToLogical(0)] = color; // shift the pixels one pixel up for (int i = SEGLEN; i > 0; i--) { // Move up - leds[realPixelIndex(i)] = leds[realPixelIndex(i-1)]; + leds[segmentToLogical(i)] = leds[segmentToLogical(i-1)]; } //fadeval = fade; @@ -6178,12 +6267,17 @@ uint16_t WS2812FX::mode_freqmatrix(void) { // Freqmatrix. By Andr uint16_t WS2812FX::mode_freqpixels(void) { // Freqpixel. By Andrew Tuline. uint16_t fadeRate = 2*SEGMENT.speed - SEGMENT.speed*SEGMENT.speed/255; // Get to 255 as quick as you can. + + float my_magnitude = FFT_Magnitude / 16.0; + if (soundAgc) my_magnitude *= multAgc; + if (sampleAvg < 1 ) my_magnitude = 0.001; // noise gate closed - mute + fade_out(fadeRate); for (int i=0; i < SEGMENT.intensity/32+1; i++) { uint16_t locn = random16(0,SEGLEN); - uint8_t pixCol = (log10((int)FFT_MajorPeak) - 1.78) * 255.0/(3.71-1.78); // Scale log10 of frequency values to the 255 colour index. - setPixelColor(locn, color_blend(SEGCOLOR(1), color_from_palette(SEGMENT.intensity+pixCol, false, PALETTE_SOLID_WRAP, 0), (int)FFT_Magnitude>>8)); + uint8_t pixCol = (log10f(FFT_MajorPeak) - 1.78) * 255.0/(3.71-1.78); // Scale log10 of frequency values to the 255 colour index. + setPixelColor(locn, color_blend(SEGCOLOR(1), color_from_palette(SEGMENT.intensity+pixCol, false, PALETTE_SOLID_WRAP, 0), (int)my_magnitude)); } return FRAMETIME; } // mode_freqpixels() @@ -6222,11 +6316,13 @@ uint16_t WS2812FX::mode_freqwave(void) { // Freqwave. By Andrea //uint8_t fade = SEGMENT.custom3; //uint8_t fadeval; - double sensitivity = mapf(SEGMENT.custom3, 1, 255, 1, 10); - int pixVal = sampleAvg * SEGMENT.intensity / 256 * sensitivity; + float tmpSound = (soundAgc) ? sampleAgc : sampleAvg; + + float sensitivity = mapf(SEGMENT.custom3, 1, 255, 1, 10); + float pixVal = tmpSound * (float)SEGMENT.intensity / 256.0 * sensitivity; if (pixVal > 255) pixVal = 255; - double intensity = map(pixVal, 0, 255, 0, 100) / 100.0; // make a brightness from the last avg + float intensity = mapf(pixVal, 0, 255, 0, 100) / 100.0; // make a brightness from the last avg CRGB color = 0; CHSV c; @@ -6242,21 +6338,21 @@ uint16_t WS2812FX::mode_freqwave(void) { // Freqwave. By Andrea int upperLimit = 20 * SEGMENT.custom2; int lowerLimit = 2 * SEGMENT.custom1; int i = lowerLimit!=upperLimit?map(FFT_MajorPeak, lowerLimit, upperLimit, 0, 255):FFT_MajorPeak; - uint16_t b = 255 * intensity; + uint16_t b = 255.0 * intensity; if (b > 255) b=255; c = CHSV(i, 240, (uint8_t)b); color = c; // implicit conversion to RGB supplied by FastLED } // Serial.println(color); - leds[realPixelIndex(SEGLEN/2)] = color; + leds[segmentToLogical(SEGLEN/2)] = color; // shift the pixels one pixel outwards for (int i = SEGLEN; i > SEGLEN/2; i--) { // Move to the right. - leds[realPixelIndex(i)] = leds[realPixelIndex(i-1)]; + leds[segmentToLogical(i)] = leds[segmentToLogical(i-1)]; } for (int i = 0; i < SEGLEN/2; i++) { // Move to the left. - leds[realPixelIndex(i)] = leds[realPixelIndex(i+1)]; + leds[segmentToLogical(i)] = leds[segmentToLogical(i+1)]; } // DISPLAY ARRAY @@ -6279,9 +6375,12 @@ uint16_t WS2812FX::mode_gravfreq(void) { // Gravfreq. By Andrew fade_out(240); - float segmentSampleAvg = sampleAvg * SEGMENT.intensity / 255; + float tmpSound = (soundAgc) ? sampleAgc : sampleAvg; + float segmentSampleAvg = tmpSound * (float)SEGMENT.intensity / 255.0; + segmentSampleAvg *= 0.125; // divide by 8, to compensate for later "sensitivty" upscaling - int tempsamp = constrain(segmentSampleAvg*2,0,SEGLEN/2); // Keep the sample from overflowing. + float mySampleAvg = mapf(segmentSampleAvg*2.0, 0,32, 0, (float)SEGLEN/2.0); // map to pixels availeable in current segment + int tempsamp = constrain(mySampleAvg,0,SEGLEN/2); // Keep the sample from overflowing. uint8_t gravity = 8 - SEGMENT.speed/32; for (int i=0; i 500) volTemp = 255; // We need to squelch out the background noise. + + float my_magnitude = FFT_Magnitude / 16.0; // scale magnitude to be aligned with scaling of FFT bins + if (soundAgc) my_magnitude *= multAgc; // apply gain + if (sampleAvg < 1 ) my_magnitude = 0.001; // mute + + if (my_magnitude > 32) volTemp = 255; // We need to squelch out the background noise. while ( frTemp > 249 ) { octCount++; // This should go up to 5. @@ -6350,13 +6454,13 @@ uint16_t WS2812FX::mode_rocktaves(void) { // Rocktaves. Same not } frTemp -=132; // This should give us a base musical note of C3 - frTemp = abs(frTemp * 2.1); // Fudge factors to compress octave range starting at 0 and going to 255; + frTemp = fabs(frTemp * 2.1); // Fudge factors to compress octave range starting at 0 and going to 255; // Serial.print(frTemp); Serial.print("\t"); Serial.print(volTemp); Serial.print("\t");Serial.print(octCount); Serial.print("\t"); Serial.println(FFT_Magnitude); // leds[beatsin8(8+octCount*4,0,SEGLEN-1,0,octCount*8)] += CHSV((uint8_t)frTemp,255,volTemp); // Back and forth with different frequencies and phase shift depending on current octave. - leds[realPixelIndex(map(beatsin8(8+octCount*4,0,SEGLEN-1,0,octCount*8),0,255,0,SEGLEN))] += color_blend(SEGCOLOR(1), color_from_palette((uint8_t)frTemp, false, PALETTE_SOLID_WRAP, 0), volTemp); + leds[segmentToLogical(mapf(beatsin8(8+octCount*4,0,255,0,octCount*8),0,255,0,SEGLEN-1))] += color_blend(SEGCOLOR(1), color_from_palette((uint8_t)frTemp, false, PALETTE_SOLID_WRAP, 0), volTemp); setPixels(leds); @@ -6383,14 +6487,18 @@ uint16_t WS2812FX::mode_waterfall(void) { // Waterfall. By: An if (SEGENV.aux0 != secondHand) { // Triggered millis timing. SEGENV.aux0 = secondHand; + float my_magnitude = FFT_Magnitude / 8.0; + if (soundAgc) my_magnitude *= multAgc; + if (sampleAvg < 1 ) my_magnitude = 0.001; // noise gate closed - mute + uint8_t pixCol = (log10((int)FFT_MajorPeak) - 2.26) * 177; // log10 frequency range is from 2.26 to 3.7. Let's scale accordingly. if (samplePeak) { - leds[realPixelIndex(SEGLEN-1)] = CHSV(92,92,92); + leds[segmentToLogical(SEGLEN-1)] = CHSV(92,92,92); } else { - leds[realPixelIndex(SEGLEN-1)] = color_blend(SEGCOLOR(1), color_from_palette(pixCol+SEGMENT.intensity, false, PALETTE_SOLID_WRAP, 0), (int)FFT_Magnitude>>8); + leds[segmentToLogical(SEGLEN-1)] = color_blend(SEGCOLOR(1), color_from_palette(pixCol+SEGMENT.intensity, false, PALETTE_SOLID_WRAP, 0), (int)my_magnitude); } - for (int i=0; i= 255 - SEGMENT.intensity) { @@ -6431,46 +6526,53 @@ uint16_t WS2812FX::GEQ_base(int NUMB_BANDS, bool centered_horizontal, bool cente else rippleTime = false; - static int previousBarHeight[16]; //array of previous bar heights per frequency band + static int previousBarHeight[64]; //array of previous bar heights per frequency band + + int xCount = SEGMENT.width; + if (centered_vertical) xCount /= 2; - int b = 0; - for (int band = 0; band < NUMB_BANDS; band += bandInc) { + for (int x=0; x < xCount; x++) { + int band = map(x, 0, xCount-1, 0, 15); int barHeight = map(fftResult[band], 0, 255, 0, SEGMENT.height); if ((barHeight % 2 == 1) && centered_horizontal) barHeight++; //get an even barHeight if centered_horizontal int yStartBar = centered_horizontal?(SEGMENT.height - barHeight) / 2:0; //lift up the bar if centered_horizontal - int yStartPeak = centered_horizontal?(SEGMENT.height - previousBarHeight[band]) / 2:0; //lift up the peaks if centered_horizontal + int yStartPeak = centered_horizontal?(SEGMENT.height - previousBarHeight[x]) / 2:0; //lift up the peaks if centered_horizontal - for (int w = 0; w < barWidth; w++) { - int x = (barWidth * b) + w; - for (int y=0; y= yStartBar && y < yStartBar + barHeight) { - if (color_vertical) - color = color_from_palette(map(y, 0, SEGMENT.height - 1, 0, 255), false, PALETTE_SOLID_WRAP, 0); - else - color = color_from_palette(band * 35, false, PALETTE_SOLID_WRAP, 0); - } + if (centered_horizontal) + colorIndex = map(abs(y - (SEGMENT.height - 1)/2.0), 0, SEGMENT.height/2 - 1, 0, 255); + else + colorIndex = map(y, 0, SEGMENT.height - 1, 0, 255); + } + else + colorIndex = band * 17; + heightColor = color_from_palette(colorIndex, false, PALETTE_SOLID_WRAP, 0); - //low and high peak (must exist && on peak position && only below if centered_horizontal effect) - if ((previousBarHeight[band] > 0) && (SEGMENT.intensity < 255) && (y==yStartPeak || y==yStartPeak + previousBarHeight[band]-1) && (centered_horizontal || y!=yStartPeak)) - color = SEGCOLOR(2)==CRGB::Black?color_from_palette((band * 35), false, PALETTE_SOLID_WRAP, 0):SEGCOLOR(2); //low peak + CRGB ledColor = CRGB::Black; //if not part of bars or peak, make black (not fade to black) - if (centered_vertical) { - leds[XY(SEGMENT.width / 2 - 1 + x, SEGMENT.height - 1 - y)] = color; - leds[XY(SEGMENT.width / 2 - 1 - x, SEGMENT.height - 1 - y)] = color; - } - else { - leds[XY(x, SEGMENT.height - 1 - y)] = color; - } + //bar + if (y >= yStartBar && y < yStartBar + barHeight) + ledColor = heightColor; + + //low and high peak (must exist && on peak position && only below if centered_horizontal effect) + if ((previousBarHeight[x] > 0) && (SEGMENT.intensity < 255) && (y==yStartPeak || y==yStartPeak + previousBarHeight[x]-1) && (centered_horizontal || y!=yStartPeak)) + ledColor = SEGCOLOR(2)==CRGB::Black?heightColor:CRGB(SEGCOLOR(2)); //low peak + + if (centered_vertical) { + leds[XY(SEGMENT.width / 2 + x, SEGMENT.height - 1 - y)] = ledColor; + leds[XY(SEGMENT.width / 2 - 1 - x, SEGMENT.height - 1 - y)] = ledColor; } - } //barWidth - b++; + else + leds[XY(x, SEGMENT.height - 1 - y)] = ledColor; + } - if (rippleTime) previousBarHeight[band]-=centered_horizontal?2:1; //delay/ripple effect - if (barHeight > previousBarHeight[band]) previousBarHeight[band] = barHeight; //drive the peak up + if (rippleTime) previousBarHeight[x] -= centered_horizontal?2:1; //delay/ripple effect + if (barHeight > previousBarHeight[x]) previousBarHeight[x] = barHeight; //drive the peak up } setPixels(leds); @@ -6478,7 +6580,7 @@ uint16_t WS2812FX::GEQ_base(int NUMB_BANDS, bool centered_horizontal, bool cente } //GEQ_base uint16_t WS2812FX::mode_2DGEQ(void) { // By Will Tatam. Code reduction by Ewoud Wijma. - return GEQ_base(map(SEGMENT.custom1, 0, 255, 1, 16), false, false, false); + return GEQ_base(false, false, false); } // mode_2DGEQ() @@ -6487,7 +6589,7 @@ uint16_t WS2812FX::mode_2DGEQ(void) { // By Will Tatam. Code ///////////////////////// uint16_t WS2812FX::mode_2DCenterBars(void) { // Written by Scott Marley Adapted by Spiro-C.. - return GEQ_base(16, SEGMENT.custom1 > 128, SEGMENT.custom2 > 128, SEGMENT.custom3 > 128); + return GEQ_base(SEGMENT.custom1 > 128, SEGMENT.custom2 > 128, SEGMENT.custom3 > 128); } // mode_2DCenterBars() @@ -6627,4 +6729,4 @@ uint16_t WS2812FX::mode_2DAkemi(void) { setPixels(leds); return FRAMETIME; -} // mode_2DAkemi \ No newline at end of file +} // mode_2DAkemi diff --git a/wled00/FX.h b/wled00/FX.h index 60e2c0bf26..f9a54a9ca1 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -46,6 +46,11 @@ #define MAX(a,b) ((a)>(b)?(a):(b)) #endif +//color mangling macros +#ifndef RGBW32 +#define RGBW32(r,g,b,w) (uint32_t((byte(w) << 24) | (byte(r) << 16) | (byte(g) << 8) | (byte(b)))) +#endif + /* Not used in all effects yet */ #define WLED_FPS 42 #define FRAMETIME_FIXED (1000/WLED_FPS) @@ -71,7 +76,6 @@ assuming each segment uses the same amount of data. 256 for ESP8266, 640 for ESP32. */ #define FAIR_DATA_PER_SEG (MAX_SEGMENT_DATA / MAX_NUM_SEGMENTS) -#define LED_SKIP_AMOUNT 1 // NEED WORKAROUND TO ACCESS PRIVATE CLASS VARIABLE '_frametime' #define MIN_SHOW_DELAY (_frametime < 16 ? 8 : 15) @@ -332,7 +336,7 @@ class WS2812FX { public: // FastLED array, so we can refer to leds[i] instead of getPixel() and setPixel() - CRGB leds[MAX_LEDS+1]; // See const.h for a value of 1500. The plus 1 is just in case we go over with XY(). + CRGB leds[MAX_LEDS+1]; // See const.h for a value of 1500. The plus 1 is just in case we go over with XY(). typedef struct Segment { // 31 (32 in memory) bytes uint16_t start; @@ -432,7 +436,6 @@ class WS2812FX { vLength = (vLength + 1) /2; // divide by 2 if mirror, leave at least a single LED return vLength; } - uint8_t differs(Segment& b); inline uint8_t getLightCapabilities() {return _capabilities;} void refreshLightCapabilities(); @@ -804,7 +807,7 @@ class WS2812FX { _brightness = DEFAULT_BRIGHTNESS; currentPalette = CRGBPalette16(CRGB::Black); targetPalette = CloudColors_p; - ablMilliampsMax = 850; + ablMilliampsMax = ABL_MILLIAMPS_DEFAULT; currentMilliamps = 0; timebase = 0; resetSegments(); @@ -834,12 +837,13 @@ class WS2812FX { resetSegments(), makeAutoSegments(bool forceReset = false), fixInvalidSegments(), - setPixelColor(uint16_t n, uint32_t c), setPixelColor(uint16_t n, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0), show(void), setTargetFps(uint8_t fps), deserializeMap(uint8_t n=0); + inline void setPixelColor(uint16_t n, uint32_t c) {setPixelColor(n, byte(c>>16), byte(c>>8), byte(c), byte(c>>24));} + bool gammaCorrectBri = false, gammaCorrectCol = true, @@ -1136,7 +1140,7 @@ class WS2812FX { // mode_2DCAElementary(void); uint16_t - GEQ_base(int, bool, bool, bool); //private??? + GEQ_base(bool, bool, bool); //private??? uint16_t _lengthRaw; //private? not in AC (anymore) @@ -1237,9 +1241,8 @@ class WS2812FX { friend class ColorTransition; uint16_t - realPixelIndex(uint16_t i), + segmentToLogical(uint16_t i), transitionProgress(uint8_t tNr); - public: inline bool hasWhiteChannel(void) {return _hasWhiteChannel;} inline bool isOffRefreshRequired(void) {return _isOffRefreshRequired;} @@ -1250,16 +1253,17 @@ class WS2812FX { // Technical notes // =============== // If an effect name is followed by an @, slider and color control is effective. -// See setSliderAndColorControl in index.js for implementation +// See setEffectParameters in index.js for implementation // If not effective then: // - For AC effects (id<128) 2 sliders and 3 colors and the palette will be shown -// - For SR effects (id<128) 5 sliders and 3 colors and the palette will be shown +// - For SR effects (id>128) 5 sliders and 3 colors and the palette will be shown // If effective (@) // - a ; seperates slider controls (left) from color controls (middle) and palette control (right) // - if left, middle or right is empty no controls are shown // - a , seperates slider controls (max 5) or color controls (max 3). Palette has only one value // - a ! means that the default is used. // - For sliders: Effect speeds, Effect intensity, Custom 1, Custom 2, Custom 3 +// - Blazoncek default values: e.g. Sensitivity=128 // - For colors: Fx color, Background color, Custom // - For palette: prompt Color palette // @@ -1272,61 +1276,61 @@ class WS2812FX { const char JSON_mode_names[] PROGMEM = R"=====([ "Solid@;!;", -"Blink@;!;!", -"Breathe@Speed;,!;!", -"Wipe", -"Wipe Random", -"Random Colors", -"Sweep", -"Dynamic", -"Colorloop", -"Rainbow", -"Scan@!,# of dots;,!,?;!", -"Scan Dual@!,# of dots;,!,?;!", -"Fade", -"Theater", -"Theater Rainbow", -"Running", -"Saw", -"Twinkle", -"Dissolve", -"Dissolve Rnd", -"Sparkle", -"Sparkle Dark", -"Sparkle+", -"Strobe", -"Strobe Rainbow", -"Strobe Mega", -"Blink Rainbow", -"Android", -"Chase", -"Chase Random", -"Chase Rainbow", -"Chase Flash", -"Chase Flash Rnd", -"Rainbow Runner", -"Colorful", -"Traffic Light", +"Blink@!,;!,!,;!", +"Breathe@!,;!,!;!", +"Wipe@!,!;!,!,;!", +"Wipe Random@!,;1,2,3;!", +"Random Colors@!,Fade time;1,2,3;!", +"Sweep@!,!;!,!,;!", +"Dynamic@!,!;1,2,3;!", +"Colorloop@!,Saturation;1,2,3;!", +"Rainbow@!,Size;1,2,3;!", +"Scan@!,# of dots;!,!,;!", +"Scan Dual@!,# of dots;!,!,;!", +"Fade@!,;!,!,;!", +"Theater@!,Gap size;!,!,;!", +"Theater Rainbow@!,Gap size;1,2,3;!", +"Running@!,Wave width;!,!,;!", +"Saw@!,Width;!,!,;!", +"Twinkle@!,;!,!,;!", +"Dissolve@Repeat speed,Dissolve speed;!,!,;!", +"Dissolve Rnd@Repeat speed,Dissolve speed;,!,;!", +"Sparkle@!,;!,!,;!", +"Sparkle Dark@!,!;Bg,Fx,;!", +"Sparkle+@!,!;Bg,Fx,;!", +"Strobe@!,;!,!,;!", +"Strobe Rainbow@!,;,!,;!", +"Strobe Mega@!,!;!,!,;!", +"Blink Rainbow@Frequency,Blink duration;!,!,;!", +"Android@!,Width;!,!,;!", +"Chase@!,Width;!,!,!;!", +"Chase Random@!,Width;!,,!;!", +"Chase Rainbow@!,Width;!,!,;0", +"Chase Flash@!,;Bg,Fx,!;!", +"Chase Flash Rnd@!,;,Fx,;!", +"Rainbow Runner@!,Size;Bg,,;!", +"Colorful@!,Saturation;1,2,3;!", +"Traffic Light@!,;,!,;!", "Sweep Random", -"Chase 2", -"Aurora", +"Chase 2@!,Width;!,!,;!", +"Aurora@!=24,!;1,2,3;!=50", "Stream", "Scanner", "Lighthouse", -"Fireworks", -"Rain", -"Tetrix", -"Fire Flicker", -"Gradient", -"Loading", -"Police", +"Fireworks@Sharpness=96,Frequency=192;!,2,;!=11", +"Rain@Fade rate=128,Frequency=128;!,2,;!", +"Tetrix@!=224,Width=0;!,!,;!=11", +"Fire Flicker@!,!;!,,;!", +"Gradient@!,Spread=16;!,!,;!", +"Loading@!,Fade=16;!,!,;!", +"Police@!,Width;,Bg,;0", "Fairy", -"Two Dots", -"Fairytwinkle", +"Two Dots@!,Dot size;1,2,Bg;!", +"Fairy Twinkle", "Running Dual", "Halloween", -"Tri Chase", -"Tri Wipe", +"Tri Chase@!,Size;1,2,3;0", +"Tri Wipe@!,Width;1,2,3;0", "Tri Fade", "Lightning", "ICU", @@ -1334,59 +1338,59 @@ const char JSON_mode_names[] PROGMEM = R"=====([ "Scanner Dual", "Stream 2", "Oscillate", -"Pride 2015", -"Juggle", -"Palette", -"Fire 2012", +"Pride 2015@!,;;", +"Juggle@!=16,Trail=240;!,!,;!", +"Palette@!,;1,2,3;!", +"Fire 2012@Spark rate=120,Decay=64;1,2,3;!", "Colorwaves", -"Bpm", +"Bpm@!=64,;1,2,3;!", "Fill Noise", "Noise 1", "Noise 2", "Noise 3", "Noise 4", -"Colortwinkles", -"Lake", -"Meteor", -"Meteor Smooth", +"Colortwinkles@Fade speed,Spawn speed;1,2,3;!", +"Lake@!,;1,2,3;!", +"Meteor@!,Trail length;!,!,;!", +"Meteor Smooth@!,Trail length;!,!,;!", "Railway", "Ripple", "Twinklefox", "Twinklecat", "Halloween Eyes", -"Solid Pattern", -"Solid Pattern Tri", -"Spots", -"Spots Fade", +"Solid Pattern@Fg size,Bg size;Fg,Bg,;!=0", +"Solid Pattern Tri@,Size;1,2,3;!=0", +"Spots@Spread,Width;!,!,;!", +"Spots Fade@Spread,Width;!,!,;!", "Glitter", -"Candle", +"Candle@Flicker rate=96,Flicker intensity=224;!,!,;0", "Fireworks Starburst", -"Fireworks 1D@Gravity,Firing side;;!", -"Bouncing Balls", +"Fireworks 1D@Gravity,Firing side;!,!,;!", +"Bouncing Balls@Gravity,# of balls;!,!,;!", "Sinelon", "Sinelon Dual", "Sinelon Rainbow", "Popcorn", "Drip@Gravity,# of drips;!,!;!", -"Plasma", -"Percent", +"Plasma@Phase,;1,2,3;!", +"Percent@,% of fill;!,!,;!", "Ripple Rainbow", -"Heartbeat", +"Heartbeat@!,!;!,!,;!", "Pacifica", -"Candle Multi", -"Solid Glitter", -"Sunrise", +"Candle Multi@Flicker rate=96,Flicker intensity=224;!,!,;0", +"Solid Glitter@,!;!,,;0", +"Sunrise@Time [min]=60,;;0", "Phased", -"Twinkleup@Speed,Intensity,Min;,!;!", +"Twinkleup@!,Intensity;!,!,;!", "Noise Pal", "Sine", "Phased Noise", "Flow", -"Chunchun", -"Dancing Shadows", +"Chunchun@!,Gap size;!,!,;!", +"Dancing Shadows@!,# of shadows;!,,;!", "Washing Machine", -"Candy Cane", -"Blends", +"Candy Cane@!,Width;;", +"Blends@Shift speed,Blend speed;1,2,3,!", "TV Simulator", "Dynamic Smooth", "Reserved0", @@ -1400,17 +1404,17 @@ const char JSON_mode_names[] PROGMEM = R"=====([ "Reserved8", "Reserved9", " ♪ Pixels@Fade rate,# of pixels;,!;!", -" ♪ Pixelwave@!,Sensitivity;!,!;!", +" ♪ Pixelwave@!,Sensitivity=64;!,!;!", " ♪ Juggles@!,# of balls;,!;!", -" ♪ Matripix@!,Brightness;,!;!", -" ♪ Gravimeter@Rate of fall,Sensitivity;,!;!", -" ♪ Plasmoid@,# of pixels;!,!;!", +" ♪ Matripix@!,Brightness=64;,!;!", +" ♪ Gravimeter@Rate of fall,Sensitivity=128;,!;!", +" ♪ Plasmoid@Phase=128,# of pixels=128;,!;!", " ♪ Puddles@Fade rate,Puddle size;!,!;!", -" ♪ Midnoise@Fade rate,Maximum length;,!;!", -" ♪ Noisemeter@Fade rate,Width;!,!;!", +" ♪ Midnoise@Fade rate,Maximum length=128;,!;!", +" ♪ Noisemeter@Fade rate,Width=128;!,!;!", " ♫ Freqwave@Time delay,Sound effect,Low bin,High bin,Pre-amp;;", " ♫ Freqmatrix@Time delay,Sound effect,Low bin,High bin,Sensivity;;", -" ♫ 2D GEQ@Bar speed,Ripple decay,Bands;,,Peak Color;!", +" ♫ 2D GEQ@Bar speed,Ripple decay;,,Peak Color;!", " ♫ Waterfall@!,Adjust color,,Select bin, Volume (minimum);!,!;!", " ♫ Freqpixels@Fade rate,Starting colour and # of pixels;;", " ♫ Binmap@;!,!;!", @@ -1427,26 +1431,26 @@ const char JSON_mode_names[] PROGMEM = R"=====([ "2D Matrix@Falling speed,Spawning rate,Trail,Custom color ☑;Spawn,Trail;", "2D Metaballs@;;", " ♫ Freqmap@Fade rate,Starting color;,!;!", -" ♪ Gravcenter@Rate of fall,Sensitivity;,!;!", -" ♪ Gravcentric@Rate of fall,Sensitivity;!;!", -" ♫ Gravfreq@Rate of fall,Sensivity;,!;!", +" ♪ Gravcenter@Rate of fall,Sensitivity=128;,!;!", +" ♪ Gravcentric@Rate of fall,Sensitivity=128;!;!", +" ♫ Gravfreq@Rate of fall,Sensivity=128;,!;!", " ♫ DJ Light@Speed;;", " ♫ 2D Funky Plank@Scroll speed,,# of bands;;", -" ♫ 2D CenterBars@Bar speed,Ripple decay,Center ↔ ☑,Center ↕ ☑, Color ↕ ☑;,,Peak Color;!", +" ♫ 2D CenterBars@Bar speed=250,Ripple decay=250,Center ↔ ☑=192,Center ↕ ☑=192, Color ↕ ☑=192;,,Peak Color;!=11", "2D Pulser@Speed,Blur;;!", " ♫ Blurz@Fade rate,Blur amount;,Color mix;!", "2D Drift@Rotation speed,Blur amount;;!", -" ♪ 2D Waverly@Amplification,Sensitivity;;!", +" ♪ 2D Waverly@Amplification,Sensitivity=64;;!", "2D Sun Radiation@Variance,Brightness;;", "2D Colored Bursts@Speed,Number of lines;;!", "2D Julia@,Max iterations per pixel,X center,Y center,Area size;;!", "Reserved for PoolNoise", "Reserved for Twister", "Reserved for Elementary", -"2D Game Of Life@!,Palette ☑;!,!;!", +"2D Game Of Life@!,Use Palette ☑;!,!;!", "2D Tartan@X scale,Y scale;;!", "2D Polar Lights@Speed,X scale,Palette;;", -" ♪ 2D Swirl@!,Sensitivity,Blur;,Bg Swirl;!", +" ♪ 2D Swirl@!,Sensitivity=64,Blur;,Bg Swirl;!", "2D Lissajous@X frequency,Fadetime;;!", "2D Frizzles@X frequency,Y frequency;;!", "2D Plasma Ball@Speed;;!", diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 077a4b1a31..839bb998f1 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -60,6 +60,11 @@ #define DEFAULT_LED_TYPE TYPE_WS2812_RGB #endif +#ifndef DEFAULT_LED_COLOR_ORDER + #define DEFAULT_LED_COLOR_ORDER COL_ORDER_GRB //default to GRB +#endif + + #if MAX_NUM_SEGMENTS < WLED_MAX_BUSSES #error "Max segments must be at least max number of busses!" #endif @@ -77,6 +82,7 @@ void WS2812FX::finalizeInit(void) //if busses failed to load, add default (fresh install, FS issue, ...) if (busses.getNumBusses() == 0) { + DEBUG_PRINTLN(F("No busses, init default")); const uint8_t defDataPins[] = {DATA_PINS}; const uint16_t defCounts[] = {PIXEL_COUNTS}; const uint8_t defNumBusses = ((sizeof defDataPins) / (sizeof defDataPins[0])); @@ -87,7 +93,7 @@ void WS2812FX::finalizeInit(void) uint16_t start = prevLen; uint16_t count = defCounts[(i < defNumCounts) ? i : defNumCounts -1]; prevLen += count; - BusConfig defCfg = BusConfig(DEFAULT_LED_TYPE, defPin, start, count, COL_ORDER_GRB); + BusConfig defCfg = BusConfig(DEFAULT_LED_TYPE, defPin, start, count, DEFAULT_LED_COLOR_ORDER); busses.add(defCfg); } } @@ -128,6 +134,8 @@ void WS2812FX::service() { for(uint8_t i=0; i < MAX_NUM_SEGMENTS; i++) { + //if (realtimeMode && useMainSegmentOnly && i == getMainSegmentId()) continue; + _segment_index = i; // reset the segment runtime data if needed, called before isActive to ensure deleted @@ -195,32 +203,28 @@ void WS2812FX::service() { _triggered = false; } -void IRAM_ATTR WS2812FX::setPixelColor(uint16_t n, uint32_t c) { - setPixelColor(n, R(c), G(c), B(c), W(c)); -} - -// used to map from segment index to logical pixel, taking into account grouping, offsets, reverse and mirroring -uint16_t IRAM_ATTR WS2812FX::realPixelIndex(uint16_t i) { // ewowi20210703: will not map to physical pixel index but to rotated and mirrored logical pixel index as matrix panels will require mapping. - // Mapping is done in logicalToPhysical below. Function will not be renamed to keep it consistent with Aircoookie +// WLEDSR used to map from segment index to logical pixel, taking into account grouping, offsets, reverse and mirroring +uint16_t IRAM_ATTR WS2812FX::segmentToLogical(uint16_t i) { // ewowi20210703: will not map to physical pixel index but to rotated and mirrored logical pixel index as matrix panels will require mapping. + // Mapping is done in logicalToPhysical below. int16_t iGroup = i * SEGMENT.groupLength(); /* reverse just an individual segment */ - int16_t realIndex = iGroup; + int16_t logicalIndex = iGroup; if (IS_REVERSE && stripOrMatrixPanel == 0) { // in case of 1D if (IS_MIRROR) { - realIndex = (SEGMENT.length() -1) / 2 - iGroup; // only need to index half the pixels + logicalIndex = (SEGMENT.length() -1) / 2 - iGroup; // only need to index half the pixels } else { - realIndex = (SEGMENT.length() - 1) - iGroup; + logicalIndex = (SEGMENT.length() - 1) - iGroup; } } // segment index to segment XY - uint16_t x = realIndex; + uint16_t x = logicalIndex; uint16_t y = 0; if (SEGMENT.width) { // ewowi20210624: in case of 2D: index needs to be mapped from segment index to matrix index. Also works for 1D strips // need to check SEGMENT.width as it looks like Peek is using segment 15 with Width=0 - x = realIndex % SEGMENT.width; - y = realIndex / SEGMENT.width; + x = logicalIndex % SEGMENT.width; + y = logicalIndex / SEGMENT.width; } // ewowi20210703: apply rotation, mirrorX and mirrorY. @@ -244,17 +248,16 @@ uint16_t IRAM_ATTR WS2812FX::realPixelIndex(uint16_t i) { // ewowi20210703: will } // segment XY to rotated and mirrored logical index - realIndex = newX + SEGMENT.startX + (newY + SEGMENT.startY) * matrixWidth; + logicalIndex = newX + SEGMENT.startX + (newY + SEGMENT.startY) * matrixWidth; - return realIndex; + return logicalIndex; } void IRAM_ATTR WS2812FX::setPixelColor(uint16_t i, byte r, byte g, byte b, byte w) { - if (SEGLEN) {//from segment - uint16_t realIndex = realPixelIndex(i); // ewowi20210624: from segment index to logical index - uint16_t len = SEGMENT.length(); + uint8_t segIdx; + if (SEGLEN) { // SEGLEN!=0 -> from segment/FX //color_blend(getpixel, col, _bri_t); (pseudocode for future blending of segments) if (_bri_t < 255) { r = scale8(r, _bri_t); @@ -262,11 +265,29 @@ void IRAM_ATTR WS2812FX::setPixelColor(uint16_t i, byte r, byte g, byte b, byte b = scale8(b, _bri_t); w = scale8(w, _bri_t); } + segIdx = _segment_index; + } else // from live/realtime + segIdx = _mainSegment; + + if (SEGLEN || (realtimeMode && useMainSegmentOnly)) { uint32_t col = RGBW32(r, g, b, w); + uint16_t len = _segments[segIdx].length(); + uint16_t logicalIndex = segmentToLogical(i); // ewowi20210624: from segment index to logical index + + // get physical pixel address (taking into account start, grouping, spacing [and offset]) + i = i * _segments[segIdx].groupLength(); + if (_segments[segIdx].options & REVERSE) { // is segment reversed? + if (_segments[segIdx].options & MIRROR) { // is segment mirrored? + i = (len - 1) / 2 - i; //only need to index half the pixels + } else { + i = (len - 1) - i; + } + } + i += _segments[segIdx].start; /* Set all the pixels in the group */ for (uint16_t j = 0; j < SEGMENT.grouping; j++) { - uint16_t indexSet = realIndex + (IS_REVERSE ? -j : j); + uint16_t indexSet = logicalIndex + (IS_REVERSE ? -j : j); if (indexSet >= SEGMENT.start && indexSet < SEGMENT.stop) { if (IS_MIRROR) { // set the corresponding mirrored pixel uint16_t indexMir = SEGMENT.stop - indexSet + SEGMENT.start - 1; @@ -274,19 +295,18 @@ void IRAM_ATTR WS2812FX::setPixelColor(uint16_t i, byte r, byte g, byte b, byte indexMir += SEGMENT.offset; if (indexMir >= SEGMENT.stop) indexMir -= len; + if (indexMir >= _segments[segIdx].stop) indexMir -= len; if (indexMir < customMappingSize) indexMir = customMappingTable[indexMir]; busses.setPixelColor(logicalToPhysical(indexSet), col); busses.setPixelColor(logicalToPhysical(indexMir), col); // ewowi20210624: logicalToPhysical: Maps logical led index to physical led index. } - /* offset/phase */ - indexSet += SEGMENT.offset; - if (indexSet >= SEGMENT.stop) indexSet -= len; + indexSet += _segments[segIdx].offset; // offset/phase if (indexSet < customMappingSize) indexSet = customMappingTable[indexSet]; // This line is also on L292 busses.setPixelColor(logicalToPhysical(indexSet), col); } } - } else { //live data, etc. + } else { if (i < customMappingSize) i = customMappingTable[i]; busses.setPixelColor(i, RGBW32(r, g, b, w)); busses.setPixelColor(logicalToPhysical(i), RGBW32(r, g, b, w)); // ewowi20210624: logicalToPhysical: Maps logical led index to physical led index. @@ -597,7 +617,13 @@ uint8_t WS2812FX::getActiveSegmentsNum(void) { uint32_t WS2812FX::getPixelColor(uint16_t i) { - i = realPixelIndex(i); + // get physical pixel + i = i * SEGMENT.groupLength();; + if (IS_REVERSE) { + if (IS_MIRROR) i = (SEGMENT.length() - 1) / 2 - i; //only need to index half the pixels + else i = (SEGMENT.length() - 1) - i; + } + i += SEGMENT.start; if (SEGLEN) { /* offset/phase */ @@ -612,7 +638,7 @@ uint32_t WS2812FX::getPixelColor(uint16_t i) } WS2812FX::Segment& WS2812FX::getSegment(uint8_t id) { - if (id >= MAX_NUM_SEGMENTS) return _segments[0]; + if (id >= MAX_NUM_SEGMENTS) return _segments[getMainSegmentId()]; return _segments[id]; } @@ -661,7 +687,6 @@ uint8_t WS2812FX::Segment::differs(Segment& b) { if ((options & 0b00101110) != (b.options & 0b00101110)) d |= SEG_DIFFERS_OPT; if ((options & 0x01) != (b.options & 0x01)) d |= SEG_DIFFERS_SEL; - for (uint8_t i = 0; i < NUM_COLORS; i++) { if (colors[i] != b.colors[i]) d |= SEG_DIFFERS_COL; @@ -911,7 +936,6 @@ void WS2812FX::makeAutoSegments(bool forceReset) { setSegment(i, 0, 0); } } - if (getActiveSegmentsNum() < 2) { setSegment(mainSeg, 0, _length); } @@ -1445,4 +1469,4 @@ WS2812FX* WS2812FX::instance = nullptr; //Bus static member definition, would belong in bus_manager.cpp int16_t Bus::_cct = -1; uint8_t Bus::_cctBlend = 0; -uint8_t Bus::_autoWhiteMode = RGBW_MODE_DUAL; \ No newline at end of file +uint8_t Bus::_autoWhiteMode = RGBW_MODE_DUAL; diff --git a/wled00/audio_reactive.h b/wled00/audio_reactive.h index ce64df1530..9cbccc16db 100644 --- a/wled00/audio_reactive.h +++ b/wled00/audio_reactive.h @@ -19,12 +19,15 @@ #include #include "audio_source.h" -AudioSource *audioSource; +static AudioSource *audioSource; +static volatile bool disableSoundProcessing = false; // if true, sound processing (FFT, filters, AGC) will be suspended. "volatile" as its shared between tasks. // ALL AUDIO INPUT PINS DEFINED IN wled.h AND CONFIGURABLE VIA UI // Comment/Uncomment to toggle usb serial debugging -// #define SR_DEBUG +// #define MIC_LOGGER // MIC sampling & sound input debugging (serial plotter) +// #define FFT_SAMPLING_LOG // FFT result debugging +// #define SR_DEBUG // generic SR DEBUG messages #ifdef SR_DEBUG #define DEBUGSR_PRINT(x) Serial.print(x) @@ -36,51 +39,98 @@ AudioSource *audioSource; #define DEBUGSR_PRINTF(x...) #endif -// #define MIC_LOGGER -// #define MIC_SAMPLING_LOG -// #define FFT_SAMPLING_LOG +// legacy support +#if defined(SR_DEBUG) && !defined(MIC_LOGGER) && !defined(NO_MIC_LOGGER) +#define MIC_LOGGER +#endif -//#define MAJORPEAK_SUPPRESS_NOISE // define to activate a dirty hack that ignores the lowest + hightest FFT bins -const i2s_port_t I2S_PORT = I2S_NUM_0; -const int BLOCK_SIZE = 128; -const int SAMPLE_RATE = 10240; // Base sample rate in Hz +// hackers corner +// #define SOUND_DYNAMICS_LIMITER // experimental: define to enable a dynamics limiter that avoids "sudden flashes" at onsets. Make some effects look more "smooth and fluent" -//Use userVar0 and userVar1 (API calls &U0=,&U1=, uint16_t) -#ifndef LED_BUILTIN // Set LED_BUILTIN if it is not defined by Arduino framework - #define LED_BUILTIN 3 -#endif +constexpr i2s_port_t I2S_PORT = I2S_NUM_0; +constexpr int BLOCK_SIZE = 128; +constexpr int SAMPLE_RATE = 10240; // Base sample rate in Hz + +//Use userVar0 and userVar1 (API calls &U0=,&U1=, uint16_t) #define UDP_SYNC_HEADER "00001" uint8_t maxVol = 10; // Reasonable value for constant volume for 'peak detector', as it won't always trigger -uint8_t binNum; // Used to select the bin for FFT based beat detection. -uint8_t targetAgc = 60; // This is our setPoint at 20% of max for the adjusted output +uint8_t binNum = 8; // Used to select the bin for FFT based beat detection. + + +// +// AGC presets +// Note: in C++, "const" implies "static" - no need to explicitly declare everything as "static const" +// +#define AGC_NUM_PRESETS 3 // AGC currently has 3 presets: normal, vivid, lazy + + // Normal, Vivid, Lazy +const double agcSampleDecay[AGC_NUM_PRESETS] = // decay factor for sampleMax, in case the current sample is below sampleMax + {0.9994, 0.9985, 0.9997}; + +const float agcZoneLow[AGC_NUM_PRESETS] = // low volume emergency zone + { 32, 28, 36}; +const float agcZoneHigh[AGC_NUM_PRESETS] = // high volume emergency zone + { 240, 240, 248}; +const float agcZoneStop[AGC_NUM_PRESETS] = // disable AGC integrator if we get above this level + { 336, 448, 304}; + +const float agcTarget0[AGC_NUM_PRESETS] = // first AGC setPoint -> between 40% and 65% + { 112, 144, 164}; +const float agcTarget0Up[AGC_NUM_PRESETS] = // setpoint switching value (a poor man's bang-bang) + { 88, 64, 116}; +const float agcTarget1[AGC_NUM_PRESETS] = // second AGC setPoint -> around 85% + { 220, 224, 216}; + +const double agcFollowFast[AGC_NUM_PRESETS] = // quickly follow setpoint - ~0.15 sec + { 1.0/192.0, 1.0/128.0, 1.0/256.0}; +const double agcFollowSlow[AGC_NUM_PRESETS] = // slowly follow setpoint - ~2-15 secs + {1.0/6144.0, 1.0/4096.0, 1.0/8192.0}; + +const double agcControlKp[AGC_NUM_PRESETS] = // AGC - PI control, proportional gain parameter + { 0.6, 1.5, 0.65}; +const double agcControlKi[AGC_NUM_PRESETS] = // AGC - PI control, integral gain parameter + { 1.7, 1.85, 1.2}; + +const float agcSampleSmooth[AGC_NUM_PRESETS] = // smoothing factor for sampleAgc (use rawSampleAgc if you want the non-smoothed value) + { 1.0/12.0, 1.0/6.0, 1.0/16.0}; +// +// AGC presets end +// + +double sampleMax = 0; // Max sample over a few seconds. Needed for AGC controler. + + uint8_t myVals[32]; // Used to store a pile of samples because WLED frame rate and WLED sample rate are not synchronized. Frame rate is too low. bool samplePeak = 0; // Boolean flag for peak. Responding routine must reset this flag bool udpSamplePeak = 0; // Boolean flag for peak. Set at the same tiem as samplePeak, but reset by transmitAudioData -int delayMs = 10; // I don't want to sample too often and overload WLED -int micIn; // Current sample starts with negative values and large values, which is why it's 16 bit signed -int sample; // Current sample. Must only be updated ONCE!!! -int tmpSample; // An interim sample variable used for calculatioins. -int sampleAdj; // Gain adjusted sample value -int sampleAgc; // Our AGC sample +constexpr int delayMs = 10; // I don't want to sample too often and overload WLED +static int micIn = 0.0; // Current sample starts with negative values and large values, which is why it's 16 bit signed +int sampleRaw; // Current sample. Must only be updated ONCE!!! +float sampleReal = 0.0; // "sample" as float, to provide bits that are lost otherwise. Needed for AGC. +static float tmpSample; // An interim sample variable used for calculations. +static float sampleAdj; // Gain adjusted sample value +int rawSampleAgc = 0; // Our AGC sample - raw +float sampleAgc = 0.0; // AGC sample, smoothed uint16_t micData; // Analog input for FFT uint16_t micDataSm; // Smoothed mic data, as it's a bit twitchy +float micDataReal = 0.0; // future support - this one has the full 24bit MicIn data - lowest 8bit after decimal point long timeOfPeak = 0; -long lastTime = 0; -float micLev = 0; // Used to convert returned value to have '0' as minimum. A leveller -float multAgc; // sample * multAgc = sampleAgc. Our multiplier +static unsigned long lastTime = 0; +static double micLev = 0.0; // Used to convert returned value to have '0' as minimum. A leveller +float multAgc = 1.0; // sample * multAgc = sampleAgc. Our multiplier float sampleAvg = 0; // Smoothed Average -double beat = 0; // beat Detection +//double beat = 0; // beat Detection -float expAdjF; // Used for exponential filter. +static float expAdjF; // Used for exponential filter. float weighting = 0.2; // Exponential filter weighting. Will be adjustable in a future release. // FFT Variables -const uint16_t samples = 512; // This value MUST ALWAYS be a power of 2 +constexpr uint16_t samplesFFT = 512; // Samples in an FFT batch - This value MUST ALWAYS be a power of 2 unsigned int sampling_period_us; unsigned long microseconds; @@ -89,9 +139,9 @@ double FFT_Magnitude = 0; uint16_t mAvg = 0; // These are the input and output vectors. Input vectors receive computed results from FFT. -double vReal[samples]; -double vImag[samples]; -double fftBin[samples]; +static double vReal[samplesFFT]; +static double vImag[samplesFFT]; +double fftBin[samplesFFT]; // Try and normalize fftBin values to a max of 4096, so that 4096/16 = 256. // Oh, and bins 0,1,2 are no good, so we'll zero them out. @@ -111,7 +161,7 @@ struct audioSyncPacket { char header[6] = UDP_SYNC_HEADER; uint8_t myVals[32]; // 32 Bytes int sampleAgc; // 04 Bytes - int sample; // 04 Bytes + int sampleRaw; // 04 Bytes float sampleAvg; // 04 Bytes bool samplePeak; // 01 Bytes uint8_t fftResult[16]; // 16 Bytes @@ -122,60 +172,59 @@ struct audioSyncPacket { double mapf(double x, double in_min, double in_max, double out_min, double out_max); bool isValidUdpSyncVersion(char header[6]) { - if (strncmp(header, UDP_SYNC_HEADER, 6) == 0) { + if (strncmp(header, UDP_SYNC_HEADER, 5) == 0) { return true; } else { return false; } } +/* get current max sample ("published" by the I2S and FFT thread) and perform some sound processing */ void getSample() { static long peakTime; - //extern double FFT_Magnitude; // Optional inclusion for our volume routines // COMMENTED OUT - UNUSED VARIABLE COMPILER WARNINGS - //extern double FFT_MajorPeak; // Same here. Not currently used though // COMMENTED OUT - UNUSED VARIABLE COMPILER WARNINGS + const int AGC_preset = (soundAgc > 0)? (soundAgc-1): 0; // make sure the _compiler_ knows this value will not change while we are inside the function #ifdef WLED_DISABLE_SOUND micIn = inoise8(millis(), millis()); // Simulated analog read + micDataReal = micIn; // Simulated I2S read #else - micIn = micDataSm; // micDataSm = ((micData * 3) + micData)/4; -/*---------DEBUG---------*/ - DEBUGSR_PRINT("micIn:\tmicData:\tmicIn>>2:\tmic_In_abs:\tsample:\tsampleAdj:\tsampleAvg:\n"); - DEBUGSR_PRINT(micIn); DEBUGSR_PRINT("\t"); DEBUGSR_PRINT(micData); -/*-------END DEBUG-------*/ -// We're still using 10 bit, but changing the analog read resolution in usermod.cpp -// if (digitalMic == false) micIn = micIn >> 2; // ESP32 has 2 more bits of A/D than ESP8266, so we need to normalize to 10 bit. -/*---------DEBUG---------*/ - DEBUGSR_PRINT("\t\t"); DEBUGSR_PRINT(micIn); -/*-------END DEBUG-------*/ + micIn = micDataSm; #endif - micLev = ((micLev * 31) + micIn) / 32; // Smooth it out over the last 32 samples for automatic centering - micIn -= micLev; // Let's center it to 0 now -/*---------DEBUG---------*/ - DEBUGSR_PRINT("\t\t"); DEBUGSR_PRINT(micIn); -/*-------END DEBUG-------*/ -// Using an exponential filter to smooth out the signal. We'll add controls for this in a future release. - expAdjF = (weighting * micIn + (1.0-weighting) * expAdjF); - expAdjF = (expAdjF <= soundSquelch) ? 0: expAdjF; + // remove remaining DC offset from sound signal + micLev = ((micLev * 8191.0) + micDataReal) / 8192.0; // takes a few seconds to "catch up" with the Mic Input + if(micIn < micLev) micLev = ((micLev * 31.0) + micDataReal) / 32.0; // align MicLev to lowest input signal + micIn -= micLev; // Let's center it to 0 now + // Using an exponential filter to smooth out the signal. We'll add controls for this in a future release. + float micInNoDC = fabs(micDataReal - micLev); + expAdjF = weighting * micInNoDC + ((1.0-weighting) * expAdjF); expAdjF = fabs(expAdjF); // Now (!) take the absolute value - tmpSample = (int)expAdjF; -/*---------DEBUG---------*/ - DEBUGSR_PRINT("\t\t"); DEBUGSR_PRINT(sample); -/*-------END DEBUG-------*/ + expAdjF = (expAdjF <= soundSquelch) ? 0: expAdjF; // simple noise gate + if ((soundSquelch == 0) && (expAdjF < 0.25f)) expAdjF = 0; + + tmpSample = expAdjF; micIn = abs(micIn); // And get the absolute value of each sample - sampleAdj = tmpSample * sampleGain / 40 + tmpSample / 16; // Adjust the gain. - sampleAdj = min(sampleAdj, 255); // Question: why are we limiting the value to 8 bits ??? - sample = sampleAdj; // ONLY update sample ONCE!!!! + sampleAdj = tmpSample * sampleGain / 40 * inputLevel/128 + tmpSample / 16; // Adjust the gain. with inputLevel adjustment + sampleReal = tmpSample; + + sampleAdj = fmax(fmin(sampleAdj, 255), 0); // Question: why are we limiting the value to 8 bits ??? + sampleRaw = (int)sampleAdj; // ONLY update sample ONCE!!!! - sampleAvg = ((sampleAvg * 15) + sample) / 16; // Smooth it out over the last 16 samples. + // keep "peak" sample, but decay value if current sample is below peak + if ((sampleMax < sampleReal) && (sampleReal > 0.5)) { + sampleMax = sampleMax + 0.5 * (sampleReal - sampleMax); // new peak - with some filtering + } else { + if ((multAgc*sampleMax > agcZoneStop[AGC_preset]) && (soundAgc > 0)) + sampleMax = sampleMax + 0.5 * (sampleReal - sampleMax); // over AGC Zone - get back quickly + else + sampleMax = sampleMax * agcSampleDecay[AGC_preset]; // signal to zero --> 5-8sec + } + if (sampleMax < 0.5) sampleMax = 0.0; -/*---------DEBUG---------*/ - DEBUGSR_PRINT("\t"); DEBUGSR_PRINT(sample); - DEBUGSR_PRINT("\t\t"); DEBUGSR_PRINT(sampleAvg); DEBUGSR_PRINT("\n\n"); -/*-------END DEBUG-------*/ + sampleAvg = ((sampleAvg * 15.0) + sampleAdj) / 16.0; // Smooth it out over the last 16 samples. // Fixes private class variable compiler error. Unsure if this is the correct way of fixing the root problem. -THATDONFC uint16_t MinShowDelay = strip.getMinShowDelay(); @@ -187,7 +236,6 @@ void getSample() { if (userVar1 == 0) samplePeak = 0; // Poor man's beat detection by seeing if sample > Average + some value. - // Serial.print(binNum); Serial.print("\t"); Serial.print(fftBin[binNum]); Serial.print("\t"); Serial.print(fftAvg[binNum/16]); Serial.print("\t"); Serial.print(maxVol); Serial.print("\t"); Serial.println(samplePeak); if ((fftBin[binNum] > maxVol) && (millis() > (peakTime + 100))) { // This goe through ALL of the 255 bins // if (sample > (sampleAvg + maxVol) && millis() > (peakTime + 200)) { // Then we got a peak, else we don't. The peak has to time out on its own in order to support UDP sound sync. @@ -200,35 +248,147 @@ void getSample() { } // getSample() /* - * A simple averaging multiplier to automatically adjust sound sensitivity. - */ -/* - * A simple, but hairy, averaging multiplier to automatically adjust sound sensitivity. - * not sure if not sure "sample" or "sampleAvg" is the correct input signal for AGC + * A "PI control" multiplier to automatically adjust sound sensitivity. + * + * A few tricks are implemented so that sampleAgc does't only utilize 0% and 100%: + * 0. don't amplify anything below squelch (but keep previous gain) + * 1. gain input = maximum signal observed in the last 5-10 seconds + * 2. we use two setpoints, one at ~60%, and one at ~80% of the maximum signal + * 3. the amplification depends on signal level: + * a) normal zone - very slow adjustment + * b) emergency zome (<10% or >90%) - very fast adjustment */ -void agcAvg() { +void agcAvg(unsigned long the_time) { + const int AGC_preset = (soundAgc > 0)? (soundAgc-1): 0; // make sure the _compiler_ knows this value will not change while we are inside the function + static int last_soundAgc = -1; + + float lastMultAgc = multAgc; // last muliplier used + float multAgcTemp = multAgc; // new multiplier + float tmpAgc = sampleReal * multAgc; // what-if amplified signal + + float control_error; // "control error" input for PI control + static double control_integrated = 0.0; // "integrator control" = accumulated error + + if (last_soundAgc != soundAgc) + control_integrated = 0.0; // new preset - reset integrator + + // For PI control, we need to have a contant "frequency" + // so let's make sure that the control loop is not running at insane speed + static unsigned long last_time = 0; + unsigned long time_now = millis(); + if ((the_time > 0) && (the_time < time_now)) time_now = the_time; // allow caller to override my clock + + if (time_now - last_time > 2) { + last_time = time_now; + + if((fabs(sampleReal) < 2.0) || (sampleMax < 1.0)) { + // MIC signal is "squelched" - deliver silence + multAgcTemp = multAgc; // keep old control value (no change) + tmpAgc = 0; + // we need to "spin down" the intgrated error buffer + if (fabs(control_integrated) < 0.01) control_integrated = 0.0; + else control_integrated = control_integrated * 0.91; + } else { + // compute new setpoint + if (tmpAgc <= agcTarget0Up[AGC_preset]) + multAgcTemp = agcTarget0[AGC_preset] / sampleMax; // Make the multiplier so that sampleMax * multiplier = first setpoint + else + multAgcTemp = agcTarget1[AGC_preset] / sampleMax; // Make the multiplier so that sampleMax * multiplier = second setpoint + } + // limit amplification + if (multAgcTemp > 32.0) multAgcTemp = 32.0; + if (multAgcTemp < 1.0/64.0) multAgcTemp = 1.0/64.0; + + // compute error terms + control_error = multAgcTemp - lastMultAgc; + + if (((multAgcTemp > 0.085) && (multAgcTemp < 6.5)) //integrator anti-windup by clamping + && (multAgc*sampleMax < agcZoneStop[AGC_preset])) //integrator ceiling (>140% of max) + control_integrated += control_error * 0.002 * 0.25; // 2ms = intgration time; 0.25 for damping + else + control_integrated *= 0.9; // spin down that beasty integrator + + // apply PI Control + tmpAgc = sampleReal * lastMultAgc; // check "zone" of the signal using previous gain + if ((tmpAgc > agcZoneHigh[AGC_preset]) || (tmpAgc < soundSquelch + agcZoneLow[AGC_preset])) { // upper/lower emergy zone + multAgcTemp = lastMultAgc + agcFollowFast[AGC_preset] * agcControlKp[AGC_preset] * control_error; + multAgcTemp += agcFollowFast[AGC_preset] * agcControlKi[AGC_preset] * control_integrated; + } else { // "normal zone" + multAgcTemp = lastMultAgc + agcFollowSlow[AGC_preset] * agcControlKp[AGC_preset] * control_error; + multAgcTemp += agcFollowSlow[AGC_preset] * agcControlKi[AGC_preset] * control_integrated; + } - float lastMultAgc = multAgc; - float tmpAgc; - if(fabs(sampleAvg) < 2.0) { - tmpAgc = sampleAvg; // signal below squelch -> deliver silence - multAgc = multAgc * 0.95; // slightly decrease gain multiplier - } else { - multAgc = (sampleAvg < 1) ? targetAgc : targetAgc / sampleAvg; // Make the multiplier so that sampleAvg * multiplier = setpoint + // limit amplification again - PI controler sometimes "overshoots" + if (multAgcTemp > 32.0) multAgcTemp = 32.0; + if (multAgcTemp < 1.0/64.0) multAgcTemp = 1.0/64.0; } - if (multAgc < 0.5) multAgc = 0.5; // signal higher than 2*setpoint -> don't reduce it further - multAgc = (lastMultAgc*127.0 +multAgc) / 128.0; //apply some filtering to gain multiplier -> smoother transitions - tmpAgc = (float)sample * multAgc; // apply gain to signal - if (tmpAgc <= (soundSquelch*1.2)) tmpAgc = sample; // check against squelch threshold - increased by 20% to avoid artefacts (ripples) + // NOW finally amplify the signal + tmpAgc = sampleReal * multAgcTemp; // apply gain to signal + if(fabs(sampleReal) < 2.0) tmpAgc = 0; // apply squelch threshold + if (tmpAgc > 255) tmpAgc = 255; // limit to 8bit + if (tmpAgc < 1) tmpAgc = 0; // just to be sure + + // update global vars ONCE - multAgc, sampleAGC, rawSampleAgc + multAgc = multAgcTemp; + rawSampleAgc = 0.8 * tmpAgc + 0.2 * (float)rawSampleAgc; + + + // update smoothed AGC sample + if(fabs(tmpAgc) < 1.0) + sampleAgc = 0.5 * tmpAgc + 0.5 * sampleAgc; // fast path to zero + else + sampleAgc = sampleAgc + agcSampleSmooth[AGC_preset] * (tmpAgc - sampleAgc); // smooth path - if (tmpAgc > 255) tmpAgc = 255; - sampleAgc = tmpAgc; // ONLY update sampleAgc ONCE because it's used elsewhere asynchronously!!!! userVar0 = sampleAvg * 4; if (userVar0 > 255) userVar0 = 255; + + last_soundAgc = soundAgc; } // agcAvg() +/* limit sound dynamics by contraining "attack" and "decay" times */ +constexpr float bigChange = 196; // just a representative number - a large, expected sample value +/* values below will be made user-configurable later */ +constexpr float attackTime = 800; // attack time -> 0.8sec +constexpr float decayTime = 2800; // decay time -> 2.8sec + +/* This fuctions limits the dynamics of sampleAvg and sampleAgc. It does not affect FFTResult[] or raw samples (sample, rawSampleAgc) */ +// effects: Gravimeter, Gravcenter, Gravcentric, Noisefire, Plasmoid, Freqpixels, Freqwave, Gravfreq, (2D Swirl, 2D Waverly) +// experimental, as it still has side-effects on AGC - AGC detects "silence" to late (due to long decay time) and ditches up the gain multiplier. +void limitSampleDynamics(void) { +#ifdef SOUND_DYNAMICS_LIMITER + static unsigned long last_time = 0; + static float last_sampleAvg = 0.0f; + static float last_sampleAgc = 0.0f; + + long delta_time = millis() - last_time; + delta_time = constrain(delta_time , 1, 1000); // below 1ms -> 1ms; above 1sec -> sily lil hick-up + float maxAttack = bigChange * float(delta_time) / attackTime; + float maxDecay = - bigChange * float(delta_time) / decayTime; + float deltaSample; + + // non-AGC sample + if ((attackTime > 0) && (decayTime > 0)) { + deltaSample = sampleAvg - last_sampleAvg; + if (deltaSample > maxAttack) deltaSample = maxAttack; + if (deltaSample < maxDecay) deltaSample = maxDecay; + sampleAvg = last_sampleAvg + deltaSample; + } + // same for AGC sample + if ((attackTime > 0) && (decayTime > 0)) { + deltaSample = sampleAgc - last_sampleAgc; + if (deltaSample > maxAttack) deltaSample = maxAttack; + if (deltaSample < maxDecay) deltaSample = maxDecay; + sampleAgc = last_sampleAgc + deltaSample; + } + + last_sampleAvg = sampleAvg; + last_sampleAgc = sampleAgc; + last_time = millis(); +#endif +} + //////////////////// // Begin FFT Code // @@ -238,23 +398,15 @@ void agcAvg() { void transmitAudioData() { if (!udpSyncConnected) return; - extern uint8_t myVals[]; - extern int sampleAgc; - extern int sample; - extern float sampleAvg; - extern bool udpSamplePeak; - extern int fftResult[]; - extern double FFT_Magnitude; - extern double FFT_MajorPeak; - - audioSyncPacket transmitData; + static audioSyncPacket transmitData; // softhack007: added "static" + strncpy(transmitData.header, UDP_SYNC_HEADER, 6); // softhack007: I don't trust in type initialization for (int i = 0; i < 32; i++) { transmitData.myVals[i] = myVals[i]; } transmitData.sampleAgc = sampleAgc; - transmitData.sample = sample; + transmitData.sampleRaw = sampleRaw; transmitData.sampleAvg = sampleAvg; transmitData.samplePeak = udpSamplePeak; udpSamplePeak = 0; // Reset udpSamplePeak after we've transmitted it @@ -276,7 +428,7 @@ void transmitAudioData() { // Create FFT object -arduinoFFT FFT = arduinoFFT( vReal, vImag, samples, SAMPLE_RATE ); +static arduinoFFT FFT = arduinoFFT( vReal, vImag, samplesFFT, SAMPLE_RATE ); double fftAdd( int from, int to) { int i = from; @@ -290,36 +442,43 @@ double fftAdd( int from, int to) { // FFT main code void FFTcode( void * parameter) { DEBUG_PRINT("FFT running on core: "); DEBUG_PRINTLN(xPortGetCoreID()); -#ifdef MAJORPEAK_SUPPRESS_NOISE - static double xtemp[24] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0}; -#endif for(;;) { delay(1); // DO NOT DELETE THIS LINE! It is needed to give the IDLE(0) task enough time and to keep the watchdog happy. // taskYIELD(), yield(), vTaskDelay() and esp_task_wdt_feed() didn't seem to work. - // Only run the FFT computing code if we're not in Receive mode - if (audioSyncEnabled & (1 << 1)) + // Only run the FFT computing code if we're not in "realime mode" or in Receive mode + if (disableSoundProcessing || (audioSyncEnabled & (1 << 1))) { + delay(7); // release CPU - delay is implemeted using vTaskDelay() continue; - audioSource->getSamples(vReal, samples); + } + audioSource->getSamples(vReal, samplesFFT); // old code - Last sample in vReal is our current mic sample //micDataSm = (uint16_t)vReal[samples - 1]; // will do a this a bit later // micDataSm = ((micData * 3) + micData)/4; - double maxSample = 0.0; - for (int i=0; i < samples; i++) + const int halfSamplesFFT = samplesFFT / 2; // samplesFFT divided by 2 + double maxSample1 = 0.0; // max sample from first half of FFT batch + double maxSample2 = 0.0; // max sample from second half of FFT batch + for (int i=0; i < samplesFFT; i++) { // set imaginary parts to 0 vImag[i] = 0; // pick our our current mic sample - we take the max value from all samples that go into FFT if ((vReal[i] <= (INT16_MAX - 1024)) && (vReal[i] >= (INT16_MIN + 1024))) //skip extreme values - normally these are artefacts { - if (fabs(vReal[i]) > maxSample) maxSample = fabs(vReal[i]); + if (i <= halfSamplesFFT) { + if (fabs(vReal[i]) > maxSample1) maxSample1 = fabs(vReal[i]); + } else { + if (fabs(vReal[i]) > maxSample2) maxSample2 = fabs(vReal[i]); + } } } - micDataSm = (uint16_t)maxSample; + // release first sample to volume reactive effects + micDataSm = (uint16_t)maxSample1; + micDataReal = maxSample1; FFT.DCRemoval(); // let FFT lib remove DC component, so we don't need to care about this in getSamples() @@ -334,68 +493,10 @@ void FFTcode( void * parameter) { // vReal[3 .. 255] contain useful data, each a 20Hz interval (60Hz - 5120Hz). // There could be interesting data at bins 0 to 2, but there are too many artifacts. // -#ifdef MAJORPEAK_SUPPRESS_NOISE - // teporarily reduce signal strength in the highest + lowest bins - xtemp[0] = vReal[0]; vReal[0] *= 0.005; - xtemp[1] = vReal[1]; vReal[1] *= 0.005; - xtemp[2] = vReal[2]; vReal[2] *= 0.005; - xtemp[3] = vReal[3]; vReal[3] *= 0.02; - xtemp[4] = vReal[4]; vReal[4] *= 0.02; - xtemp[5] = vReal[5]; vReal[5] *= 0.02; - xtemp[6] = vReal[6]; vReal[6] *= 0.05; - xtemp[7] = vReal[7]; vReal[7] *= 0.08; - xtemp[8] = vReal[8]; vReal[8] *= 0.1; - xtemp[9] = vReal[9]; vReal[9] *= 0.2; - xtemp[10] = vReal[10]; vReal[10] *= 0.2; - xtemp[11] = vReal[11]; vReal[11] *= 0.25; - xtemp[12] = vReal[12]; vReal[12] *= 0.3; - xtemp[13] = vReal[13]; vReal[13] *= 0.3; - xtemp[14] = vReal[14]; vReal[14] *= 0.4; - xtemp[15] = vReal[15]; vReal[15] *= 0.4; - xtemp[16] = vReal[16]; vReal[16] *= 0.4; - xtemp[17] = vReal[17]; vReal[17] *= 0.5; - xtemp[18] = vReal[18]; vReal[18] *= 0.5; - xtemp[19] = vReal[19]; vReal[19] *= 0.6; - xtemp[20] = vReal[20]; vReal[20] *= 0.7; - xtemp[21] = vReal[21]; vReal[21] *= 0.8; - - xtemp[22] = vReal[samples-2]; vReal[samples-2] =0.0; - xtemp[23] = vReal[samples-1]; vReal[samples-1] =0.0; -#endif FFT.MajorPeak(&FFT_MajorPeak, &FFT_Magnitude); // let the effects know which freq was most dominant -#ifdef MAJORPEAK_SUPPRESS_NOISE - // dirty hack: limit suppressed channel intensities to FFT_Magnitude - for (int k=0; k < 24; k++) if(xtemp[k] > FFT_Magnitude) xtemp[k] = FFT_Magnitude; - // restore bins - vReal[0] = xtemp[0]; - vReal[1] = xtemp[1]; - vReal[2] = xtemp[2]; - vReal[3] = xtemp[3]; - vReal[4] = xtemp[4]; - vReal[5] = xtemp[5]; - vReal[6] = xtemp[6]; - vReal[7] = xtemp[7]; - vReal[8] = xtemp[8]; - vReal[9] = xtemp[9]; - vReal[10] = xtemp[10]; - vReal[11] = xtemp[11]; - vReal[12] = xtemp[12]; - vReal[13] = xtemp[13]; - vReal[14] = xtemp[14]; - vReal[15] = xtemp[15]; - vReal[16] = xtemp[16]; - vReal[17] = xtemp[17]; - vReal[18] = xtemp[18]; - vReal[19] = xtemp[19]; - vReal[20] = xtemp[20]; - vReal[21] = xtemp[21]; - vReal[samples-2] = xtemp[22]; - vReal[samples-1] = xtemp[23]; -#endif - - for (int i = 0; i < samples; i++) { // Values for bins 0 and 1 are WAY too large. Might as well start at 3. + for (int i = 0; i < samplesFFT; i++) { // Values for bins 0 and 1 are WAY too large. Might as well start at 3. double t = 0.0; t = fabs(vReal[i]); // just to be sure - values in fft bins should be positive any way t = t / 16.0; // Reduce magnitude. Want end result to be linear and ~4096 max. @@ -406,7 +507,7 @@ void FFTcode( void * parameter) { /* This FFT post processing is a DIY endeavour. What we really need is someone with sound engineering expertise to do a great job here AND most importantly, that the animations look GREAT as a result. * * - * Andrew's updated mapping of 256 bins down to the 16 result bins with Sample Freq = 10240, samples = 512 and some overlap. + * Andrew's updated mapping of 256 bins down to the 16 result bins with Sample Freq = 10240, samplesFFT = 512 and some overlap. * Based on testing, the lowest/Start frequency is 60 Hz (with bin 3) and a highest/End frequency of 5120 Hz in bin 255. * Now, Take the 60Hz and multiply by 1.320367784 to get the next frequency and so on until the end. Then detetermine the bins. * End frequency = Start frequency * multiplier ^ 16 @@ -423,7 +524,7 @@ void FFTcode( void * parameter) { fftCalc[5] = (fftAdd(12,16)) /5; // 240 - 340 fftCalc[6] = (fftAdd(16,21)) /6; // 320 - 440 fftCalc[7] = (fftAdd(21,28)) /8; // 420 - 600 - fftCalc[8] = (fftAdd(29,37)) /10; // 580 - 760 + fftCalc[8] = (fftAdd(28,37)) /10; // 580 - 760 fftCalc[9] = (fftAdd(37,48)) /12; // 740 - 980 fftCalc[10] = (fftAdd(48,64)) /17; // 960 - 1300 fftCalc[11] = (fftAdd(64,84)) /21; // 1280 - 1700 @@ -445,7 +546,10 @@ void FFTcode( void * parameter) { // Manual linear adjustment of gain using sampleGain adjustment for different input types. for (int i=0; i < 16; i++) { - fftCalc[i] = fftCalc[i] * sampleGain / 40 + fftCalc[i]/16.0; + if (soundAgc) + fftCalc[i] = fftCalc[i] * multAgc; + else + fftCalc[i] = fftCalc[i] * (double)sampleGain / 40.0 * inputLevel/128 + (double)fftCalc[i]/16.0; //with inputLevel adjustment } @@ -456,6 +560,10 @@ void FFTcode( void * parameter) { fftAvg[i] = (float)fftResult[i]*.05 + (1-.05)*fftAvg[i]; } +// release second sample to volume reactive effects. + // The FFT process currently takes ~20ms, so releasing a second sample now effectively doubles the "sample rate" + micDataSm = (uint16_t)maxSample2; + micDataReal = maxSample2; // Looking for fftResultMax for each bin using Pink Noise // for (int i=0; i<16; i++) { @@ -470,36 +578,24 @@ void FFTcode( void * parameter) { void logAudio() { #ifdef MIC_LOGGER + // Debugging functions for audio input and sound processing. Comment out the values you want to see - - //Serial.print("micData:"); Serial.print(micData); Serial.print("\t"); - //Serial.print("micDataSm:"); Serial.print(micDataSm); Serial.print("\t"); - //Serial.print("micIn:"); Serial.print(micIn); Serial.print("\t"); + Serial.print("micReal:"); Serial.print(micDataReal); Serial.print("\t"); + //Serial.print("micData:"); Serial.print(micData); Serial.print("\t"); + //Serial.print("micDataSm:"); Serial.print(micDataSm); Serial.print("\t"); + //Serial.print("micIn:"); Serial.print(micIn); Serial.print("\t"); //Serial.print("micLev:"); Serial.print(micLev); Serial.print("\t"); - Serial.print("sample:"); Serial.print(sample); Serial.print("\t"); + //Serial.print("sampleReal:"); Serial.print(sampleReal); Serial.print("\t"); + //Serial.print("sample:"); Serial.print(sample); Serial.print("\t"); //Serial.print("sampleAvg:"); Serial.print(sampleAvg); Serial.print("\t"); - //Serial.print("multAgc:"); Serial.print(multAgc); Serial.print("\t"); - Serial.print("sampleAgc:"); Serial.print(sampleAgc); Serial.print("\t"); + //Serial.print("sampleMax:"); Serial.print(sampleMax); Serial.print("\t"); + //Serial.print("samplePeak:"); Serial.print((samplePeak!=0) ? 128:0); Serial.print("\t"); + //Serial.print("multAgc:"); Serial.print(multAgc, 4); Serial.print("\t"); + Serial.print("sampleAgc:"); Serial.print(sampleAgc); Serial.print("\t"); Serial.println(" "); #endif -#ifdef MIC_SAMPLING_LOG - //------------ Oscilloscope output --------------------------- - Serial.print(targetAgc); Serial.print(" "); - Serial.print(multAgc); Serial.print(" "); - Serial.print(sampleAgc); Serial.print(" "); - - Serial.print(sample); Serial.print(" "); - Serial.print(sampleAvg); Serial.print(" "); - Serial.print(micLev); Serial.print(" "); - Serial.print(samplePeak); Serial.print(" "); //samplePeak = 0; - Serial.print(micIn); Serial.print(" "); - Serial.print(100); Serial.print(" "); - Serial.print(0); Serial.print(" "); - Serial.println(" "); -#endif - #ifdef FFT_SAMPLING_LOG #if 0 for(int i=0; i<16; i++) { diff --git a/wled00/audio_source.h b/wled00/audio_source.h index 47b84665d4..35c506995a 100644 --- a/wled00/audio_source.h +++ b/wled00/audio_source.h @@ -3,23 +3,54 @@ #include #include "wled.h" #include +#include +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 4, 0) +#include +#include +#endif + +//#include +//#include +//#include /* ToDo: remove. ES7243 is controlled via compiler defines Until this configuration is moved to the webinterface */ + // data type requested from the I2S driver - currently we always use 32bit -//#define I2S_USE_16BIT_SAMPLES // (experimental) define this to request 16bit - more efficient but possibly less compatible +//#define I2S_USE_16BIT_SAMPLES // (experimental) define this to request 16bit - more efficient but possibly less compatible + +// if you have problems to get your microphone work on the left channel, uncomment the following line +//#define I2S_USE_RIGHT_CHANNEL // (experimental) define this to use right channel (digital mics only) + +// Uncomment the line below to utilize ADC1 _exclusively_ for I2S sound input. +// benefit: analog mic inputs will be sampled contiously -> better response times and less "glitches" +// WARNING: this option WILL lock-up your device in case that any other analogRead() operation is performed; +// for example if you want to read "analog buttons" +//#define I2S_GRAB_ADC1_COMPLETELY // (experimental) continously sample analog ADC microphone. WARNING will cause analogRead() lock-up + + #ifdef I2S_USE_16BIT_SAMPLES #define I2S_SAMPLE_RESOLUTION I2S_BITS_PER_SAMPLE_16BIT #define I2S_datatype int16_t +#define I2S_unsigned_datatype uint16_t #undef I2S_SAMPLE_DOWNSCALE_TO_16BIT #else #define I2S_SAMPLE_RESOLUTION I2S_BITS_PER_SAMPLE_32BIT #define I2S_datatype int32_t +#define I2S_unsigned_datatype uint32_t #define I2S_SAMPLE_DOWNSCALE_TO_16BIT #endif +#ifdef I2S_USE_RIGHT_CHANNEL +#define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_RIGHT +#define I2S_MIC_CHANNEL_TEXT "right channel only." +#else +#define I2S_MIC_CHANNEL I2S_CHANNEL_FMT_ONLY_LEFT +#define I2S_MIC_CHANNEL_TEXT "left channel only." +#endif + #ifndef MCLK_PIN int mclkPin = 0; #else @@ -78,17 +109,25 @@ class AudioSource { /* Get an up-to-date sample without DC offset */ virtual int getSampleWithoutDCOffset() = 0; + /* check if the audio source driver was initialized successfully */ + virtual bool isInitialized(void) {return(_initialized);} + protected: // Private constructor, to make sure it is not callable except from derived classes - AudioSource(int sampleRate, int blockSize, int16_t lshift, uint32_t mask) : _sampleRate(sampleRate), _blockSize(blockSize), _sampleNoDCOffset(0), _dcOffset(0.0f), _shift(lshift), _mask(mask), _initialized(false) {}; + AudioSource(int sampleRate, int blockSize, int16_t lshift, uint32_t mask) : _sampleRate(sampleRate), _blockSize(blockSize), _sampleNoDCOffset(0), _dcOffset(0.0f), _shift(lshift), _mask(mask), + _initialized(false), _myADCchannel(0x0F), _lastADCsample(0), _broken_samples_counter(0) {}; - int _sampleRate; /* Microphone sampling rate */ + int _sampleRate; /* Microphone sampling rate (from uint16_t to int to suppress warning)*/ int _blockSize; /* I2S block size */ volatile int _sampleNoDCOffset; /* Up-to-date sample without DCOffset */ float _dcOffset; /* Rolling average DC offset */ int16_t _shift; /* Shift obtained samples to the right (positive) or left(negative) by this amount */ uint32_t _mask; /* Bitmask for sample data after shifting. Bitmask 0X0FFF means that we need to convert 12bit ADC samples from unsigned to signed*/ bool _initialized; /* Gets set to true if initialization is successful */ + int8_t _myADCchannel; /* current ADC channel, in case of analog input. 0x0F if undefined */ + I2S_datatype _lastADCsample; /* last sample from ADC */ + I2S_datatype decodeADCsample(I2S_unsigned_datatype rawData); /* function to handle ADC samples */ + unsigned int _broken_samples_counter; /* counts number of broken (and fixed) ADC samples */ }; /* Basic I2S microphone source @@ -100,10 +139,14 @@ class I2SSource : public AudioSource { AudioSource(sampleRate, blockSize, lshift, mask) { _config = { .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX), - .sample_rate = _sampleRate, + .sample_rate = _sampleRate, // "narrowing conversion" warning can be ignored here - our _sampleRate is never bigger that INT32_MAX .bits_per_sample = I2S_SAMPLE_RESOLUTION, - .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, + .channel_format = I2S_MIC_CHANNEL, +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) + .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_STAND_I2S), +#else .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB), +#endif .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = 8, .dma_buf_len = _blockSize @@ -123,7 +166,7 @@ class I2SSource : public AudioSource { virtual void initialize() { if (!pinManager.allocatePin(i2swsPin, true, PinOwner::DigitalMic) || - !pinManager.allocatePin(i2ssdPin, true, PinOwner::DigitalMic)) { + !pinManager.allocatePin(i2ssdPin, false, PinOwner::DigitalMic)) { return; } @@ -149,11 +192,13 @@ class I2SSource : public AudioSource { } virtual void deinitialize() { - _initialized = false; - esp_err_t err = i2s_driver_uninstall(I2S_NUM_0); - if (err != ESP_OK) { - Serial.printf("Failed to uninstall i2s driver: %d\n", err); - return; + if (_initialized) { + _initialized = false; + esp_err_t err = i2s_driver_uninstall(I2S_NUM_0); + if (err != ESP_OK) { + Serial.printf("Failed to uninstall i2s driver: %d\n", err); + return; + } } pinManager.deallocatePin(i2swsPin, PinOwner::DigitalMic); pinManager.deallocatePin(i2ssdPin, PinOwner::DigitalMic); @@ -167,46 +212,60 @@ class I2SSource : public AudioSource { if(_initialized) { esp_err_t err; size_t bytes_read = 0; /* Counter variable to check if we actually got enough data */ - I2S_datatype samples[num_samples]; /* Intermediary sample storage */ + I2S_datatype newSamples[num_samples]; /* Intermediary sample storage */ + + _dcOffset = 0.0f; // Reset dc offset + _broken_samples_counter = 0; // Reset ADC broken samples counter - // Reset dc offset - _dcOffset = 0.0f; - - err = i2s_read(I2S_NUM_0, (void *)samples, sizeof(samples), &bytes_read, portMAX_DELAY); + // get fresh samples + err = i2s_read(I2S_NUM_0, (void *)newSamples, sizeof(newSamples), &bytes_read, portMAX_DELAY); if ((err != ESP_OK)){ Serial.printf("Failed to get samples: %d\n", err); return; } // For correct operation, we need to read exactly sizeof(samples) bytes from i2s - if(bytes_read != sizeof(samples)) { - Serial.printf("Failed to get enough samples: wanted: %d read: %d\n", sizeof(samples), bytes_read); + if(bytes_read != sizeof(newSamples)) { + Serial.printf("Failed to get enough samples: wanted: %d read: %d\n", sizeof(newSamples), bytes_read); return; } // Store samples in sample buffer and update DC offset for (int i = 0; i < num_samples; i++) { + + if (_mask == 0x0FFF) { // mask = 0x0FFF means we are in I2SAdcSource + I2S_unsigned_datatype rawData = * reinterpret_cast (newSamples + i); // C++ acrobatics to get sample as "unsigned" + I2S_datatype sampleNoFilter = decodeADCsample(rawData); + if (_broken_samples_counter >= num_samples-1) { // kill-switch: ADC sample correction off when all samples in a batch were "broken" + _myADCchannel = 0x0F; + Serial.println("AS: too many broken audio samples from ADC - sample correction switched off."); + } + + newSamples[i] = (3 * sampleNoFilter + _lastADCsample) / 4; // apply low-pass filter (2-tap FIR) + //newSamples[i] = (sampleNoFilter + lastADCsample) / 2; // apply stronger low-pass filter (2-tap FIR) + _lastADCsample = sampleNoFilter; // update ADC last sample + } + // pre-shift samples down to 16bit #ifdef I2S_SAMPLE_DOWNSCALE_TO_16BIT - samples[i] >>= 16; + if (_shift != 0) + newSamples[i] >>= 16; #endif - // pre-processing of ADC samples - if (_mask == 0x0FFF) { // 0x0FFF means that we have 12bit unsigned data from ADC -> convert to signed - samples[i] = samples[i] - 2048; - } - // From the old code. - // double sample = (double)abs((samples[i] >> _shift)); - double sample = 0.0; + double currSample = 0.0; if(_shift > 0) - sample = (double) (samples[i] >> _shift); + currSample = (double) (newSamples[i] >> _shift); else { if(_shift < 0) - sample = (double) (samples[i] << (- _shift)); // need to "pump up" 12bit ADC to full 16bit as delivered by other digital mics + currSample = (double) (newSamples[i] << (- _shift)); // need to "pump up" 12bit ADC to full 16bit as delivered by other digital mics else - sample = (double) samples[i]; +#ifdef I2S_SAMPLE_DOWNSCALE_TO_16BIT + currSample = (double) newSamples[i] / 65536.0; // _shift == 0 -> use the chance to keep lower 16bits +#else + currSample = (double) newSamples[i]; +#endif } - buffer[i] = sample; - _dcOffset = ((_dcOffset * 31) + sample) / 32; + buffer[i] = currSample; + _dcOffset = ((_dcOffset * 31) + currSample) / 32; } // Update no-DC sample @@ -214,6 +273,33 @@ class I2SSource : public AudioSource { } } + // function to handle ADC samples + I2S_datatype decodeADCsample(I2S_unsigned_datatype rawData) { +#ifndef I2S_USE_16BIT_SAMPLES + rawData = (rawData >> 16) & 0xFFFF; // scale input down from 32bit -> 16bit + I2S_datatype lastGoodSample = _lastADCsample / 16384 ; // 26bit-> 12bit with correct sign handling +#else + rawData = rawData & 0xFFFF; // input is already in 16bit, just mask off possible junk + I2S_datatype lastGoodSample = _lastADCsample * 4; // 10bit-> 12bit +#endif + // decode ADC sample + uint16_t the_channel = (rawData >> 12) & 0x000F; // upper 4 bit = ADC channel + uint16_t the_sample = rawData & 0x0FFF; // lower 12bit -> ADC sample (unsigned) + I2S_datatype finalSample = (int(the_sample) - 2048); // convert to signed (centered at 0); + + // fix bad samples + if ((the_channel != _myADCchannel) && (_myADCchannel != 0x0F)) { // 0x0F means "don't know what my channel is" + finalSample = lastGoodSample; // replace with the last good ADC sample + _broken_samples_counter ++; + //Serial.print("\n!ADC rogue sample 0x"); Serial.print(rawData, HEX); Serial.print("\tchannel:");Serial.println(the_channel); + } +#ifndef I2S_USE_16BIT_SAMPLES + finalSample = finalSample << 16; // scale up from 16bit -> 32bit; +#endif + finalSample = finalSample / 4; // mimic old analog driver behaviour (12bit -> 10bit) + return(finalSample); + } + virtual int getSampleWithoutDCOffset() { return _sampleNoDCOffset; } @@ -332,29 +418,53 @@ class I2SAdcSource : public I2SSource { I2SSource(sampleRate, blockSize, lshift, mask){ _config = { .mode = i2s_mode_t(I2S_MODE_MASTER | I2S_MODE_RX | I2S_MODE_ADC_BUILT_IN), - .sample_rate = _sampleRate, + .sample_rate = _sampleRate, // "narrowing conversion" warning can be ignored here - our _sampleRate is never bigger that INT32_MAX .bits_per_sample = I2S_SAMPLE_RESOLUTION, .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) + .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_STAND_I2S), +#else .communication_format = i2s_comm_format_t(I2S_COMM_FORMAT_I2S | I2S_COMM_FORMAT_I2S_MSB), +#endif .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = 8, - .dma_buf_len = _blockSize + .dma_buf_len = _blockSize, + .use_apll = false, // must be disabled for analog microphone + .tx_desc_auto_clear = false, + .fixed_mclk = 0 }; } void initialize() { + // check if "analog buttons" are configured for ADC1, and issue warning + for (int b=0; b= 0) && (buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED) && (digitalPinToAnalogChannel(btnPin[b]) < 10)) { + if ((btnPin[b] >= 0) && (buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED) && (digitalPinToAnalogChannel(btnPin[b]) >= 0)) { + Serial.println("AS: Analog Microphone does not work reliably when analog buttons are configured on ADC1."); + Serial.printf( " Button %d GPIO %d\n", b, btnPin[b]); + Serial.println(" To recover, please disable any analog button in LED preferences, then restart your device."); + Serial.flush(); +#ifdef I2S_GRAB_ADC1_COMPLETELY + Serial.println("AS: Analog Microphone initialization aborted. Cannot use ADC1 exclusively"); + return; +#endif + } + } + if(!pinManager.allocatePin(audioPin, false, PinOwner::AnalogMic)) { return; } // Determine Analog channel. Only Channels on ADC1 are supported int8_t channel = digitalPinToAnalogChannel(audioPin); - if (channel > 9) { + if ((channel < 0) || (channel > 9)) { // channel == -1 means "not an ADC pin" Serial.printf("Incompatible GPIO used for audio in: %d\n", audioPin); return; } else { adc_gpio_init(ADC_UNIT_1, adc_channel_t(channel)); } + _myADCchannel = channel; + _lastADCsample = 0; // Install Driver esp_err_t err = i2s_driver_install(I2S_NUM_0, &_config, 0, nullptr); @@ -370,7 +480,7 @@ class I2SAdcSource : public I2SSource { return; } -#if defined(ARDUINO_ARCH_ESP32) +#if defined(I2S_GRAB_ADC1_COMPLETELY) // according to docs from espressif, the ADC needs to be started explicitly // fingers crossed err = i2s_adc_enable(I2S_NUM_0); @@ -378,21 +488,25 @@ class I2SAdcSource : public I2SSource { Serial.printf("Failed to enable i2s adc: %d\n", err); //return; } +#else + err = i2s_adc_disable(I2S_NUM_0); + //err = i2s_stop(I2S_NUM_0); + if (err != ESP_OK) { + Serial.printf("Failed to initially disable i2s adc: %d\n", err); + } #endif - _initialized = true; } void getSamples(double *buffer, uint16_t num_samples) { /* Enable ADC. This has to be enabled and disabled directly before and - after sampling, otherwise Wifi dies + after sampling, otherwise Wifi dies and analogRead() hangs */ if (_initialized) { -#if !defined(ARDUINO_ARCH_ESP32) - // old code - works for me without enable/disable, at least on ESP32. - esp_err_t err = i2s_adc_enable(I2S_NUM_0); +#if !defined(I2S_GRAB_ADC1_COMPLETELY) //esp_err_t err = i2s_start(I2S_NUM_0); + esp_err_t err = i2s_adc_enable(I2S_NUM_0); if (err != ESP_OK) { Serial.printf("Failed to enable i2s adc: %d\n", err); return; @@ -400,8 +514,7 @@ class I2SAdcSource : public I2SSource { #endif I2SSource::getSamples(buffer, num_samples); -#if !defined(ARDUINO_ARCH_ESP32) - // old code - works for me without enable/disable, at least on ESP32. +#if !defined(I2S_GRAB_ADC1_COMPLETELY) err = i2s_adc_disable(I2S_NUM_0); //err = i2s_stop(I2S_NUM_0); if (err != ESP_OK) { @@ -414,22 +527,28 @@ class I2SAdcSource : public I2SSource { void deinitialize() { pinManager.deallocatePin(audioPin, PinOwner::AnalogMic); - _initialized = false; esp_err_t err; -#if defined(ARDUINO_ARCH_ESP32) - // according to docs from espressif, the ADC needs to be stopped explicitly - // fingers crossed - err = i2s_adc_disable(I2S_NUM_0); - if (err != ESP_OK) { - Serial.printf("Failed to disable i2s adc: %d\n", err); - //return; - } + if (_initialized) { + _initialized = false; +#if defined(I2S_GRAB_ADC1_COMPLETELY) + // according to docs from espressif, the ADC needs to be stopped explicitly + // fingers crossed + err = i2s_adc_disable(I2S_NUM_0); + if (err != ESP_OK) { + Serial.printf("Failed to disable i2s adc: %d\n", err); + //return; + } #endif - err = i2s_driver_uninstall(I2S_NUM_0); - if (err != ESP_OK) { - Serial.printf("Failed to uninstall i2s driver: %d\n", err); - return; + i2s_adc_disable(I2S_NUM_0); // just to be sure + i2s_stop(I2S_NUM_0); + err = i2s_driver_uninstall(I2S_NUM_0); + if (err != ESP_OK) { + Serial.printf("Failed to uninstall i2s driver: %d\n", err); + return; + } } + _myADCchannel = 0x0F; + _lastADCsample = 0; } }; diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 7e065f8fa2..ef9355839f 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -587,7 +587,7 @@ class BusManager { //utility to get the approx. memory usage of a given BusConfig static uint32_t memUsage(BusConfig &bc) { uint8_t type = bc.type; - uint16_t len = bc.count; + uint16_t len = bc.count + bc.skipAmount; if (type > 15 && type < 32) { #ifdef ESP8266 if (bc.pins[0] == 3) { //8266 DMA uses 5x the mem diff --git a/wled00/button.cpp b/wled00/button.cpp index 31d3022c89..4fcebf2b46 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -131,21 +131,42 @@ void handleSwitch(uint8_t b) } } +#define ANALOG_BTN_READ_CYCLE 250 // min time between two analog reading cycles +#define STRIP_WAIT_TIME 6 // max wait time in case of strip.isUpdating() +#define POT_SMOOTHING 0.25f // smoothing factor for raw potentiometer readings +#define POT_SENSITIVITY 4 // changes below this amount are noise (POT scratching, or ADC noise) + void handleAnalog(uint8_t b) { - static uint8_t oldRead[WLED_MAX_BUTTONS]; + static uint8_t oldRead[WLED_MAX_BUTTONS] = {0}; + static float filteredReading[WLED_MAX_BUTTONS] = {0.0f}; + uint16_t rawReading; // raw value from analogRead, scaled to 12bit + #ifdef ESP8266 - uint16_t aRead = analogRead(A0) >> 2; // convert 10bit read to 8bit + rawReading = analogRead(A0) << 2; // convert 10bit read to 12bit #else - uint16_t aRead = analogRead(btnPin[b]) >> 4; // convert 12bit read to 8bit + rawReading = analogRead(btnPin[b]); // collect at full 12bit resolution #endif + yield(); // keep WiFi task running - analog read may take several millis on ESP8266 + + filteredReading[b] += POT_SMOOTHING * ((float(rawReading) / 16.0f) - filteredReading[b]); // filter raw input, and scale to [0..255] + uint16_t aRead = max(min(int(filteredReading[b]), 255), 0); // squash into 8bit + if(aRead <= POT_SENSITIVITY) aRead = 0; // make sure that 0 and 255 are used + if(aRead >= 255-POT_SENSITIVITY) aRead = 255; if (buttonType[b] == BTN_TYPE_ANALOG_INVERTED) aRead = 255 - aRead; // remove noise & reduce frequency of UI updates - aRead &= 0xFC; + if (abs(int(aRead) - int(oldRead[b])) <= POT_SENSITIVITY) return; // no significant change in reading + + // Unomment the next lines if you still see flickering related to potentiometer + // This waits until strip finishes updating (why: strip was not updating at the start of handleButton() but may have started during analogRead()?) + //unsigned long wait_started = millis(); + //while(strip.isUpdating() && (millis() - wait_started < STRIP_WAIT_TIME)) { + // delay(1); + //} + //if (strip.isUpdating()) return; // give up - if (oldRead[b] == aRead) return; // no change in reading oldRead[b] = aRead; // if no macro for "short press" and "long press" is defined use brightness control @@ -168,6 +189,7 @@ void handleAnalog(uint8_t b) } else if (macroDoublePress[b] == 247) { // selected palette effectPalette = map(aRead, 0, 252, 0, strip.getPaletteCount()-1); + effectPalette = constrain(effectPalette, 0, strip.getPaletteCount()-1); // map is allowed to "overshoot", so we need to contrain the result } else if (macroDoublePress[b] == 200) { // primary color, hue, full saturation colorHStoRGB(aRead*256,255,col); @@ -196,6 +218,8 @@ void handleButton() static unsigned long lastRead = 0UL; bool analog = false; + if (strip.isUpdating()) return; // don't interfere with strip updates. Our button will still be there in 1ms (next cycle) + for (uint8_t b=0; b 250) { // button is not a button but a potentiometer + if ((buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED) && millis() - lastRead > ANALOG_BTN_READ_CYCLE) { // button is not a button but a potentiometer analog = true; handleAnalog(b); continue; } diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index a54152dcb5..e20a67b1aa 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -116,6 +116,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { uint8_t s = 0; // bus iterator if (fromFS) busses.removeAll(); // can't safely manipulate busses directly in network callback uint32_t mem = 0; + bool busesChanged = false; for (JsonObject elm : ins) { if (s >= WLED_MAX_BUSSES) break; uint8_t pins[5] = {255, 255, 255, 255, 255}; @@ -137,7 +138,6 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { bool reversed = elm["rev"]; bool refresh = elm["ref"] | false; ledType |= refresh << 7; // hack bit 7 to indicate strip requires off refresh - s++; if (fromFS) { BusConfig bc = BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst); mem += BusManager::memUsage(bc); @@ -145,9 +145,11 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { } else { if (busConfigs[s] != nullptr) delete busConfigs[s]; busConfigs[s] = new BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst); - doInitBusses = true; + busesChanged = true; } + s++; } + doInitBusses = busesChanged; // finalization done in beginStrip() } if (hw_led["rev"]) busses.getBus(0)->reversed = true; //set 0.11 global reversed setting for first bus @@ -352,6 +354,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { JsonObject if_live = interfaces["live"]; CJSON(receiveDirect, if_live["en"]); + CJSON(useMainSegmentOnly, if_live[F("mso")]); CJSON(e131Port, if_live["port"]); // 5568 if (e131Port == DDP_DEFAULT_PORT) e131Port = E131_DEFAULT_PORT; // prevent double DDP port allocation CJSON(e131Multicast, if_live[F("mc")]); @@ -534,7 +537,9 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { } if (fromFS) return needsSave; + // if from /json/cfg doReboot = doc[F("rb")] | doReboot; + if (doInitBusses) return false; // no save needed, will do after bus init in wled.cpp loop return (doc["sv"] | true); } @@ -810,6 +815,7 @@ void serializeConfig() { JsonObject if_live = interfaces.createNestedObject("live"); if_live["en"] = receiveDirect; + if_live[F("mso")] = useMainSegmentOnly; if_live["port"] = e131Port; if_live[F("mc")] = e131Multicast; diff --git a/wled00/const.h b/wled00/const.h index 0b907e7111..d93bc20e31 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -72,6 +72,10 @@ #define USERMOD_ID_QUINLED_AN_PENTA 23 //Usermod "quinled-an-penta.h" #define USERMOD_ID_SSDR 24 //Usermod "usermod_v2_seven_segment_display_reloaded.h" #define USERMOD_ID_CRONIXIE 25 //Usermod "usermod_cronixie.h" +#define USERMOD_ID_WIZLIGHTS 26 //Usermod "wizlights.h" +#define USERMOD_ID_WORDCLOCK 27 //Usermod "usermod_v2_word_clock.h" +#define USERMOD_ID_MY9291 28 //Usermod "usermod_MY9291.h" +#define USERMOD_ID_SI7021_MQTT_HA 29 //Usermod "usermod_si7021_mqtt_ha.h" //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot @@ -192,7 +196,7 @@ #define BTN_TYPE_ANALOG_INVERTED 8 //Ethernet board types -#define WLED_NUM_ETH_TYPES 7 +#define WLED_NUM_ETH_TYPES 8 #define WLED_ETH_NONE 0 #define WLED_ETH_WT32_ETH01 1 @@ -301,7 +305,13 @@ #endif #endif -#define ABL_MILLIAMPS_DEFAULT 850 // auto lower brightness to stay close to milliampere limit +#ifndef ABL_MILLIAMPS_DEFAULT + #define ABL_MILLIAMPS_DEFAULT 850 // auto lower brightness to stay close to milliampere limit +#else + #if ABL_MILLIAMPS_DEFAULT < 250 // make sure value is at least 250 + #define ABL_MILLIAMPS_DEFAULT 250 + #endif +#endif // PWM settings #ifndef WLED_PWM_FREQ diff --git a/wled00/data/icons-ui/Read Me.txt b/wled00/data/icons-ui/Read Me.txt new file mode 100644 index 0000000000..8491652f88 --- /dev/null +++ b/wled00/data/icons-ui/Read Me.txt @@ -0,0 +1,7 @@ +Open *demo.html* to see a list of all the glyphs in your font along with their codes/ligatures. + +To use the generated font in desktop programs, you can install the TTF font. In order to copy the character associated with each icon, refer to the text box at the bottom right corner of each glyph in demo.html. The character inside this text box may be invisible; but it can still be copied. See this guide for more info: https://icomoon.io/#docs/local-fonts + +You won't need any of the files located under the *demo-files* directory when including the generated font in your own projects. + +You can import *selection.json* back to the IcoMoon app using the *Import Icons* button (or via Main Menu → Manage Projects) to retrieve your icon selection. diff --git a/wled00/data/icons-ui/demo-files/demo.css b/wled00/data/icons-ui/demo-files/demo.css new file mode 100644 index 0000000000..39b8991da7 --- /dev/null +++ b/wled00/data/icons-ui/demo-files/demo.css @@ -0,0 +1,152 @@ +body { + padding: 0; + margin: 0; + font-family: sans-serif; + font-size: 1em; + line-height: 1.5; + color: #555; + background: #fff; +} +h1 { + font-size: 1.5em; + font-weight: normal; +} +small { + font-size: .66666667em; +} +a { + color: #e74c3c; + text-decoration: none; +} +a:hover, a:focus { + box-shadow: 0 1px #e74c3c; +} +.bshadow0, input { + box-shadow: inset 0 -2px #e7e7e7; +} +input:hover { + box-shadow: inset 0 -2px #ccc; +} +input, fieldset { + font-family: sans-serif; + font-size: 1em; + margin: 0; + padding: 0; + border: 0; +} +input { + color: inherit; + line-height: 1.5; + height: 1.5em; + padding: .25em 0; +} +input:focus { + outline: none; + box-shadow: inset 0 -2px #449fdb; +} +.glyph { + font-size: 16px; + width: 15em; + padding-bottom: 1em; + margin-right: 4em; + margin-bottom: 1em; + float: left; + overflow: hidden; +} +.liga { + width: 80%; + width: calc(100% - 2.5em); +} +.talign-right { + text-align: right; +} +.talign-center { + text-align: center; +} +.bgc1 { + background: #f1f1f1; +} +.fgc1 { + color: #999; +} +.fgc0 { + color: #000; +} +p { + margin-top: 1em; + margin-bottom: 1em; +} +.mvm { + margin-top: .75em; + margin-bottom: .75em; +} +.mtn { + margin-top: 0; +} +.mtl, .mal { + margin-top: 1.5em; +} +.mbl, .mal { + margin-bottom: 1.5em; +} +.mal, .mhl { + margin-left: 1.5em; + margin-right: 1.5em; +} +.mhmm { + margin-left: 1em; + margin-right: 1em; +} +.mls { + margin-left: .25em; +} +.ptl { + padding-top: 1.5em; +} +.pbs, .pvs { + padding-bottom: .25em; +} +.pvs, .pts { + padding-top: .25em; +} +.unit { + float: left; +} +.unitRight { + float: right; +} +.size1of2 { + width: 50%; +} +.size1of1 { + width: 100%; +} +.clearfix:before, .clearfix:after { + content: " "; + display: table; +} +.clearfix:after { + clear: both; +} +.hidden-true { + display: none; +} +.textbox0 { + width: 3em; + background: #f1f1f1; + padding: .25em .5em; + line-height: 1.5; + height: 1.5em; +} +#testDrive { + display: block; + padding-top: 24px; + line-height: 1.5; +} +.fs0 { + font-size: 16px; +} +.fs1 { + font-size: 32px; +} + diff --git a/wled00/data/icons-ui/demo-files/demo.js b/wled00/data/icons-ui/demo-files/demo.js new file mode 100644 index 0000000000..6f45f1c409 --- /dev/null +++ b/wled00/data/icons-ui/demo-files/demo.js @@ -0,0 +1,30 @@ +if (!('boxShadow' in document.body.style)) { + document.body.setAttribute('class', 'noBoxShadow'); +} + +document.body.addEventListener("click", function(e) { + var target = e.target; + if (target.tagName === "INPUT" && + target.getAttribute('class').indexOf('liga') === -1) { + target.select(); + } +}); + +(function() { + var fontSize = document.getElementById('fontSize'), + testDrive = document.getElementById('testDrive'), + testText = document.getElementById('testText'); + function updateTest() { + testDrive.innerHTML = testText.value || String.fromCharCode(160); + if (window.icomoonLiga) { + window.icomoonLiga(testDrive); + } + } + function updateSize() { + testDrive.style.fontSize = fontSize.value + 'px'; + } + fontSize.addEventListener('change', updateSize, false); + testText.addEventListener('input', updateTest, false); + testText.addEventListener('change', updateTest, false); + updateSize(); +}()); diff --git a/wled00/data/icons-ui/demo.html b/wled00/data/icons-ui/demo.html new file mode 100644 index 0000000000..0416231fb0 --- /dev/null +++ b/wled00/data/icons-ui/demo.html @@ -0,0 +1,360 @@ + + + + + IcoMoon Demo + + + + + +
+

Font Name: wled122 (Glyphs: 23)

+
+
+

Grid Size: Unknown

+
+
+ + i-pattern +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-segments +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-sun +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-palette +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-eye +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-speed +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-expand +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-power +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-settings +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-playlist +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-night +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-cancel +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-sync +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-confirm +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-brightness +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-nodes +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-add +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-edit +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-intensity +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-star +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-info +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-del +
+
+ + +
+
+ liga: + +
+
+
+
+ + i-presets +
+
+ + +
+
+ liga: + +
+
+
+ + +
+

Font Test Drive

+ + +
  +
+
+ +
+

Generated by IcoMoon

+
+ + + + diff --git a/wled00/data/icons-ui/fonts/wled122.woff b/wled00/data/icons-ui/fonts/wled122.woff new file mode 100644 index 0000000000..cc389b3a5a Binary files /dev/null and b/wled00/data/icons-ui/fonts/wled122.woff differ diff --git a/wled00/data/icons-ui/fonts/wled122.woff2 b/wled00/data/icons-ui/fonts/wled122.woff2 new file mode 100644 index 0000000000..d13b37ac0c Binary files /dev/null and b/wled00/data/icons-ui/fonts/wled122.woff2 differ diff --git a/wled00/data/icons-ui/selection.json b/wled00/data/icons-ui/selection.json new file mode 100644 index 0000000000..5af8516b8b --- /dev/null +++ b/wled00/data/icons-ui/selection.json @@ -0,0 +1 @@ +{"IcoMoonType":"selection","icons":[{"icon":{"paths":["M511.573 85.333c235.947 0 427.094 191.147 427.094 426.667s-191.147 426.667-427.094 426.667c-235.52 0-426.24-191.147-426.24-426.667s190.72-426.667 426.24-426.667zM512 853.333c188.587 0 341.333-152.746 341.333-341.333s-152.746-341.333-341.333-341.333-341.333 152.746-341.333 341.333 152.746 341.333 341.333 341.333zM661.333 469.333c-35.413 0-64-28.586-64-64 0-35.413 28.587-64 64-64 35.414 0 64 28.587 64 64 0 35.414-28.586 64-64 64zM362.667 469.333c-35.414 0-64-28.586-64-64 0-35.413 28.586-64 64-64 35.413 0 64 28.587 64 64 0 35.414-28.587 64-64 64zM512 746.667c-99.413 0-183.893-62.294-218.027-149.334h436.054c-34.134 87.040-118.614 149.334-218.027 149.334z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE23D"],"defaultCode":57917,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":11,"order":26,"prevSize":32,"code":57917,"name":"pattern"},"setIdx":0,"setId":0,"iconIdx":10},{"icon":{"paths":["M511.573 791.040l314.88-244.907 69.547 54.187-384 298.667-384-298.667 69.12-53.76zM512 682.667l-384-298.667 384-298.667 384 298.667-69.973 54.187z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE34B"],"defaultCode":58187,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":14,"order":35,"ligatures":"","prevSize":32,"code":58187,"name":"segments"},"setIdx":0,"setId":0,"iconIdx":13},{"icon":{"paths":["M288.427 206.507l-60.587 60.16-76.373-76.374 60.16-60.16zM170.667 448v85.333h-128v-85.333h128zM554.667 23.467v125.866h-85.334v-125.866h85.334zM872.533 190.293l-76.373 76.374-60.16-60.16 76.373-76.374zM735.573 774.827l59.734-59.734 76.8 76.374-60.16 60.16zM853.333 448h128v85.333h-128v-85.333zM512 234.667c141.227 0 256 114.773 256 256 0 141.226-114.773 256-256 256s-256-114.774-256-256c0-141.227 114.773-256 256-256zM469.333 957.867v-125.867h85.334v125.867h-85.334zM151.467 791.040l76.373-76.8 60.16 60.16-76.373 76.8z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE333"],"defaultCode":58163,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":40,"order":73,"ligatures":"","prevSize":32,"code":58163,"name":"sun"},"setIdx":0,"setId":0,"iconIdx":39},{"icon":{"paths":["M512 128c212.053 0 384 152.747 384 341.333 0 117.76-95.573 213.334-213.333 213.334h-75.52c-35.414 0-64 28.586-64 64 0 16.213 6.4 31.146 16.213 42.24 10.24 11.52 16.64 26.453 16.64 43.093 0 35.413-28.587 64-64 64-212.053 0-384-171.947-384-384s171.947-384 384-384zM277.333 512c35.414 0 64-28.587 64-64s-28.586-64-64-64c-35.413 0-64 28.587-64 64s28.587 64 64 64zM405.333 341.333c35.414 0 64-28.586 64-64 0-35.413-28.586-64-64-64-35.413 0-64 28.587-64 64 0 35.414 28.587 64 64 64zM618.667 341.333c35.413 0 64-28.586 64-64 0-35.413-28.587-64-64-64-35.414 0-64 28.587-64 64 0 35.414 28.586 64 64 64zM746.667 512c35.413 0 64-28.587 64-64s-28.587-64-64-64c-35.414 0-64 28.587-64 64s28.586 64 64 64z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE2B3"],"defaultCode":58035,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":75,"order":22,"prevSize":32,"code":58035,"name":"palette"},"setIdx":0,"setId":0,"iconIdx":74},{"icon":{"paths":["M512 192c213.333 0 395.52 132.693 469.333 320-73.813 187.307-256 320-469.333 320s-395.52-132.693-469.333-320c73.813-187.307 256-320 469.333-320zM512 725.333c117.76 0 213.333-95.573 213.333-213.333s-95.573-213.333-213.333-213.333-213.333 95.573-213.333 213.333 95.573 213.333 213.333 213.333zM512 384c70.827 0 128 57.173 128 128s-57.173 128-128 128-128-57.173-128-128 57.173-128 128-128z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE0E8"],"defaultCode":57576,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":172,"order":74,"prevSize":32,"code":57576,"name":"eye"},"setIdx":0,"setId":0,"iconIdx":171},{"icon":{"paths":["M640 42.667v85.333h-256v-85.333h256zM469.333 597.333v-256h85.334v256h-85.334zM811.947 315.307c52.48 65.706 84.053 148.906 84.053 239.36 0 212.053-171.52 384-384 384s-384-171.947-384-384c0-212.054 171.947-384 384-384 90.453 0 173.653 31.573 239.787 84.48l60.586-60.587c21.76 17.92 41.814 38.4 60.16 60.16zM512 853.333c165.12 0 298.667-133.546 298.667-298.666s-133.547-298.667-298.667-298.667-298.667 133.547-298.667 298.667 133.547 298.666 298.667 298.666z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE325"],"defaultCode":58149,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":370,"order":21,"ligatures":"","prevSize":32,"code":58149,"name":"speed"},"setIdx":0,"setId":0,"iconIdx":369},{"icon":{"paths":["M707.84 366.507l60.16 60.16-256 256-256-256 60.16-60.16 195.84 195.413z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE395"],"defaultCode":58261,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":549,"order":69,"ligatures":"","prevSize":32,"code":58261,"name":"expand"},"setIdx":0,"setId":0,"iconIdx":548},{"icon":{"paths":["M554.667 128v426.667h-85.334v-426.667h85.334zM760.747 220.587c82.773 70.4 135.253 174.506 135.253 291.413 0 212.053-171.947 384-384 384s-384-171.947-384-384c0-116.907 52.48-221.013 135.253-291.413l60.16 60.16c-66.986 54.613-110.080 137.813-110.080 231.253 0 165.12 133.547 298.667 298.667 298.667s298.667-133.547 298.667-298.667c0-93.44-43.094-176.64-110.507-230.827z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE08F"],"defaultCode":57487,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":557,"order":19,"ligatures":"","prevSize":32,"code":57487,"name":"power"},"setIdx":0,"setId":0,"iconIdx":556},{"icon":{"paths":["M816.64 551.936l85.504 67.584c8.192 6.144 10.24 16.896 5.12 26.112l-81.92 141.824c-5.12 9.216-15.872 12.8-25.088 9.216l-101.888-40.96c-20.992 15.872-44.032 29.696-69.12 39.936l-15.36 108.544c-1.024 10.24-9.728 17.408-19.968 17.408h-163.84c-10.24 0-18.432-7.168-20.48-17.408l-15.36-108.544c-25.088-10.24-47.616-23.552-69.12-39.936l-101.888 40.96c-9.216 3.072-19.968 0-25.088-9.216l-81.92-141.824c-4.608-8.704-2.56-19.968 5.12-26.112l86.528-67.584c-2.048-12.8-3.072-26.624-3.072-39.936s1.536-27.136 3.584-39.936l-86.528-67.584c-8.192-6.144-10.24-16.896-5.12-26.112l81.92-141.824c5.12-9.216 15.872-12.8 25.088-9.216l101.888 40.96c20.992-15.872 44.032-29.696 69.12-39.936l15.36-108.544c1.536-10.24 9.728-17.408 19.968-17.408h163.84c10.24 0 18.944 7.168 20.48 17.408l15.36 108.544c25.088 10.24 47.616 23.552 69.12 39.936l101.888-40.96c9.216-3.072 19.968 0 25.088 9.216l81.92 141.824c4.608 8.704 2.56 19.968-5.12 26.112l-86.528 67.584c2.048 12.8 3.072 26.112 3.072 39.936s-1.024 27.136-2.56 39.936zM512 665.6c84.48 0 153.6-69.12 153.6-153.6s-69.12-153.6-153.6-153.6-153.6 69.12-153.6 153.6 69.12 153.6 153.6 153.6z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE0A2"],"defaultCode":57506,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":562,"order":29,"ligatures":"","prevSize":32,"code":57506,"name":"settings"},"setIdx":0,"setId":0,"iconIdx":561},{"icon":{"paths":["M556.8 417.707l125.867 94.293-256 192v-384zM556.8 417.707l125.867 94.293-256 192v-384zM556.8 417.707l-130.133-97.707v384l256-192zM469.333 173.653c-62.293 7.68-119.040 32.427-166.4 69.12l-60.586-61.013c63.146-51.627 141.226-85.76 226.986-94.293v86.186zM242.773 302.933c-36.693 47.36-61.44 104.107-69.12 166.4h-86.186c8.533-85.76 42.666-163.84 94.293-226.986zM173.653 554.667c7.68 62.293 32.427 119.040 69.12 165.973l-61.013 61.013c-51.627-63.146-85.76-141.226-94.293-226.986h86.186zM242.347 842.24l60.586-61.013c47.36 36.693 104.107 61.44 166.4 69.12v86.186c-85.333-8.533-163.84-42.666-226.986-94.293zM938.667 512c0 220.16-167.254 401.92-381.867 424.533v-86.186c167.253-22.187 296.533-165.547 296.533-338.347s-129.28-316.16-296.533-338.347v-86.186c214.613 22.613 381.867 204.373 381.867 424.533z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE139"],"defaultCode":57657,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":595,"order":46,"ligatures":"","prevSize":32,"code":57657,"name":"playlist"},"setIdx":0,"setId":0,"iconIdx":594},{"icon":{"paths":["M384 85.333c235.52 0 426.667 191.147 426.667 426.667s-191.147 426.667-426.667 426.667c-44.8 0-87.467-6.827-128-19.627 173.227-54.187 298.667-215.893 298.667-407.040s-125.44-352.853-298.667-407.040c40.533-12.8 83.2-19.627 128-19.627z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE2A2"],"defaultCode":58018,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":607,"order":34,"ligatures":"","prevSize":32,"code":58018,"name":"night"},"setIdx":0,"setId":0,"iconIdx":606},{"icon":{"paths":["M512 85.333c235.947 0 426.667 190.72 426.667 426.667s-190.72 426.667-426.667 426.667-426.667-190.72-426.667-426.667 190.72-426.667 426.667-426.667zM725.333 665.173l-153.173-153.173 153.173-153.173-60.16-60.16-153.173 153.173-153.173-153.173-60.16 60.16 153.173 153.173-153.173 153.173 60.16 60.16 153.173-153.173 153.173 153.173z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE38F"],"defaultCode":58255,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":662,"order":50,"ligatures":"","prevSize":32,"code":58255,"name":"cancel"},"setIdx":0,"setId":0,"iconIdx":661},{"icon":{"paths":["M512 170.667c188.587 0 341.333 152.746 341.333 341.333 0 66.987-19.626 129.28-52.906 181.76l-62.294-62.293c19.2-35.414 29.867-76.374 29.867-119.467 0-141.227-114.773-256-256-256v128l-170.667-170.667 170.667-170.666v128zM512 768v-128l170.667 170.667-170.667 170.666v-128c-188.587 0-341.333-152.746-341.333-341.333 0-66.987 19.626-129.28 52.906-181.76l62.294 62.293c-19.2 35.414-29.867 76.374-29.867 119.467 0 141.227 114.773 256 256 256z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE116"],"defaultCode":57622,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":709,"order":17,"ligatures":"","prevSize":32,"code":57622,"name":"sync"},"setIdx":0,"setId":0,"iconIdx":708},{"icon":{"paths":["M384 689.92l451.84-451.413 60.16 60.16-512 512-238.507-238.507 60.587-60.16z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE390"],"defaultCode":58256,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":733,"order":56,"ligatures":"","prevSize":32,"code":58256,"name":"confirm"},"setIdx":0,"setId":0,"iconIdx":732},{"icon":{"paths":["M853.333 370.773l141.227 141.227-141.227 141.227v200.106h-200.106l-141.227 141.227-141.227-141.227h-200.106v-200.106l-141.227-141.227 141.227-141.227v-200.106h200.106l141.227-141.227 141.227 141.227h200.106v200.106zM512 768c141.227 0 256-114.773 256-256s-114.773-256-256-256-256 114.773-256 256 114.773 256 256 256zM512 341.333c94.293 0 170.667 76.374 170.667 170.667s-76.374 170.667-170.667 170.667-170.667-76.374-170.667-170.667 76.374-170.667 170.667-170.667z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE2A6"],"defaultCode":58022,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":785,"order":15,"ligatures":"","prevSize":32,"code":58022,"name":"brightness"},"setIdx":0,"setId":0,"iconIdx":784},{"icon":{"paths":["M85.333 725.333v-42.666h128v170.666h-128v-42.666h85.334v-21.334h-42.667v-42.666h42.667v-21.334h-85.334zM128 341.333v-128h-42.667v-42.666h85.334v170.666h-42.667zM85.333 469.333v-42.666h128v38.4l-76.8 89.6h76.8v42.666h-128v-38.4l76.8-89.6h-76.8zM298.667 213.333h597.333v85.334h-597.333v-85.334zM298.667 810.667v-85.334h597.333v85.334h-597.333zM298.667 554.667v-85.334h597.333v85.334h-597.333z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE22D"],"defaultCode":57901,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":797,"order":58,"ligatures":"","prevSize":32,"code":57901,"name":"nodes"},"setIdx":0,"setId":0,"iconIdx":796},{"icon":{"paths":["M810.667 554.667h-256v256h-85.334v-256h-256v-85.334h256v-256h85.334v256h256v85.334z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE18A"],"defaultCode":57738,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":803,"order":59,"ligatures":"","prevSize":32,"code":57738,"name":"add"},"setIdx":0,"setId":0,"iconIdx":802},{"icon":{"paths":["M128 736l471.893-471.893 160 160-471.893 471.893h-160v-160zM883.627 300.373l-78.080 78.080-160-160 78.080-78.080c16.64-16.64 43.52-16.64 60.16 0l99.84 99.84c16.64 16.64 16.64 43.52 0 60.16z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE2C6"],"defaultCode":58054,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":834,"order":72,"ligatures":"","prevSize":32,"code":58054,"name":"edit"},"setIdx":0,"setId":0,"iconIdx":833},{"icon":{"paths":["M576 28.587c166.827 133.546 277.333 338.773 277.333 568.746 0 188.587-152.746 341.334-341.333 341.334s-341.333-152.747-341.333-341.334c0-144.213 51.626-276.906 137.813-379.306l-1.28 15.36c0 87.893 66.56 159.146 154.88 159.146 87.893 0 145.493-71.253 145.493-159.146 0-91.734-31.573-204.8-31.573-204.8zM499.627 810.667c113.066 0 204.8-91.734 204.8-204.8 0-59.307-8.534-117.334-25.174-172.374-43.52 58.454-121.6 94.72-197.12 110.080-75.093 15.36-119.893 64-119.893 133.12 0 74.24 61.44 133.974 137.387 133.974z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE409"],"defaultCode":58377,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":871,"order":10,"ligatures":"","prevSize":32,"code":58377,"name":"intensity"},"setIdx":0,"setId":0,"iconIdx":870},{"icon":{"paths":["M938.667 394.24l-232.534 201.813 69.547 299.947-263.68-159.147-263.68 159.147 69.973-299.947-232.96-201.813 306.774-26.027 119.893-282.88 119.893 282.454zM512 657.067l160.853 97.28-42.666-182.614 141.653-122.88-186.88-16.213-72.96-172.373-72.533 171.946-186.88 16.214 141.653 122.88-42.667 182.613z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE410"],"defaultCode":58384,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":927,"order":9,"ligatures":"","prevSize":32,"code":58384,"name":"star"},"setIdx":0,"setId":0,"iconIdx":926},{"icon":{"paths":["M512 85.333c235.52 0 426.667 191.147 426.667 426.667s-191.147 426.667-426.667 426.667-426.667-191.147-426.667-426.667 191.147-426.667 426.667-426.667zM554.667 725.333v-256h-85.334v256h85.334zM554.667 384v-85.333h-85.334v85.333h85.334z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE066"],"defaultCode":57446,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":952,"order":62,"ligatures":"","prevSize":32,"code":57446,"name":"info"},"setIdx":0,"setId":0,"iconIdx":951},{"icon":{"paths":["M256 810.667v-512h512v512c0 46.933-38.4 85.333-85.333 85.333h-341.334c-46.933 0-85.333-38.4-85.333-85.333zM810.667 170.667v85.333h-597.334v-85.333h149.334l42.666-42.667h213.334l42.666 42.667h149.334z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE037"],"defaultCode":57399,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":969,"order":55,"ligatures":"","prevSize":32,"code":57399,"name":"del"},"setIdx":0,"setId":0,"iconIdx":968},{"icon":{"paths":["M704 128c131.413 0 234.667 103.253 234.667 234.667 0 161.28-145.067 292.693-364.8 491.946l-61.867 56.32-61.867-55.893c-219.733-199.68-364.8-331.093-364.8-492.373 0-131.414 103.254-234.667 234.667-234.667 74.24 0 145.493 34.56 192 89.173 46.507-54.613 117.76-89.173 192-89.173zM516.267 791.467c203.093-183.894 337.066-305.494 337.066-428.8 0-85.334-64-149.334-149.333-149.334-65.707 0-129.707 42.24-151.893 100.694h-79.787c-22.613-58.454-86.613-100.694-152.32-100.694-85.333 0-149.333 64-149.333 149.334 0 123.306 133.973 244.906 337.066 428.8l4.267 4.266z"],"isMulticolor":false,"isMulticolor2":false,"tags":["uniE04C"],"defaultCode":57420,"grid":0,"attrs":[]},"attrs":[],"properties":{"id":1034,"order":61,"ligatures":"","prevSize":32,"code":57420,"name":"presets"},"setIdx":0,"setId":0,"iconIdx":1033}],"height":1024,"metadata":{"name":"wled122"},"preferences":{"showGlyphs":true,"showCodes":true,"showQuickUse":true,"showQuickUse2":true,"showSVGs":true,"fontPref":{"prefix":"i-","metadata":{"fontFamily":"wled122","majorVersion":1,"minorVersion":7},"metrics":{"emSize":1024,"baseline":20,"whitespace":0},"embed":false,"autoHost":true,"noie8":true,"ie7":false,"showSelector":false,"showMetrics":false,"showMetadata":false,"showVersion":true},"imagePref":{"prefix":"icon-","png":true,"useClassSelector":true,"color":0,"bgColor":16777215},"historySize":50,"quickUsageToken":{"MainUI":false},"showLiga":false}} \ No newline at end of file diff --git a/wled00/data/icons-ui/style.css b/wled00/data/icons-ui/style.css new file mode 100644 index 0000000000..59982557d0 --- /dev/null +++ b/wled00/data/icons-ui/style.css @@ -0,0 +1,96 @@ +@font-face { + font-family: 'wled122'; + src: + url('fonts/wled122.woff2?e3eban') format('woff2'), + url('fonts/wled122.ttf?e3eban') format('truetype'), + url('fonts/wled122.woff?e3eban') format('woff'), + url('fonts/wled122.svg?e3eban#wled122') format('svg'); + font-weight: normal; + font-style: normal; + font-display: block; +} + +[class^="i-"], [class*=" i-"] { + /* use !important to prevent issues with browser extensions that change fonts */ + font-family: 'wled122' !important; + speak: never; + font-style: normal; + font-weight: normal; + font-variant: normal; + text-transform: none; + line-height: 1; + + /* Better Font Rendering =========== */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.i-pattern:before { + content: "\e23d"; +} +.i-segments:before { + content: "\e34b"; +} +.i-sun:before { + content: "\e333"; +} +.i-palette:before { + content: "\e2b3"; +} +.i-eye:before { + content: "\e0e8"; +} +.i-speed:before { + content: "\e325"; +} +.i-expand:before { + content: "\e395"; +} +.i-power:before { + content: "\e08f"; +} +.i-settings:before { + content: "\e0a2"; +} +.i-playlist:before { + content: "\e139"; +} +.i-night:before { + content: "\e2a2"; +} +.i-cancel:before { + content: "\e38f"; +} +.i-sync:before { + content: "\e116"; +} +.i-confirm:before { + content: "\e390"; +} +.i-brightness:before { + content: "\e2a6"; +} +.i-nodes:before { + content: "\e22d"; +} +.i-add:before { + content: "\e18a"; +} +.i-edit:before { + content: "\e2c6"; +} +.i-intensity:before { + content: "\e409"; +} +.i-star:before { + content: "\e410"; +} +.i-info:before { + content: "\e066"; +} +.i-del:before { + content: "\e037"; +} +.i-presets:before { + content: "\e04c"; +} diff --git a/wled00/data/index.css b/wled00/data/index.css index 0421f4426f..59c679d6f0 100644 --- a/wled00/data/index.css +++ b/wled00/data/index.css @@ -206,6 +206,15 @@ button { border: 0px; } +/* WLEDSR */ +#liveview2D { + height: 90%; + display: none; + width: 90%; + border: 0px; + margin: 0 auto; +} + .tab { background-color: transparent; color: var(--c-d); @@ -876,6 +885,19 @@ input[type=number]::-webkit-outer-spin-button { right: 8px; } +.frz { + left: 36px; + position: absolute; + top: 0px; + cursor: pointer; + padding: 8px; +} + +/* TODO expanded does not seem to apply to .frz, what is this for? */ +.expanded .frz { + display: none; +} + .check, .radio { display: inline-block; position: relative; diff --git a/wled00/data/index.htm b/wled00/data/index.htm index 9263b032f8..33b71e7dbf 100644 --- a/wled00/data/index.htm +++ b/wled00/data/index.htm @@ -28,16 +28,25 @@ -
-

Brightness

-
- -
- -
-
-
-
+
+
+

Brightness

+ +
+ +
+
+
+ +
+

Input Level

+ 🎚 +
+ +
+
+
+
@@ -138,7 +147,7 @@

Effect speed

- +
@@ -247,6 +256,11 @@
Loading...

+ + +