diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 60b00809..9f6250cb 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -48,7 +48,7 @@ body: attributes: label: What version/release of MM 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. build 2401290, WLEDMM_0.14.1-b30.36_esp32_4MB_M.bin" + placeholder: "e.g. build 2401290, WLEDMM_0.14.1-b30.37_esp32_4MB_M.bin" validations: required: true - type: dropdown diff --git a/CHANGELOG.md b/CHANGELOG.md index 11799d26..6369804d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,37 @@ ## [WLED upstream](https://github.com/Aircoookie/WLED/tree/0_14_1) changelog +#### Build 2403290 +- WLED 0.14.3 release +- Fix for transition 0 (#3854, #3832, #3720) + +#### Build 2403170 +- WLED 0.14.2 release + +#### Build 2403110 +- Beta WLED 0.14.2-b2 +- Fixing a potential array bounds violation in ESPDMX +- LockedJsonResponse: Release early if possible (by @willmmiles) + +#### Build 2402120 +- Beta WLED 0.14.2-b1 +- Possible fix for #3589 & partial fix for #3605 +- Prevent JSON buffer clear after failed lock attempt +- Multiple analog button fix for #3549 +- UM Audioreactive: add two compiler options (#3732 by @wled-install) +- Fix for #3693 + +#### Build 2401141 +- Official release of WLED 0.14.1 +- Fix for #3566, #3665, #3672 +- Sorting of palettes in custom palette editor (#3674 by @WoodyLetsCode) + +#### Build 2401060 +- Version bump: 0.14.1-b3 +- Global JSON buffer guarding (#3648 by @willmmiles, resolves #3641, #3312, #3367, #3637, #3646, #3447) +- Fix for #3632 +- Custom palette editor mobile UI enhancement (#3617 by @imeszaros) +- changelog update + #### Build 2312290 - Fix for #3622 - NB: fix for #3613 #3609 are not needed in MoonModules fork @@ -61,6 +93,12 @@ - Improved ESP8266 stability by reducing WebSocket response resends - Updated ESP8266 core to 3.1.2 +#### Build 2306180 + +- Added client-side option for applying effect defaults from metadata +- Improved ESP8266 stability by reducing WebSocket response resends +- Updated ESP8266 core to 3.1.2 + #### Build 2306141 - Lissajous improvements - Scrolling Text improvements (leading 0) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fdf1388c..05c60b8b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,6 +2,16 @@ Here are a few suggestions to make it easier for you to contribute! +### Describe your PR + +Please add a description of your proposed code changes. It does not need to be an exhaustive essay, however a PR with no description or just a few words might not get accepted, simply because very basic information is missing. + +A good description helps us to review and understand your proposed changes. For example, you could say a few words about +* what you try to achieve (new feature, fixing a bug, refactoring, security enhancements, etc.) +* how your code works (short technical summary - focus on important aspects that might not be obvious when reading the code) +* testing you performed, known limitations, open ends you possibly could not solve. +* any areas where you like to get help from an experienced maintainer (yes WLEDMM has become big 😉) + ### Code style When in doubt, it is easiest to replicate the code style you find in the files you want to edit :) @@ -79,4 +89,5 @@ Good: There is no set character limit for a comment within a line, though as a rule of thumb you should wrap your comment if it exceeds the width of your editor window. + Inline comments are OK if they describe that line only and are not exceedingly wide. diff --git a/package-lock.json b/package-lock.json index f8c04b00..eead5461 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wled", - "version": "0.14.1-b30.36", + "version": "0.14.1-b30.37", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "wled", - "version": "0.14.1-b30.36", + "version": "0.14.1-b30.37", "license": "GPL-3.0-or-later", "dependencies": { "clean-css": "^4.2.3", diff --git a/package.json b/package.json index 476577f1..920024ce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wled", - "version": "0.14.1-b30.36", + "version": "0.14.1-b30.37", "description": "Tools for WLED project", "main": "tools/cdata.js", "directories": { diff --git a/platformio.ini b/platformio.ini index 107ab6fe..2da68bd7 100644 --- a/platformio.ini +++ b/platformio.ini @@ -113,7 +113,7 @@ arduino_core_2_7_4 = espressif8266@2.6.2 arduino_core_3_0_0 = espressif8266@3.0.0 arduino_core_3_2_0 = espressif8266@3.2.0 arduino_core_4_1_0 = espressif8266@4.1.0 -arduino_core_3_1_2 = espressif8266@4.2.0 +arduino_core_3_1_2 = espressif8266@4.2.1 # Development platforms arduino_core_develop = https://github.com/platformio/platform-espressif8266#develop @@ -123,8 +123,7 @@ arduino_core_git = https://github.com/platformio/platform-espressif8266#feature/ platform_wled_default = ${common.arduino_core_3_1_2} # We use 2.7.4.7 for all, includes PWM flicker fix and Wstring optimization #platform_packages = tasmota/framework-arduinoespressif8266 @ 3.20704.7 -platform_packages = platformio/framework-arduinoespressif8266 - platformio/toolchain-xtensa @ ~2.100300.220621 #2.40802.200502 +platform_packages = platformio/toolchain-xtensa @ ~2.100300.220621 #2.40802.200502 platformio/tool-esptool #@ ~1.413.0 platformio/tool-esptoolpy #@ ~1.30000.0 @@ -241,6 +240,7 @@ lib_deps = ;;makuna/NeoPixelBus @ 2.7.5 ;; WLEDMM will be added in board specific sections ;;https://github.com/Aircoookie/ESPAsyncWebServer.git @ ~2.0.7 https://github.com/lost-hope/ESPAsyncWebServer.git#master ;; WLEDMM to display .log and .wled files in /edit + ;; https://github.com/Aircoookie/ESPAsyncWebServer.git @ ^2.2.1 #For use of the TTGO T-Display ESP32 Module with integrated TFT display uncomment the following line #TFT_eSPI #For compatible OLED display uncomment following @@ -452,6 +452,11 @@ build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266 #-DWLED lib_deps = ${esp8266.lib_deps} monitor_filters = esp8266_exception_decoder +[env:nodemcuv2_160] +extends = env:nodemcuv2 +board_build.f_cpu = 160000000L +build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP8266_160 #-DWLED_DISABLE_2D + [env:esp8266_2m] board = esp_wroom_02 platform = ${common.platform_wled_default} @@ -461,6 +466,11 @@ build_unflags = ${common.build_unflags} build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP02 lib_deps = ${esp8266.lib_deps} +[env:esp8266_2m_160] +extends = env:esp8266_2m +board_build.f_cpu = 160000000L +build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP02_160 + ;WLEDMM: see below ; [env:esp01_1m_full] ; board = esp01_1m @@ -471,6 +481,11 @@ lib_deps = ${esp8266.lib_deps} ; build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP01 -D WLED_DISABLE_OTA ; ; -D WLED_USE_UNREAL_MATH ;; may cause wrong sunset/sunrise times, but saves 7064 bytes FLASH and 975 bytes RAM ; lib_deps = ${esp8266.lib_deps} +; [env:esp01_1m_full_160] +; extends = env:esp01_1m_full +; board_build.f_cpu = 160000000L +; build_flags = ${common.build_flags_esp8266} -D WLED_RELEASE_NAME=ESP01_160 -D WLED_DISABLE_OTA +; ; -D WLED_USE_UNREAL_MATH ;; may cause wrong sunset/sunrise times, but saves 7064 bytes FLASH and 975 bytes RAM [env:esp07] board = esp07 @@ -963,6 +978,7 @@ build_flags_S = ; -D WLED_DISABLE_2D ;; un-comment to build a firmware without 2D matrix support ; -D WLED_USE_CIE_BRIGHTNESS_TABLE ;; experimental: use different color / brightness lookup table -D USERMOD_AUDIOREACTIVE + -D USERMOD_AUTO_PLAYLIST -D UM_AUDIOREACTIVE_USE_NEW_FFT ; use latest (upstream) FFTLib, instead of older library modified by blazoncek. Slightly faster, more accurate, needs 2KB RAM extra ; -D USERMOD_ARTIFX ;; WLEDMM usermod - temporarily moved into "_M", due to problems in "_S" when compiling with -O2 -D WLEDMM_FASTPATH ;; WLEDMM experimental option. Reduces audio lag (latency), and allows for faster LED framerates. May break compatibility with previous versions. diff --git a/usermods/audioreactive/audio_reactive.h b/usermods/audioreactive/audio_reactive.h index 42ce5a0a..be86186e 100644 --- a/usermods/audioreactive/audio_reactive.h +++ b/usermods/audioreactive/audio_reactive.h @@ -7,6 +7,7 @@ #include #include +#include #endif #if defined(ARDUINO_ARCH_ESP32) && (defined(WLED_DEBUG) || defined(SR_DEBUG)) @@ -141,6 +142,8 @@ static uint8_t fftResult[NUM_GEQ_CHANNELS]= {0}; // Our calculated freq. chann static float fftCalc[NUM_GEQ_CHANNELS] = {0.0f}; // Try and normalize fftBin values to a max of 4096, so that 4096/16 = 256. (also used by dynamics limiter) static float fftAvg[NUM_GEQ_CHANNELS] = {0.0f}; // Calculated frequency channel results, with smoothing (used if dynamics limiter is ON) +static uint16_t zeroCrossingCount = 0; // number of zero crossings in the current batch of 512 samples + // TODO: probably best not used by receive nodes static float agcSensitivity = 128; // AGC sensitivity estimation, based on agc gain (multAgc). calculated by getSensitivity(). range 0..255 @@ -543,15 +546,27 @@ void FFTcode(void * parameter) } } - // find highest sample in the batch + // set imaginary parts to 0 + memset(vImag, 0, sizeof(vImag)); + + // find highest sample in the batch, and count zero crossings float maxSample = 0.0f; // max sample from FFT batch + uint_fast16_t newZeroCrossingCount = 0; 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 (fabsf((float)vReal[i]) > maxSample) maxSample = fabsf((float)vReal[i]); + + // WLED-MM/TroyHacks: Calculate zero crossings + // + if (i < (samplesFFT-1)) { + if (__builtin_signbit(vReal[i]) != __builtin_signbit(vReal[i+1])) // test sign bit: sign changed -> zero crossing + newZeroCrossingCount++; + } } + newZeroCrossingCount = (newZeroCrossingCount*2)/3; // reduce value so it typicially stays below 256 + zeroCrossingCount = newZeroCrossingCount; // update only once, to avoid that effects pick up an intermediate value + // release highest sample to volume reactive effects early - not strictly necessary here - could also be done at the end of the function // early release allows the filters (getSample() and agcAvg()) to work with fresh values - we will have matching gain and noise gate values when we want to process the FFT results. micDataReal = maxSample; @@ -770,8 +785,7 @@ void FFTcode(void * parameter) // run peak detection autoResetPeak(); detectSamplePeak(); - - // we have new results - notify UDP sound send + haveNewFFTResult = true; #if !defined(I2S_GRAB_ADC1_COMPLETELY) @@ -1007,7 +1021,7 @@ class AudioReactive : public Usermod { uint8_t samplePeak; // 01 Bytes offset 16 - 0 no peak; >=1 peak detected. In future, this will also provide peak Magnitude uint8_t frameCounter; // 01 Bytes offset 17 - track duplicate/out of order packets uint8_t fftResult[16]; // 16 Bytes offset 18 - uint8_t gap2[2]; // gap added by compiler: 02 Bytes, offset 34 + uint16_t zeroCrossingCount; // 02 Bytes, offset 34 float FFT_Magnitude; // 04 Bytes offset 36 float FFT_MajorPeak; // 04 Bytes offset 40 }; @@ -1547,6 +1561,7 @@ class AudioReactive : public Usermod { transmitData.samplePeak = udpSamplePeak ? 1:0; udpSamplePeak = false; // Reset udpSamplePeak after we've transmitted it transmitData.frameCounter = frameCounter; + transmitData.zeroCrossingCount = zeroCrossingCount; for (int i = 0; i < NUM_GEQ_CHANNELS; i++) { transmitData.fftResult[i] = (uint8_t)constrain(fftResult[i], 0, 254); @@ -1619,6 +1634,7 @@ class AudioReactive : public Usermod { FFT_MajorPeak = constrain(receivedPacket->FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects soundPressure = volumeSmth; // substitute - V2 format does not (yet) include this value agcSensitivity = 128.0f; // substitute - V2 format does not (yet) include this value + zeroCrossingCount = receivedPacket->zeroCrossingCount; return true; } @@ -1720,7 +1736,7 @@ class AudioReactive : public Usermod { // usermod exchangeable data // we will assign all usermod exportable data here as pointers to original variables or arrays and allocate memory for pointers um_data = new um_data_t; - um_data->u_size = 11; + um_data->u_size = 12; um_data->u_type = new um_types_t[um_data->u_size]; um_data->u_data = new void*[um_data->u_size]; um_data->u_data[0] = &volumeSmth; //*used (New) @@ -1746,6 +1762,8 @@ class AudioReactive : public Usermod { um_data->u_type[9] = UMT_FLOAT; um_data->u_data[10] = &agcSensitivity; // used (New) um_data->u_type[10] = UMT_FLOAT; + um_data->u_data[11] = &zeroCrossingCount; + um_data->u_type[11] = UMT_UINT16; #else // ESP8266 // See https://github.com/MoonModules/WLED/pull/60#issuecomment-1666972133 for explanation of these alternative sources of data @@ -1760,6 +1778,8 @@ class AudioReactive : public Usermod { um_data->u_type[9] = UMT_FLOAT; um_data->u_data[10] = &agcSensitivity; // used (New) - dummy value (128 => 50%) um_data->u_type[10] = UMT_FLOAT; + um_data->u_data[11] = &zeroCrossingCount; + um_data->u_type[11] = UMT_UINT16; #endif } @@ -1921,7 +1941,7 @@ class AudioReactive : public Usermod { USER_PRINTF("| uint8_t samplePeak offset = %2d size = %2d\n", offsetof(audioSyncPacket, samplePeak), sizeof(data.samplePeak)); // offset 16 size 1 USER_PRINTF("| uint8_t frameCounter offset = %2d size = %2d\n", offsetof(audioSyncPacket, frameCounter), sizeof(data.frameCounter)); // offset 17 size 1 USER_PRINTF("| uint8_t fftResult[16] offset = %2d size = %2d\n", offsetof(audioSyncPacket, fftResult[0]), sizeof(data.fftResult)); // offset 18 size 16 - USER_PRINTF("| uint8_t gap2[2] offset = %2d size = %2d\n", offsetof(audioSyncPacket, gap2[0]), sizeof(data.gap2)); // offset 34 size 2 + USER_PRINTF("| uint16_t zeroCrossingCount offset = %2d size = %2d\n", offsetof(audioSyncPacket, zeroCrossingCount), sizeof(data.zeroCrossingCount)); // offset 34 size 2 USER_PRINTF("| float FFT_Magnitude offset = %2d size = %2d\n", offsetof(audioSyncPacket, FFT_Magnitude), sizeof(data.FFT_Magnitude));// offset 36 size 4 USER_PRINTF("| float FFT_MajorPeak offset = %2d size = %2d\n", offsetof(audioSyncPacket, FFT_MajorPeak), sizeof(data.FFT_MajorPeak));// offset 40 size 4 USER_PRINTLN(); USER_FLUSH(); diff --git a/usermods/audioreactive/audio_source.h b/usermods/audioreactive/audio_source.h index 5acbed46..8f552bbc 100644 --- a/usermods/audioreactive/audio_source.h +++ b/usermods/audioreactive/audio_source.h @@ -667,7 +667,13 @@ class WM8978Source : public I2SSource { _wm8978I2cWrite( 1,0b000111110); // Power Management 1 - power off most things, but enable mic bias and I/O tie-off to help mitigate mic leakage. _wm8978I2cWrite( 2,0b110111111); // Power Management 2 - enable output and amp stages (amps may lift signal but it works better on the ADCs) _wm8978I2cWrite( 3,0b000001100); // Power Management 3 - enable L&R output mixers + + #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(4, 2, 0) _wm8978I2cWrite( 4,0b001010000); // Audio Interface - standard I2S, 24-bit + #else + _wm8978I2cWrite( 4,0b001001000); // Audio Interface - left-justified I2S, 24-bit + #endif + _wm8978I2cWrite( 6,0b000000000); // Clock generation control - use external mclk _wm8978I2cWrite( 7,0b000000100); // Sets sample rate to ~24kHz (only used for internal calculations, not I2S) _wm8978I2cWrite(14,0b010001000); // 128x ADC oversampling - high pass filter disabled as it kills the bass response diff --git a/usermods/usermod_v2_auto_playlist/usermod_v2_auto_playlist.h b/usermods/usermod_v2_auto_playlist/usermod_v2_auto_playlist.h new file mode 100644 index 00000000..c15f116e --- /dev/null +++ b/usermods/usermod_v2_auto_playlist/usermod_v2_auto_playlist.h @@ -0,0 +1,510 @@ +#pragma once + +#ifdef WLED_DEBUG + #ifndef USERMOD_AUTO_PLAYLIST_DEBUG + #define USERMOD_AUTO_PLAYLIST_DEBUG + #endif +#endif + +#include "wled.h" + +class AutoPlaylistUsermod : public Usermod { + + private: + + // experimental parameters by softhack007 - more balanced but need testing + const uint_fast32_t MAX_DISTANCE_TRACKER = 184; // maximum accepted distance_tracker + const uint_fast32_t ENERGY_SCALE = 1500; + const float FILTER_SLOW1 = 0.0075f; // for "slow" energy - was 0.01f + const float FILTER_SLOW2 = 0.005f; // for "slow" lfc / zcr - was 0.01f + const float FILTER_FAST1 = 0.2f; // for "fast" energy - was 0.10f + const float FILTER_FAST2 = 0.25f; // for "fast" lfc / zcr - was 0.10f + const float FILTER_VOLUME = 0.03f; // for volumeSmth averaging - takes 8-10sec until "silence" + + bool initDone = false; + bool functionality_enabled = false; + bool silenceDetected = true; + byte ambientPlaylist = 1; + byte musicPlaylist = 2; + int timeout = 60; + bool autoChange = false; + byte lastAutoPlaylist = 0; + unsigned long lastSoundTime = millis()-(timeout*1000)-100; + unsigned long change_timer = millis(); + unsigned long autochange_timer = millis(); + float avg_volumeSmth = 0; + + // fftesult de-scaling factors: 2.8f / fftResultPink[] + const float fftDeScaler[NUM_GEQ_CHANNELS] = {2.8/2.35, 2.8/1.32, 2.8/1.32, 2.8/1.40, 2.8/1.48, 2.8/1.57, 2.8/1.68, 2.8/1.80, 2.8/1.89, 2.8/1.95, 2.8/2.14, 2.8/2.26, 2.8/2.50, 2.8/2.90, 2.8/4.20, 2.8/6.50}; + + uint_fast32_t energy = 0; + + float avg_long_energy = 250; + float avg_long_lfc = 1000; + float avg_long_zcr = 500; + + float avg_short_energy = 250; + float avg_short_lfc = 1000; + float avg_short_zcr = 500; + + bool resetFilters = true; // to (re)initialize filters on first run + uint_fast32_t vector_energy = 0; + uint_fast32_t vector_lfc = 0; + uint_fast32_t vector_zcr = 0; + + uint_fast32_t distance = 0; + uint_fast32_t distance_tracker = UINT_FAST32_MAX; + + unsigned long lastchange = millis(); + + int_fast16_t change_threshold = 50; // arbitrary starting point. + uint_fast16_t change_threshold_change = 0; + + int change_lockout = 1000; // never change below this number of millis. Ideally 60000/your_average_bpm*beats_to_skip = change_lockout (1000 = skip 2 beats at 120bpm) + int ideal_change_min = 10000; // ideally change patterns no less than this number of millis + int ideal_change_max = 20000; // ideally change patterns no more than this number of millis + + std::vector autoChangeIds; + + static const char _name[]; + static const char _autoPlaylistEnabled[]; + static const char _ambientPlaylist[]; + static const char _musicPlaylist[]; + static const char _timeout[]; + static const char _autoChange[]; + static const char _change_lockout[]; + static const char _ideal_change_min[]; + static const char _ideal_change_max[]; + + public: + + AutoPlaylistUsermod(bool enabled):Usermod("AutoPlaylist", enabled) { + // noop + } + + // gets called once at boot. Do all initialization that doesn't depend on + // network here + void setup() { + USER_PRINTLN("AutoPlaylistUsermod"); + initDone = true; + } + + // gets called every time WiFi is (re-)connected. Initialize own network + // interfaces here + void connected() { + // noop + } + + void change(um_data_t *um_data) { + + uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; + + energy = 0; + + for (int i=0; i < NUM_GEQ_CHANNELS; i++) { + + // make an attempt to undo some "trying to look better" FFT manglings in AudioReactive postProcessFFTResults() + + float amplitude = float(fftResult[i]) * fftDeScaler[i]; // undo "pink noise" scaling + amplitude /= 0.85f + (float(i)/4.5f); // undo extra up-scaling for high frequencies + energy += roundf(amplitude * amplitude); // calc energy from amplitude + + } + + energy /= ENERGY_SCALE; // scale down so we get 0 sometimes + + uint16_t lfc = float(fftResult[0]) * fftDeScaler[0] / 0.85f; // might as well undo pink noise here too. + uint16_t zcr = *(uint16_t*)um_data->u_data[11]; + + // WLED-MM/TroyHacks: Calculate the long- and short-running averages + // and the individual vector distances. + + if (volumeSmth > 1.0f) { + + // initialize filters on first run + if (resetFilters) { + avg_short_energy = avg_long_energy = energy; + avg_short_lfc = avg_long_lfc = lfc; + avg_short_zcr = avg_long_zcr = zcr; + resetFilters = false; + #ifdef USERMOD_AUTO_PLAYLIST_DEBUG + USER_PRINTLN("AutoPlaylist: Filters reset."); + #endif + } + + avg_long_energy = avg_long_energy + FILTER_SLOW1 * (float(energy) - avg_long_energy); + avg_long_lfc = avg_long_lfc + FILTER_SLOW2 * (float(lfc) - avg_long_lfc); + avg_long_zcr = avg_long_zcr + FILTER_SLOW2 * (float(zcr) - avg_long_zcr); + + avg_short_energy = avg_short_energy + FILTER_FAST1 * (float(energy) - avg_short_energy); + avg_short_lfc = avg_short_lfc + FILTER_FAST2 * (float(lfc) - avg_short_lfc); + avg_short_zcr = avg_short_zcr + FILTER_FAST2 * (float(zcr) - avg_short_zcr); + + // allegedly this is faster than pow(whatever,2) + vector_lfc = (avg_short_lfc-avg_long_lfc)*(avg_short_lfc-avg_long_lfc); + vector_energy = (avg_short_energy-avg_long_energy)*(avg_short_energy-avg_long_energy); + vector_zcr = (avg_short_zcr-avg_long_zcr)*(avg_short_zcr-avg_long_zcr); + + } + + distance = vector_lfc + vector_energy + vector_zcr; + + long change_interval = millis()-lastchange; + + if (distance < distance_tracker && change_interval > change_lockout && volumeSmth > 1.0f) { + distance_tracker = distance; + } + + // Debug for adjusting formulas, etc: + // USER_PRINTF("Distance: %5lu - v_lfc: %5lu v_energy: %5lu v_zcr: %5lu\n",(unsigned long)distance,(unsigned long)vector_lfc,(unsigned long)vector_energy,(unsigned long)vector_zcr); + + if ((millis() - change_timer) > ideal_change_min) { // softhack007 same result as "millis() > change_timer + ideal_change_min", but more robust against unsigned overflow + + // Make the analysis less sensitive if we miss the window. + // Sometimes the analysis lowers the change_threshold too much for + // the current music, especially after track changes or during + // sparse intros and breakdowns. + + if (change_interval > ideal_change_min && distance_tracker <= MAX_DISTANCE_TRACKER) { + + if (distance_tracker >= change_threshold) { + change_threshold_change = distance_tracker-change_threshold; + } else { + change_threshold_change = change_threshold-distance_tracker; + } + + change_threshold = distance_tracker; + + if (change_threshold_change > 9999) change_threshold_change = 0; // cosmetic for debug + + if (functionality_enabled) { + #ifdef USERMOD_AUTO_PLAYLIST_DEBUG + USER_PRINTF("--- lowest distance = %4lu - no changes done in %6ldms - next change_threshold is %4u (%4u diff approx)\n", (unsigned long)distance_tracker,change_interval,change_threshold,change_threshold_change); + #endif + } + + distance_tracker = UINT_FAST32_MAX; + + } + + change_timer = millis(); + + } + + if (distance <= change_threshold && change_interval > change_lockout && volumeSmth > 1.0f) { + + change_threshold_change = max(1.0f, roundf(change_threshold-(distance*0.9f))); // exclude negatives, ensure change_threshold_change is always >= 1 + + if (change_interval > ideal_change_max) { + change_threshold += change_threshold_change; // make changes more sensitive + } else if (change_interval < ideal_change_min) { + change_threshold -= change_threshold_change; // make changes less sensitive + } else { + change_threshold_change = 0; // change was within our window, no sensitivity change + } + + if (change_threshold < 1) change_threshold = 0; // we need change_threshold to be signed because otherwise this wraps to UINT_FAST16_MAX + + distance_tracker = UINT_FAST32_MAX; + + if (functionality_enabled) { + + if (autoChangeIds.size() == 0) { + if(currentPlaylist < 1) return; + + #ifdef USERMOD_AUTO_PLAYLIST_DEBUG + USER_PRINTF("Loading presets from playlist: %3d\n", currentPlaylist); + #endif + + JsonObject playtlistOjb = doc.to(); + serializePlaylist(playtlistOjb); + JsonArray playlistArray = playtlistOjb["playlist"]["ps"]; + + for(JsonVariant v : playlistArray) { + #ifdef USERMOD_AUTO_PLAYLIST_DEBUG + USER_PRINTF("Adding %3u to autoChangeIds\n", v.as()); + #endif + autoChangeIds.push_back(v.as()); + } + + } + + uint8_t newpreset = 0; + + do { + newpreset = autoChangeIds.at(random(0, autoChangeIds.size())); // random() is *exclusive* of the last value, so it's OK to use the full size. + } while ((currentPreset == newpreset) && (autoChangeIds.size() > 1)); // make sure we get a different random preset. Unless there is only one. + + if (change_interval > change_lockout+3) { + + // Make sure we have a statistically significant change and we aren't + // just bouncing off change_lockout. That's valid for changing the + // thresholds, but might be a bit crazy for lighting changes. + // When the music changes quite a bit, the distance calculation can + // go into freefall - this logic stops that from triggering right + // after change_lockout. Better for smaller change_lockout values. + + suspendPlaylist(); // suspend the playlist engine before changing to another preset + applyPreset(newpreset); + + #ifdef USERMOD_AUTO_PLAYLIST_DEBUG + USER_PRINTF("*** CHANGE distance = %4lu - change_interval was %5ldms - next change_threshold is %4u (%4u diff aprox)\n",(unsigned long)distance,change_interval,change_threshold,change_threshold_change); + #endif + + } else { + + #ifdef USERMOD_AUTO_PLAYLIST_DEBUG + USER_PRINTF("^^^ SKIP!! distance = %4lu - change_interval was %5ldms - next change_threshold is %4u (%4u diff aprox)\n",(unsigned long)distance,change_interval,change_threshold,change_threshold_change); + #endif + + } + + } + + lastchange = millis(); + change_timer = millis(); + + } + + } + + /* + * Da loop. + */ + void loop() { + + if (!enabled) return; + + if (millis() < 10000) return; // Wait for device to settle + + if (lastAutoPlaylist > 0 && currentPlaylist != lastAutoPlaylist && currentPreset != 0) { + if (functionality_enabled) { + #ifdef USERMOD_AUTO_PLAYLIST_DEBUG + USER_PRINTF("AutoPlaylist: disable due to manual change of playlist from %u to %d, preset:%u\n", lastAutoPlaylist, currentPlaylist, currentPreset); + #endif + functionality_enabled = false; + } else if (currentPlaylist == musicPlaylist) { + #ifdef USERMOD_AUTO_PLAYLIST_DEBUG + USER_PRINTF("AutoPlaylist: enabled due to manual change of playlist back to %u\n", currentPlaylist); + #endif + functionality_enabled = true; + lastAutoPlaylist = currentPlaylist; + } + } + + if (!functionality_enabled && currentPlaylist == musicPlaylist) { + #ifdef USERMOD_AUTO_PLAYLIST_DEBUG + USER_PRINTF("AutoPlaylist: enabled due selecting musicPlaylist(%u)\n", musicPlaylist); + #endif + functionality_enabled = true; + } + + if (bri == 0) return; + + um_data_t *um_data; + + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // No Audio Reactive + silenceDetected = true; + return; + } + + float volumeSmth = *(float*)um_data->u_data[0]; + + avg_volumeSmth = avg_volumeSmth + FILTER_VOLUME * (volumeSmth - avg_volumeSmth); + + if (avg_volumeSmth >= 1.0f) { + lastSoundTime = millis(); + } + + if (millis() - lastSoundTime > (long(timeout) * 1000)) { + if (!silenceDetected) { + silenceDetected = true; + USER_PRINTLN("AutoPlaylist: Silence detected"); + changePlaylist(ambientPlaylist); + } + } else { + if (silenceDetected) { + silenceDetected = false; + USER_PRINTLN("AutoPlaylist: Sound detected"); + changePlaylist(musicPlaylist); + } + if (autoChange && millis() >= autochange_timer+22) { + change(um_data); + autochange_timer = millis(); + } + } + } + + /* + * 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 addToJsonInfo(JsonObject& root) { + JsonObject user = root["u"]; + if (user.isNull()) { + user = root.createNestedObject("u"); + } + + if (!enabled) return; // usermod disabled -> don't add to info page + + String uiNameString = FPSTR(_name); + if (enabled && functionality_enabled) { + uiNameString += F(" Running"); + } else if (!enabled) { + uiNameString += F(" Disabled"); + } else { + uiNameString += F(" Suspended"); + } + JsonArray infoArr = user.createNestedArray(uiNameString); // name + status + + String uiDomString = (currentPlaylist > 0) ? String("#") + String(currentPlaylist) + String(" ") : String(""); + + if (currentPlaylist == musicPlaylist && currentPlaylist > 0) { + uiDomString += F("Music Playlist"); + } else if (currentPlaylist == ambientPlaylist && currentPlaylist > 0) { + uiDomString += F("Ambient Playlist"); + } else { + uiDomString += F("Playlist Overridden"); + } + + uiDomString += F("
"); + + if (enabled && autoChange && currentPlaylist == musicPlaylist && functionality_enabled) { + uiDomString += F("AutoChange is Active"); + } else if (autoChange && (currentPlaylist != musicPlaylist || !functionality_enabled || !enabled)) { + uiDomString += F("AutoChange on Stand-by"); + } else if (!autoChange) { + uiDomString += F("AutoChange is Disabled"); + } + + // #ifdef USERMOD_AUTO_PLAYLIST_DEBUG + // uiDomString += F("
"); + // uiDomString += F("Change Threshold: "); + // uiDomString += String(change_threshold); + // #endif + + infoArr.add(uiDomString); + + } + + /* + * 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) { + return; + } + + void appendConfigData() { + oappend(SET_F("addHB('AutoPlaylist');")); + } + + /* + * 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 also not yet add your setting to one of the settings pages automatically. + * To make that work you still have to add the setting to the HTML, xml.cpp and set.cpp manually. + * + * 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(FPSTR(_name)); // usermodname + + top[FPSTR(_autoPlaylistEnabled)] = enabled; + top[FPSTR(_timeout)] = timeout; + top[FPSTR(_ambientPlaylist)] = ambientPlaylist; // usermodparam + top[FPSTR(_musicPlaylist)] = musicPlaylist; // usermodparam + top[FPSTR(_autoChange)] = autoChange; + top[FPSTR(_change_lockout)] = change_lockout; + top[FPSTR(_ideal_change_min)] = ideal_change_min; + top[FPSTR(_ideal_change_max)] = ideal_change_max; + + lastAutoPlaylist = 0; + + #ifdef USERMOD_AUTO_PLAYLIST_DEBUG + USER_PRINTLN(F("AutoPlaylist config saved.")); + #endif + + } + + /* + * 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 once immediately after boot) + * + * 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 :) + * + * The function should return true if configuration was successfully loaded or false if there was no configuration. + */ + bool readFromConfig(JsonObject& root) { + + JsonObject top = root[FPSTR(_name)]; + + if (top.isNull()) { + USER_PRINT(FPSTR(_name)); + USER_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + enabled = top[FPSTR(_autoPlaylistEnabled)] | enabled; + timeout = top[FPSTR(_timeout)] | timeout; + ambientPlaylist = top[FPSTR(_ambientPlaylist)] | ambientPlaylist; + musicPlaylist = top[FPSTR(_musicPlaylist)] | musicPlaylist; + autoChange = top[FPSTR(_autoChange)] | autoChange; + change_lockout = top[FPSTR(_change_lockout)] | change_lockout; + ideal_change_min = top[FPSTR(_ideal_change_min)] | ideal_change_min; + ideal_change_max = top[FPSTR(_ideal_change_max)] | ideal_change_max; + + #ifdef USERMOD_AUTO_PLAYLIST_DEBUG + USER_PRINT(FPSTR(_name)); + USER_PRINTLN(F(" config (re)loaded.")); + #endif + + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + return true; + + } + + /* + * 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_AUTOPLAYLIST; + } + + private: + + void changePlaylist(byte id) { + String name = ""; + getPresetName(id, name); + #ifdef USERMOD_AUTO_PLAYLIST_DEBUG + USER_PRINTF("AutoPlaylist: Applying \"%s\"\n", name.c_str()); + #endif + // if (currentPlaylist != id) { // un-comment to only change on "real" changes + unloadPlaylist(); // applying a preset requires to unload previous playlist + applyPreset(id, CALL_MODE_NOTIFICATION); + // } + lastAutoPlaylist = id; + } + +}; + +const char AutoPlaylistUsermod::_name[] PROGMEM = "AutoPlaylist"; +const char AutoPlaylistUsermod::_autoPlaylistEnabled[] PROGMEM = "enabled"; +const char AutoPlaylistUsermod::_ambientPlaylist[] PROGMEM = "ambientPlaylist"; +const char AutoPlaylistUsermod::_musicPlaylist[] PROGMEM = "musicPlaylist"; +const char AutoPlaylistUsermod::_timeout[] PROGMEM = "timeout"; +const char AutoPlaylistUsermod::_autoChange[] PROGMEM = "autoChange"; +const char AutoPlaylistUsermod::_change_lockout[] PROGMEM = "change_lockout"; +const char AutoPlaylistUsermod::_ideal_change_min[] PROGMEM = "ideal_change_min"; +const char AutoPlaylistUsermod::_ideal_change_max[] PROGMEM = "ideal_change_max"; diff --git a/wled00/FX.cpp b/wled00/FX.cpp index c4b1e0e9..5fa0a0fe 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -3194,7 +3194,13 @@ static uint16_t mode_popcorn_core(bool useaudio) { if (SEGLEN == 1) return mode_static(); //allocate segment data uint16_t strips = SEGMENT.nrOfVStrips(); - uint16_t dataSize = sizeof(spark) * maxNumPopcorn; + size_t dataSize = sizeof(spark) * maxNumPopcorn; + uint8_t neededPopcorn = maxNumPopcorn; // WLEDMM + if (strips > 8) { // WLEDMM more than 8 virtual strips --> reduce memory requirements to minimum necessary + neededPopcorn = (SEGMENT.intensity*maxNumPopcorn)/255; + neededPopcorn = min(max(neededPopcorn, uint8_t(2)), uint8_t(maxNumPopcorn)); + dataSize = sizeof(spark) * neededPopcorn; + } if (!SEGENV.allocateData(dataSize * strips)) return mode_static(); //allocation failed Spark* popcorn = reinterpret_cast(SEGENV.data); @@ -3212,7 +3218,7 @@ static uint16_t mode_popcorn_core(bool useaudio) { struct virtualStrip { static void runStrip(uint16_t stripNr, Spark* popcorn, bool useaudio, um_data_t *um_data) { // WLEDMM added useaudio and um_data - float gravity = -0.0001 - (SEGMENT.speed/200000.0); // m/s/s + float gravity = -0.0001f - (SEGMENT.speed/200000.0f); // m/s/s gravity *= SEGLEN; uint8_t numPopcorn = SEGMENT.intensity*maxNumPopcorn/255; @@ -3267,7 +3273,7 @@ static uint16_t mode_popcorn_core(bool useaudio) { }; for (int stripNr=0; stripNr customMappingTableSize) { - size_t size = max(ledmapMaxSize, size_t(Segment::maxWidth * Segment::maxHeight));//TroyHack + size_t size = max(ledmapMaxSize, size_t(Segment::maxWidth * Segment::maxHeight)); // TroyHacks USER_PRINTF("setupmatrix customMappingTable alloc %d from %d\n", size, customMappingTableSize); //if (customMappingTable != nullptr) delete[] customMappingTable; //customMappingTable = new uint16_t[size]; @@ -80,7 +80,10 @@ void WS2812FX::setUpMatrix() { if ((size > 0) && (customMappingTable == nullptr)) { // second try DEBUG_PRINTLN("setUpMatrix: trying to get fresh memory block."); customMappingTable = (uint16_t*) calloc(size, sizeof(uint16_t)); - if (customMappingTable == nullptr) USER_PRINTLN("setUpMatrix: alloc failed"); + if (customMappingTable == nullptr) { + USER_PRINTLN("setUpMatrix: alloc failed"); + errorFlag = ERR_LOW_MEM; // WLEDMM raise errorflag + } } if (customMappingTable != nullptr) customMappingTableSize = size; } @@ -159,6 +162,7 @@ void WS2812FX::setUpMatrix() { } else { // memory allocation error customMappingTableSize = 0; USER_PRINTLN(F("Ledmap alloc error.")); + errorFlag = ERR_LOW_MEM; // WLEDMM raise errorflag isMatrix = false; //WLEDMM does not like this done in teh background while end users are confused whats happened... panels = 0; panel.clear(); diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index fa464320..0bf310dc 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -115,7 +115,7 @@ Segment::Segment(const Segment &orig) { //WLEDMM: recreate ledsrgb if more space needed (will not free ledsrgb!) void Segment::allocLeds() { - size_t size = sizeof(CRGB)*max((size_t) length(), ledmapMaxSize); //TroyHack + size_t size = sizeof(CRGB)*max((size_t) length(), ledmapMaxSize); // TroyHacks if ((size < sizeof(CRGB)) || (size > 164000)) { //softhack too small (<3) or too large (>160Kb) DEBUG_PRINTF("allocLeds warning: size == %u !!\n", size); if (ledsrgb && (ledsrgbSize == 0)) { @@ -128,10 +128,13 @@ void Segment::allocLeds() { if (ledsrgb) free(ledsrgb); // we need a bigger buffer, so free the old one first ledsrgb = (CRGB*)calloc(size, 1); ledsrgbSize = ledsrgb?size:0; - if (ledsrgb == nullptr) USER_PRINTLN("allocLeds failed!!"); + if (ledsrgb == nullptr) { + USER_PRINTLN("allocLeds failed!!"); + errorFlag = ERR_LOW_MEM; // WLEDMM raise errorflag + } } else { - USER_PRINTF("reuse Leds %u from %u\n", size, ledsrgb?ledsrgbSize:0); + //USER_PRINTF("reuse Leds %u from %u\n", size, ledsrgb?ledsrgbSize:0); } } @@ -212,7 +215,11 @@ bool Segment::allocateData(size_t len) { //DEBUG_PRINTF("allocateData(%u) start %d, stop %d, vlen %d\n", len, start, stop, virtualLength()); deallocateData(); if (len == 0) return false; // nothing to do - if (Segment::getUsedSegmentData() + len > MAX_SEGMENT_DATA) return false; //not enough memory + if (Segment::getUsedSegmentData() + len > MAX_SEGMENT_DATA) { + //USER_PRINTF("Segment::allocateData: Segment data quota exceeded! used:%u request:%u max:%d\n", Segment::getUsedSegmentData(), len, MAX_SEGMENT_DATA); + if (len > 0) errorFlag = ERR_LOW_SEG_MEM; // WLEDMM raise errorflag + return false; //not enough memory + } // do not use SPI RAM on ESP32 since it is slow //#if defined(ARDUINO_ARCH_ESP32) && defined(BOARD_HAS_PSRAM) && defined(WLED_USE_PSRAM) //if (psramFound()) @@ -220,10 +227,17 @@ bool Segment::allocateData(size_t len) { //else //#endif data = (byte*) malloc(len); - if (!data) { _dataLen = 0; return false;} //allocation failed // WLEDMM reset dataLen + if (!data) { + _dataLen = 0; // WLEDMM reset dataLen + errorFlag = ERR_LOW_MEM; // WLEDMM raise errorflag + USER_PRINT(F("Segment::allocateData: FAILED to allocate ")); + USER_PRINT(len); USER_PRINTLN(F(" bytes.")); + return false; + } //allocation failed Segment::addUsedSegmentData(len); _dataLen = len; memset(data, 0, len); + if (errorFlag == ERR_LOW_SEG_MEM) errorFlag = ERR_NONE; // WLEDMM reset errorflag on success return true; } @@ -231,7 +245,7 @@ void Segment::deallocateData() { if (!data) {_dataLen = 0; return;} // WLEDMM reset dataLen free(data); data = nullptr; - //DEBUG_PRINTLN("deallocateData() called free()."); + //USER_PRINTF("Segment::deallocateData: free'd %d bytes.\n", _dataLen); Segment::addUsedSegmentData(-_dataLen); _dataLen = 0; } @@ -482,6 +496,8 @@ void Segment::setUp(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, uint16_t && (!grp || (grouping == grp && spacing == spc)) && (ofs == UINT16_MAX || ofs == offset)) return; + stateChanged = true; // send UDP/WS broadcast + if (stop>start) fill(BLACK); //turn old segment range off // WLEDMM stop > start if (i2 <= i1) { //disable segment stop = 0; @@ -1536,7 +1552,7 @@ void WS2812FX::enumerateLedmaps() { USER_PRINTF("enumerateLedmaps %s \"%s\"", fileName, name); if (isMatrix) { - //WLEDMM calc ledmapMaxSize (TroyHack) + //WLEDMM calc ledmapMaxSize (TroyHacks) char dim[34] = { '\0' }; f.find("\"width\":"); f.readBytesUntil('\n', dim, sizeof(dim)-1); //hack: use fileName as we have this allocated already @@ -1654,6 +1670,7 @@ void WS2812FX::finalizeInit(void) //#endif if (arrSize > 0) Segment::_globalLeds = (CRGB*) malloc(arrSize); // WLEDMM avoid malloc(0) if ((Segment::_globalLeds != nullptr) && (arrSize > 0)) memset(Segment::_globalLeds, 0, arrSize); // WLEDMM avoid dereferencing nullptr + if ((Segment::_globalLeds == nullptr) && (arrSize > 0)) errorFlag = ERR_LOW_MEM; // WLEDMM raise errorflag } //segments are created in makeAutoSegments(); @@ -2273,16 +2290,16 @@ void WS2812FX::loadCustomPalettes() { if (readObjectFromFile(fileName, nullptr, &pDoc)) { JsonArray pal = pDoc[F("palette")]; - if (!pal.isNull() && pal.size()>4) { // not an empty palette (at least 2 entries) + if (!pal.isNull() && pal.size()>3) { // not an empty palette (at least 2 entries) if (pal[0].is() && pal[1].is()) { // we have an array of index & hex strings size_t palSize = min(pal.size(), (size_t)36); // WLEDMM use native min/max palSize -= palSize % 2; // make sure size is multiple of 2 - for (size_t i=0, j=0; i()<256; i+=2, j+=4) { + for (unsigned i=0, j=0; i()<256; i+=2, j+=4) { uint8_t rgbw[] = {0,0,0,0}; tcp[ j ] = (uint8_t) pal[ i ].as(); // index colorFromHexString(rgbw, pal[i+1].as()); // will catch non-string entires - for (size_t c=0; c<3; c++) tcp[j+1+c] = rgbw[c]; // only use RGB component + for (unsigned c=0; c<3; c++) tcp[j+1+c] = gamma8(rgbw[c]); // only use RGB component DEBUG_PRINTF("%d(%d) : %d %d %d\n", i, int(tcp[j]), int(tcp[j+1]), int(tcp[j+2]), int(tcp[j+3])); } } else { @@ -2290,13 +2307,15 @@ void WS2812FX::loadCustomPalettes() { palSize -= palSize % 4; // make sure size is multiple of 4 for (size_t i=0; i()<256; i+=4) { tcp[ i ] = (uint8_t) pal[ i ].as(); // index - tcp[i+1] = (uint8_t) pal[i+1].as(); // R - tcp[i+2] = (uint8_t) pal[i+2].as(); // G - tcp[i+3] = (uint8_t) pal[i+3].as(); // B + tcp[i+1] = gamma8((uint8_t) pal[i+1].as()); // R + tcp[i+2] = gamma8((uint8_t) pal[i+2].as()); // G + tcp[i+3] = gamma8((uint8_t) pal[i+3].as()); // B DEBUG_PRINTF("%d(%d) : %d %d %d\n", i, int(tcp[i]), int(tcp[i+1]), int(tcp[i+2]), int(tcp[i+3])); } } customPalettes.push_back(targetPalette.loadDynamicGradientPalette(tcp)); + } else { + DEBUG_PRINTLN(F("Wrong palette format.")); } } } else { @@ -2387,7 +2406,7 @@ bool WS2812FX::deserializeMap(uint8_t n) { //WLEDMM recreate customMappingTable if more space needed if (Segment::maxWidth * Segment::maxHeight > customMappingTableSize) { - size_t size = max(ledmapMaxSize, size_t(Segment::maxWidth * Segment::maxHeight));//TroyHack + size_t size = max(ledmapMaxSize, size_t(Segment::maxWidth * Segment::maxHeight)); // TroyHacks USER_PRINTF("deserializemap customMappingTable alloc %u from %u\n", size, customMappingTableSize); //if (customMappingTable != nullptr) delete[] customMappingTable; //customMappingTable = new uint16_t[size]; @@ -2399,7 +2418,10 @@ bool WS2812FX::deserializeMap(uint8_t n) { if ((size > 0) && (customMappingTable == nullptr)) { // second try DEBUG_PRINTLN("deserializeMap: trying to get fresh memory block."); customMappingTable = (uint16_t*) calloc(size, sizeof(uint16_t)); - if (customMappingTable == nullptr) DEBUG_PRINTLN("deserializeMap: alloc failed!"); + if (customMappingTable == nullptr) { + DEBUG_PRINTLN("deserializeMap: alloc failed!"); + errorFlag = ERR_LOW_MEM; // WLEDMM raise errorflag + } } if (customMappingTable != nullptr) customMappingTableSize = size; } diff --git a/wled00/const.h b/wled00/const.h index 578e1ab0..14f419c2 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -142,6 +142,7 @@ #define USERMOD_ID_WEATHER 91 //Usermod "usermod_v2_weather.h" #define USERMOD_ID_GAMES 92 //Usermod "usermod_v2_games.h" #define USERMOD_ID_ANIMARTRIX 93 //Usermod "usermod_v2_animartrix.h" +#define USERMOD_ID_AUTOPLAYLIST 94 // Usermod usermod_v2_auto_playlist.h //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot @@ -347,7 +348,9 @@ #define ERR_OVERTEMP 30 // An attached temperature sensor has measured above threshold temperature (not implemented) #define ERR_OVERCURRENT 31 // An attached current sensor has measured a current above the threshold (not implemented) #define ERR_UNDERVOLT 32 // An attached voltmeter has measured a voltage below the threshold (not implemented) -#define ERR_LOW_MEM 33 // low memory (RAM) +#define ERR_LOW_MEM 33 // WLEDMM: low memory (RAM) +#define ERR_LOW_SEG_MEM 34 // WLEDMM: low memory (segment data RAM) +#define ERR_LOW_WS_MEM 35 // WLEDMM: low memory (ws) // Timer mode types #define NL_MODE_SET 0 //After nightlight time elapsed, set to target brightness @@ -494,7 +497,10 @@ #define DEFAULT_LED_COUNT 30 #endif -#define INTERFACE_UPDATE_COOLDOWN 2000 //time in ms to wait between websockets, alexa, and MQTT updates +#define INTERFACE_UPDATE_COOLDOWN 1200 // time in ms to wait between websockets, alexa, and MQTT updates + +#define PIN_RETRY_COOLDOWN 3000 // time in ms after an incorrect attempt PIN and OTA pass will be rejected even if correct +#define PIN_TIMEOUT 900000 // time in ms after which the PIN will be required again, 15 minutes // HW_PIN_SCL & HW_PIN_SDA are used for information in usermods settings page and usermods themselves // which GPIO pins are actually used in a hardware layout (controller board) diff --git a/wled00/data/index.js b/wled00/data/index.js index 3ea998e3..2a4b51ba 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -1968,8 +1968,14 @@ function readState(s,command=false) errstr = "A filesystem error has occured."; break; case 33: - errstr = "Warning: Low Memory (RAM)."; - break; + errstr = "Low Memory (generic RAM)."; + break; + case 34: + errstr = "Low Memory (effect data)."; + break; + case 35: + errstr = "Low Memory (WS data)."; + break; } showToast('Error ' + s.error + ": " + errstr, true); } diff --git a/wled00/e131.cpp b/wled00/e131.cpp index 18c1e3cf..dcb1e314 100644 --- a/wled00/e131.cpp +++ b/wled00/e131.cpp @@ -231,11 +231,16 @@ void handleDMXData(uint16_t uni, uint16_t dmxChannels, uint8_t* e131_data, uint8 if (e131_data[dataOffset+3] != seg.intensity) seg.intensity = e131_data[dataOffset+3]; if (e131_data[dataOffset+4] != seg.palette) seg.setPalette(e131_data[dataOffset+4]); - uint8_t segOption = (uint8_t)floor(e131_data[dataOffset+5]/64.0); - if (segOption == 0 && (seg.mirror || seg.reverse )) {seg.setOption(SEG_OPTION_MIRROR, false); seg.setOption(SEG_OPTION_REVERSED, false);} - if (segOption == 1 && (seg.mirror || !seg.reverse)) {seg.setOption(SEG_OPTION_MIRROR, false); seg.setOption(SEG_OPTION_REVERSED, true);} - if (segOption == 2 && (!seg.mirror || seg.reverse )) {seg.setOption(SEG_OPTION_MIRROR, true); seg.setOption(SEG_OPTION_REVERSED, false);} - if (segOption == 3 && (!seg.mirror || !seg.reverse)) {seg.setOption(SEG_OPTION_MIRROR, true); seg.setOption(SEG_OPTION_REVERSED, true);} + if ((e131_data[dataOffset+5] & 0b00000010) != seg.reverse_y) { seg.setOption(SEG_OPTION_REVERSED_Y, e131_data[dataOffset+5] & 0b00000010); } + if ((e131_data[dataOffset+5] & 0b00000100) != seg.mirror_y) { seg.setOption(SEG_OPTION_MIRROR_Y, e131_data[dataOffset+5] & 0b00000100); } + if ((e131_data[dataOffset+5] & 0b00001000) != seg.transpose) { seg.setOption(SEG_OPTION_TRANSPOSED, e131_data[dataOffset+5] & 0b00001000); } + if ((e131_data[dataOffset+5] & 0b00110000) / 8 != seg.map1D2D) { + seg.map1D2D = (e131_data[dataOffset+5] & 0b00110000) / 8; + } + // To maintain backwards compatibility with prior e1.31 values, reverse is fixed to mask 0x01000000 + if ((e131_data[dataOffset+5] & 0b01000000) != seg.reverse) { seg.setOption(SEG_OPTION_REVERSED, e131_data[dataOffset+5] & 0b01000000); } + // To maintain backwards compatibility with prior e1.31 values, mirror is fixed to mask 0x10000000 + if ((e131_data[dataOffset+5] & 0b10000000) != seg.mirror) { seg.setOption(SEG_OPTION_MIRROR, e131_data[dataOffset+5] & 0b10000000); } uint32_t colors[3]; byte whites[3] = {0,0,0}; diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index dcf4019e..e3473043 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -208,6 +208,7 @@ void _overlayAnalogCountdown(); void _overlayAnalogClock(); //playlist.cpp +void suspendPlaylist(); // WLEDMM support function for auto playlist usermod void shufflePlaylist(); void unloadPlaylist(); int16_t loadPlaylist(JsonObject playlistObject, byte presetId = 0); @@ -370,9 +371,11 @@ void releaseJSONBufferLock(); uint8_t extractModeName(uint8_t mode, const char *src, char *dest, uint8_t maxLen); uint8_t extractModeSlider(uint8_t mode, uint8_t slider, char *dest, uint8_t maxLen, uint8_t *var = nullptr); int16_t extractModeDefaults(uint8_t mode, const char *segVar); +void checkSettingsPIN(const char *pin); uint16_t __attribute__((pure)) crc16(const unsigned char* data_p, size_t length); // WLEDMM: added attribute pure um_data_t* simulateSound(uint8_t simulationId); // WLEDMM enumerateLedmaps(); moved to FX.h +uint8_t get_random_wheel_index(uint8_t pos); CRGB getCRGBForBand(int x, uint8_t *fftResult, int pal); //WLEDMM netmindz ar palette char *cleanUpName(char *in); // to clean up a name that was read from file diff --git a/wled00/improv.cpp b/wled00/improv.cpp index c84406e8..af30065d 100644 --- a/wled00/improv.cpp +++ b/wled00/improv.cpp @@ -210,7 +210,7 @@ void sendImprovInfoResponse() { //Use serverDescription if it has been changed from the default "WLED", else mDNS name bool useMdnsName = (strcmp(serverDescription, "WLED") == 0 && strlen(cmDNS) > 0); char vString[32]; - snprintf_P(vString, sizeof(vString)-1, PSTR("0.14.1-b30.36/%i"),VERSION); + snprintf_P(vString, sizeof(vString)-1, PSTR("0.14.1-b30.37/%i"),VERSION); const char *str[4] = {"WLED", vString, bString, useMdnsName ? cmDNS : serverDescription}; sendImprovRPCResult(ImprovRPCType::Request_Info, 4, str); diff --git a/wled00/led.cpp b/wled00/led.cpp index c6857c4a..70863396 100644 --- a/wled00/led.cpp +++ b/wled00/led.cpp @@ -105,6 +105,7 @@ void stateUpdated(byte callMode) { if (stateChanged) currentPreset = 0; //something changed, so we are no longer in the preset if (callMode != CALL_MODE_NOTIFICATION && callMode != CALL_MODE_NO_NOTIFY) notify(callMode); + if (bri != briOld && nodeBroadcastEnabled) sendSysInfoUDP(); // update on state //set flag to update ws and mqtt interfaceUpdateCallMode = callMode; @@ -139,6 +140,8 @@ void stateUpdated(byte callMode) { jsonTransitionOnce = false; strip.setTransition(transitionDelayTemp); if (transitionDelayTemp == 0) { + jsonTransitionOnce = false; + transitionActive = false; applyFinalBri(); strip.trigger(); return; @@ -161,8 +164,12 @@ void stateUpdated(byte callMode) { void updateInterfaces(uint8_t callMode) { + if (!interfaceUpdateCallMode || millis() - lastInterfaceUpdate < INTERFACE_UPDATE_COOLDOWN) return; + sendDataWs(); lastInterfaceUpdate = millis(); + interfaceUpdateCallMode = 0; //disable + if (callMode == CALL_MODE_WS_SEND) return; #ifndef WLED_DISABLE_ALEXA @@ -172,14 +179,13 @@ void updateInterfaces(uint8_t callMode) } #endif doPublishMqtt = true; - interfaceUpdateCallMode = 0; //disable } void handleTransitions() { //handle still pending interface update - if (interfaceUpdateCallMode && millis() - lastInterfaceUpdate > INTERFACE_UPDATE_COOLDOWN) updateInterfaces(interfaceUpdateCallMode); + updateInterfaces(interfaceUpdateCallMode); #ifndef WLED_DISABLE_MQTT if (doPublishMqtt) publishMqtt(); #endif @@ -190,12 +196,15 @@ void handleTransitions() if (tper >= 1.0f) { strip.setTransitionMode(false); + // restore (global) transition time if not called from UDP notifier or single/temporary transition from JSON (also playlist) + if (jsonTransitionOnce) strip.setTransition(transitionDelay); transitionActive = false; + jsonTransitionOnce = false; tperLast = 0; applyFinalBri(); return; } - if (tper - tperLast < 0.004) return; + if (tper - tperLast < 0.004f) return; tperLast = tper; briT = briOld + ((bri - briOld) * tper); @@ -205,7 +214,7 @@ void handleTransitions() // legacy method, applies values from col, effectCurrent, ... to selected segments -void colorUpdated(byte callMode){ +void colorUpdated(byte callMode) { applyValuesToSelectedSegs(); stateUpdated(callMode); } diff --git a/wled00/playlist.cpp b/wled00/playlist.cpp index 8a6227e1..236cc324 100644 --- a/wled00/playlist.cpp +++ b/wled00/playlist.cpp @@ -41,6 +41,12 @@ void shufflePlaylist() { DEBUG_PRINTLN(F("Playlist shuffle.")); } +// WLEDMM supporting function for auto_playlist usermod +// prevents the active playlist from progressing (until it gets unloaded) +static bool playlistSuspended = false; +void suspendPlaylist() { + playlistSuspended = true; +} void unloadPlaylist() { if (playlistEntries != nullptr) { @@ -49,6 +55,7 @@ void unloadPlaylist() { } currentPlaylist = playlistIndex = -1; playlistLen = playlistEntryDur = playlistOptions = 0; + playlistSuspended = false; // WLEDMM DEBUG_PRINTLN(F("Playlist unloaded.")); } @@ -125,6 +132,11 @@ void handlePlaylist() { // if fileDoc is not null JSON buffer is in use so just quit if (currentPlaylist < 0 || playlistEntries == nullptr || fileDoc != nullptr) return; + if (playlistSuspended) { // WLEDMM + if (millis() - presetCycledTime > (100*playlistEntryDur)) presetCycledTime = millis(); // keep updating timer + return; // but don't progress to next extry, and don't shuffle + } + if (millis() - presetCycledTime > (100*playlistEntryDur)) { presetCycledTime = millis(); if (bri == 0 || nightlightActive) return; diff --git a/wled00/udp.cpp b/wled00/udp.cpp index c2395357..0a2d5332 100644 --- a/wled00/udp.cpp +++ b/wled00/udp.cpp @@ -712,6 +712,7 @@ void sendSysInfoUDP() #else data[38] = NODE_TYPE_ID_UNDEFINED; #endif + if (bri) data[38] |= 0x80U; // add on/off state data[39] = ip[3]; // unit ID == last IP number uint32_t build = VERSION; diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp index 777ca9d3..a6e7ba7a 100644 --- a/wled00/usermods_list.cpp +++ b/wled00/usermods_list.cpp @@ -203,6 +203,9 @@ #ifdef USERMOD_ANIMARTRIX #include "../usermods/usermod_v2_animartrix/usermod_v2_animartrix.h" #endif +#ifdef USERMOD_AUTO_PLAYLIST +#include "../usermods/usermod_v2_auto_playlist/usermod_v2_auto_playlist.h" +#endif void registerUsermods() { @@ -402,4 +405,9 @@ void registerUsermods() usermods.add(new AnimartrixUsermod("Animartrix", false)); #endif +#ifdef USERMOD_AUTO_PLAYLIST + usermods.add(new AutoPlaylistUsermod(false)); +#endif + + } diff --git a/wled00/util.cpp b/wled00/util.cpp index 31f9716c..ceefcc2f 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -204,7 +204,7 @@ bool requestJSONBufferLock(uint8_t module) { unsigned long now = millis(); - while (jsonBufferLock && millis()-now < 1200) delay(1); // wait for fraction for buffer lock + while (jsonBufferLock && millis()-now < 1100) delay(1); // wait for fraction for buffer lock if (jsonBufferLock) { USER_PRINT(F("ERROR: Locking JSON buffer failed! (still locked by ")); @@ -239,9 +239,10 @@ uint8_t extractModeName(uint8_t mode, const char *src, char *dest, uint8_t maxLe { if (src == JSON_mode_names || src == nullptr) { if (mode < strip.getModeCount()) { - char lineBuffer[256]; + char lineBuffer[256] = { '\0' }; //strcpy_P(lineBuffer, (const char*)pgm_read_dword(&(WS2812FX::_modeData[mode]))); - strcpy_P(lineBuffer, strip.getModeData(mode)); + strncpy_P(lineBuffer, strip.getModeData(mode), sizeof(lineBuffer)/sizeof(char)-1); + lineBuffer[sizeof(lineBuffer)/sizeof(char)-1] = '\0'; // terminate string size_t len = strlen(lineBuffer); size_t j = 0; for (; j < maxLen && j < len; j++) { @@ -253,6 +254,12 @@ uint8_t extractModeName(uint8_t mode, const char *src, char *dest, uint8_t maxLe } else return 0; } + if (src == JSON_palette_names && mode > GRADIENT_PALETTE_COUNT) { + snprintf_P(dest, maxLen, PSTR("~ Custom %d~"), 255-mode); + dest[maxLen-1] = '\0'; + return strlen(dest); + } + uint8_t qComma = 0; bool insideQuotes = false; uint8_t printedChars = 0; @@ -363,9 +370,9 @@ uint8_t extractModeSlider(uint8_t mode, uint8_t slider, char *dest, uint8_t maxL int16_t extractModeDefaults(uint8_t mode, const char *segVar) { if (mode < strip.getModeCount()) { - char lineBuffer[128] = ""; - strncpy_P(lineBuffer, strip.getModeData(mode), 127); - lineBuffer[127] = '\0'; // terminate string + char lineBuffer[256] = { '\0' }; + strncpy_P(lineBuffer, strip.getModeData(mode), sizeof(lineBuffer)/sizeof(char)-1); + lineBuffer[sizeof(lineBuffer)/sizeof(char)-1] = '\0'; // terminate string if (lineBuffer[0] != 0) { char* startPtr = strrchr(lineBuffer, ';'); // last ";" in FX data if (!startPtr) return -1; @@ -381,6 +388,16 @@ int16_t extractModeDefaults(uint8_t mode, const char *segVar) } +void checkSettingsPIN(const char* pin) { + if (!pin) return; + if (!correctPIN && millis() - lastEditTime < PIN_RETRY_COOLDOWN) return; // guard against PIN brute force + bool correctBefore = correctPIN; + correctPIN = (strlen(settingsPIN) == 0 || strncmp(settingsPIN, pin, 4) == 0); + if (correctBefore != correctPIN) createEditHandler(correctPIN); + lastEditTime = millis(); +} + + uint16_t crc16(const unsigned char* data_p, size_t length) { uint8_t x; uint16_t crc = 0xFFFF; @@ -401,9 +418,9 @@ uint16_t crc16(const unsigned char* data_p, size_t length) { // (only 2 used as stored in 1 bit in segment options, consider switching to a single global simulation type) typedef enum UM_SoundSimulations { UMS_BeatSin = 0, - UMS_WeWillRockYou - //UMS_10_13, - //UMS_14_3 + UMS_WeWillRockYou, + UMS_10_13, + UMS_14_3 } um_soundSimulations_t; um_data_t* simulateSound(uint8_t simulationId) @@ -491,7 +508,7 @@ um_data_t* simulateSound(uint8_t simulationId) fftResult[i] = 0; } break; - /*case UMS_10_3: + case UMS_10_13: for (int i = 0; i<16; i++) fftResult[i] = inoise8(beatsin8(90 / (i+1), 0, 200)*15 + (ms>>10), ms>>3); volumeSmth = fftResult[8]; @@ -500,11 +517,11 @@ um_data_t* simulateSound(uint8_t simulationId) for (int i = 0; i<16; i++) fftResult[i] = inoise8(beatsin8(120 / (i+1), 10, 30)*10 + (ms>>14), ms>>3); volumeSmth = fftResult[8]; - break;*/ + break; } samplePeak = random8() > 250; - FFT_MajorPeak = 21 + (volumeSmth*volumeSmth) / 8.0f; // WLEDMM 21hz...8200hz + FFT_MajorPeak = 21 + (volumeSmth*volumeSmth) / 8.0f; // walk thru full range of 21hz...8200hz maxVol = 31; // this gets feedback fro UI binNum = 8; // this gets feedback fro UI volumeRaw = volumeSmth; @@ -545,6 +562,20 @@ CRGB getCRGBForBand(int x, uint8_t *fftResult, int pal) { return value; } +/* + * Returns a new, random color wheel index with a minimum distance of 42 from pos. + */ +uint8_t get_random_wheel_index(uint8_t pos) { + uint8_t r = 0, x = 0, y = 0, d = 0; + while (d < 42) { + r = random8(); + x = abs(pos - r); + y = 255 - x; + d = MIN(x, y); + } + return r; +} + // WLEDMM extended "trim string" function to support enumerateLedmaps // The function takes char* as input, and removes all leading and trailing "decorations" like spaces, tabs, line endings, quotes, colons // The conversion is "in place" (destructive). diff --git a/wled00/wled.h b/wled00/wled.h index 5008c1af..23321384 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -8,7 +8,7 @@ */ // version code in format yymmddb (b = daily build) -#define VERSION 2402252 +#define VERSION 2404161 // WLEDMM - you can check for this define in usermods, to only enabled WLEDMM specific code in the "right" fork. Its not defined in AC WLED. #define _MoonModules_WLED_ diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 8751119e..a08e9ea6 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -220,6 +220,8 @@ void initServer() if (verboseResponse) { if (!isConfig) { + lastInterfaceUpdate = millis(); // prevent WS update until cooldown + interfaceUpdateCallMode = CALL_MODE_WS_SEND; // schedule WS update serveJson(request); return; //if JSON contains "v" } else { doSerializeConfig = true; //serializeConfig(); //Save new settings to FS diff --git a/wled00/ws.cpp b/wled00/ws.cpp index 85f75406..351caad4 100644 --- a/wled00/ws.cpp +++ b/wled00/ws.cpp @@ -62,14 +62,15 @@ void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTyp } releaseJSONBufferLock(); // will clean fileDoc - // force broadcast in 500ms after updating client - if (verboseResponse) { - sendDataWs(client); - lastInterfaceUpdate = millis() - (INTERFACE_UPDATE_COOLDOWN -500); - } else { - // we have to send something back otherwise WS connection closes - client->text(F("{\"success\":true}")); - lastInterfaceUpdate = millis() - (INTERFACE_UPDATE_COOLDOWN -500); + if (!interfaceUpdateCallMode) { // individual client response only needed if no WS broadcast soon + if (verboseResponse) { + sendDataWs(client); + } else { + // we have to send something back otherwise WS connection closes + client->text(F("{\"success\":true}")); + } + // force broadcast in 500ms after updating client + //lastInterfaceUpdate = millis() - (INTERFACE_UPDATE_COOLDOWN -500); // ESP8266 does not like this } } } else { @@ -161,6 +162,7 @@ void sendDataWs(AsyncWebSocketClient * client) ws.closeAll(1013); //code 1013 = temporary overload, try again later ws.cleanupClients(0); //disconnect all clients to release memory ws._cleanBuffers(); + errorFlag = ERR_LOW_WS_MEM; return; //out of memory }