From 0ac53d83532b048f12d8475f8680b63eaab3afb2 Mon Sep 17 00:00:00 2001 From: gaaat Date: Wed, 8 May 2024 15:33:51 +0200 Subject: [PATCH 001/142] initial port of MoonModules/WLED/pull/60 and related commit --- platformio.ini | 1 + usermods/audioreactive/audio_reactive.h | 229 ++++++++++++++++++------ 2 files changed, 179 insertions(+), 51 deletions(-) diff --git a/platformio.ini b/platformio.ini index 76c4c92d6..d77650bee 100644 --- a/platformio.ini +++ b/platformio.ini @@ -195,6 +195,7 @@ build_flags = ; decrease code cache size and increase IRAM to fit all pixel functions -D PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48 ;; in case of linker errors like "section `.text1' will not fit in region `iram1_0_seg'" ; -D PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48_SECHEAP_SHARED ;; (experimental) adds some extra heap, but may cause slowdown + -D USERMOD_AUDIOREACTIVE lib_deps = #https://github.com/lorol/LITTLEFS.git diff --git a/usermods/audioreactive/audio_reactive.h b/usermods/audioreactive/audio_reactive.h index 442a651ea..2b43de1b2 100644 --- a/usermods/audioreactive/audio_reactive.h +++ b/usermods/audioreactive/audio_reactive.h @@ -1,6 +1,9 @@ #pragma once #include "wled.h" + +#ifdef ARDUINO_ARCH_ESP32 + #include #include @@ -8,11 +11,9 @@ #error This audio reactive usermod is not compatible with DMX Out. #endif -#ifndef ARDUINO_ARCH_ESP32 - #error This audio reactive usermod does not support the ESP8266. #endif -#if defined(WLED_DEBUG) || defined(SR_DEBUG) +#if defined(ARDUINO_ARCH_ESP32) && (defined(WLED_DEBUG) || defined(SR_DEBUG)) #include #endif @@ -57,6 +58,50 @@ #define MAX_PALETTES 3 +static volatile bool disableSoundProcessing = false; // if true, sound processing (FFT, filters, AGC) will be suspended. "volatile" as its shared between tasks. +static uint8_t audioSyncEnabled = 0; // bit field: bit 0 - send, bit 1 - receive (config value) +static bool udpSyncConnected = false; // UDP connection status -> true if connected to multicast group + +#define NUM_GEQ_CHANNELS 16 // number of frequency channels. Don't change !! + +// audioreactive variables +#ifdef ARDUINO_ARCH_ESP32 +static float micDataReal = 0.0f; // MicIn data with full 24bit resolution - lowest 8bit after decimal point +static float multAgc = 1.0f; // sample * multAgc = sampleAgc. Our AGC multiplier +static float sampleAvg = 0.0f; // Smoothed Average sample - sampleAvg < 1 means "quiet" (simple noise gate) +static float sampleAgc = 0.0f; // Smoothed AGC sample +static uint8_t soundAgc = 0; // Automagic gain control: 0 - none, 1 - normal, 2 - vivid, 3 - lazy (config value) +#endif +//static float volumeSmth = 0.0f; // either sampleAvg or sampleAgc depending on soundAgc; smoothed sample +static float FFT_MajorPeak = 1.0f; // FFT: strongest (peak) frequency +static float FFT_Magnitude = 0.0f; // FFT: volume (magnitude) of peak frequency +static bool samplePeak = false; // Boolean flag for peak - used in effects. Responding routine may reset this flag. Auto-reset after strip.getMinShowDelay() +static bool udpSamplePeak = false; // Boolean flag for peak. Set at the same tiem as samplePeak, but reset by transmitAudioData +static unsigned long timeOfPeak = 0; // time of last sample peak detection. +static uint8_t fftResult[NUM_GEQ_CHANNELS]= {0};// Our calculated freq. channel result table to be used by effects + +// 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 + +// user settable parameters for limitSoundDynamics() +#ifdef UM_AUDIOREACTIVE_DYNAMICS_LIMITER_OFF +static bool limiterOn = false; // bool: enable / disable dynamics limiter +#else +static bool limiterOn = true; +#endif +static uint16_t attackTime = 80; // int: attack time in milliseconds. Default 0.08sec +static uint16_t decayTime = 1400; // int: decay time in milliseconds. Default 1.40sec + +// peak detection +#ifdef ARDUINO_ARCH_ESP32 +static void detectSamplePeak(void); // peak detection function (needs scaled FFT reasults in vReal[]) - no used for 8266 receive-only mode +#endif +static void autoResetPeak(void); // peak auto-reset function +static uint8_t maxVol = 31; // (was 10) Reasonable value for constant volume for 'peak detector', as it won't always trigger (deprecated) +static uint8_t binNum = 8; // Used to select the bin for FFT based beat detection (deprecated) + +#ifdef ARDUINO_ARCH_ESP32 + // use audio source class (ESP32 specific) #include "audio_source.h" constexpr i2s_port_t I2S_PORT = I2S_NUM_0; // I2S port to use (do not change !) @@ -74,18 +119,10 @@ static uint8_t inputLevel = 128; // UI slider value #else uint8_t sampleGain = SR_GAIN; // sample gain (config value) #endif -static uint8_t soundAgc = 1; // Automagic gain control: 0 - none, 1 - normal, 2 - vivid, 3 - lazy (config value) -static uint8_t audioSyncEnabled = 0; // bit field: bit 0 - send, bit 1 - receive (config value) -static bool udpSyncConnected = false; // UDP connection status -> true if connected to multicast group +//static uint8_t soundAgc = 1; // Automagic gain control: 0 - none, 1 - normal, 2 - vivid, 3 - lazy (config value) +//static uint8_t audioSyncEnabled = 0; // bit field: bit 0 - send, bit 1 - receive (config value) +//static bool udpSyncConnected = false; // UDP connection status -> true if connected to multicast group -// user settable parameters for limitSoundDynamics() -#ifdef UM_AUDIOREACTIVE_DYNAMICS_LIMITER_OFF -static bool limiterOn = false; // bool: enable / disable dynamics limiter -#else -static bool limiterOn = true; -#endif -static uint16_t attackTime = 80; // int: attack time in milliseconds. Default 0.08sec -static uint16_t decayTime = 1400; // int: decay time in milliseconds. Default 1.40sec // user settable options for FFTResult scaling static uint8_t FFTScalingMode = 3; // 0 none; 1 optimized logarithmic; 2 optimized linear; 3 optimized square root @@ -109,23 +146,23 @@ const float agcSampleSmooth[AGC_NUM_PRESETS] = { 1/12.f, 1/6.f, 1/16.f}; // // AGC presets end static AudioSource *audioSource = nullptr; -static volatile bool disableSoundProcessing = false; // if true, sound processing (FFT, filters, AGC) will be suspended. "volatile" as its shared between tasks. +//static volatile bool disableSoundProcessing = false; // if true, sound processing (FFT, filters, AGC) will be suspended. "volatile" as its shared between tasks. static bool useBandPassFilter = false; // if true, enables a bandpass filter 80Hz-16Khz to remove noise. Applies before FFT. // audioreactive variables shared with FFT task -static float micDataReal = 0.0f; // MicIn data with full 24bit resolution - lowest 8bit after decimal point -static float multAgc = 1.0f; // sample * multAgc = sampleAgc. Our AGC multiplier -static float sampleAvg = 0.0f; // Smoothed Average sample - sampleAvg < 1 means "quiet" (simple noise gate) -static float sampleAgc = 0.0f; // Smoothed AGC sample +// static float micDataReal = 0.0f; // MicIn data with full 24bit resolution - lowest 8bit after decimal point +// static float multAgc = 1.0f; // sample * multAgc = sampleAgc. Our AGC multiplier +// static float sampleAvg = 0.0f; // Smoothed Average sample - sampleAvg < 1 means "quiet" (simple noise gate) +// static float sampleAgc = 0.0f; // Smoothed AGC sample // peak detection -static bool samplePeak = false; // Boolean flag for peak - used in effects. Responding routine may reset this flag. Auto-reset after strip.getMinShowDelay() -static uint8_t maxVol = 31; // Reasonable value for constant volume for 'peak detector', as it won't always trigger (deprecated) -static uint8_t binNum = 8; // Used to select the bin for FFT based beat detection (deprecated) -static bool udpSamplePeak = false; // Boolean flag for peak. Set at the same time as samplePeak, but reset by transmitAudioData -static unsigned long timeOfPeak = 0; // time of last sample peak detection. -static void detectSamplePeak(void); // peak detection function (needs scaled FFT results in vReal[]) -static void autoResetPeak(void); // peak auto-reset function +// static bool samplePeak = false; // Boolean flag for peak - used in effects. Responding routine may reset this flag. Auto-reset after strip.getMinShowDelay() +// static uint8_t maxVol = 31; // Reasonable value for constant volume for 'peak detector', as it won't always trigger (deprecated) +// static uint8_t binNum = 8; // Used to select the bin for FFT based beat detection (deprecated) +// static bool udpSamplePeak = false; // Boolean flag for peak. Set at the same time as samplePeak, but reset by transmitAudioData +// static unsigned long timeOfPeak = 0; // time of last sample peak detection. +// static void detectSamplePeak(void); // peak detection function (needs scaled FFT results in vReal[]) +// static void autoResetPeak(void); // peak auto-reset function //////////////////// @@ -139,7 +176,7 @@ void FFTcode(void * parameter); // audio processing task: read samples, run static void runMicFilter(uint16_t numSamples, float *sampleBuffer); // pre-filtering of raw samples (band-pass) static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels); // post-processing and post-amp of GEQ channels -#define NUM_GEQ_CHANNELS 16 // number of frequency channels. Don't change !! +//#define NUM_GEQ_CHANNELS 16 // number of frequency channels. Don't change !! static TaskHandle_t FFT_Task = nullptr; @@ -147,9 +184,9 @@ static TaskHandle_t FFT_Task = nullptr; static float fftResultPink[NUM_GEQ_CHANNELS] = { 1.70f, 1.71f, 1.73f, 1.78f, 1.68f, 1.56f, 1.55f, 1.63f, 1.79f, 1.62f, 1.80f, 2.06f, 2.47f, 3.35f, 6.83f, 9.55f }; // globals and FFT Output variables shared with animations -static float FFT_MajorPeak = 1.0f; // FFT: strongest (peak) frequency -static float FFT_Magnitude = 0.0f; // FFT: volume (magnitude) of peak frequency -static uint8_t fftResult[NUM_GEQ_CHANNELS]= {0};// Our calculated freq. channel result table to be used by effects +//static float FFT_MajorPeak = 1.0f; // FFT: strongest (peak) frequency +//static float FFT_Magnitude = 0.0f; // FFT: volume (magnitude) of peak frequency +//static uint8_t fftResult[NUM_GEQ_CHANNELS]= {0};// Our calculated freq. channel result table to be used by effects #if defined(WLED_DEBUG) || defined(SR_DEBUG) static uint64_t fftTime = 0; static uint64_t sampleTime = 0; @@ -521,6 +558,8 @@ static void detectSamplePeak(void) { } } +#endif + static void autoResetPeak(void) { uint16_t MinShowDelay = MAX(50, strip.getMinShowDelay()); // Fixes private class variable compiler error. Unsure if this is the correct way of fixing the root problem. -THATDONFC if (millis() - timeOfPeak > MinShowDelay) { // Auto-reset of samplePeak after a complete frame has passed. @@ -538,6 +577,8 @@ static void autoResetPeak(void) { class AudioReactive : public Usermod { private: +#ifdef ARDUINO_ARCH_ESP32 + #ifndef AUDIOPIN int8_t audioPin = -1; #else @@ -569,6 +610,7 @@ class AudioReactive : public Usermod { #else int8_t mclkPin = MCLK_PIN; #endif +#endif // new "V2" audiosync struct - 40 Bytes struct audioSyncPacket { @@ -612,10 +654,14 @@ class AudioReactive : public Usermod { const uint16_t delayMs = 10; // I don't want to sample too often and overload WLED uint16_t audioSyncPort= 11988;// default port for UDP sound sync + bool updateIsRunning = false; // true during OTA. + +#ifdef ARDUINO_ARCH_ESP32 // used for AGC int last_soundAgc = -1; // used to detect AGC mode change (for resetting AGC internal error buffers) double control_integrated = 0.0; // persistent across calls to agcAvg(); "integrator control" = accumulated error + // variables used by getSample() and agcAvg() int16_t micIn = 0; // Current sample starts with negative values and large values, which is why it's 16 bit signed double sampleMax = 0.0; // Max sample over a few seconds. Needed for AGC controller. @@ -624,6 +670,7 @@ class AudioReactive : public Usermod { float sampleReal = 0.0f; // "sampleRaw" as float, to provide bits that are lost otherwise (before amplification by sampleGain or inputLevel). Needed for AGC. int16_t sampleRaw = 0; // Current sample. Must only be updated ONCE!!! (amplified mic value by sampleGain and inputLevel) int16_t rawSampleAgc = 0; // not smoothed AGC sample +#endif // variables used in effects float volumeSmth = 0.0f; // either sampleAvg or sampleAgc depending on soundAgc; smoothed sample @@ -644,7 +691,9 @@ class AudioReactive : public Usermod { static const char _dynamics[]; static const char _frequency[]; static const char _inputLvl[]; +#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) static const char _analogmic[]; +#endif static const char _digitalmic[]; static const char _addPalettes[]; static const char UDP_SYNC_HEADER[]; @@ -671,11 +720,13 @@ class AudioReactive : public Usermod { //PLOT_PRINT("sampleAgc:"); PLOT_PRINT(sampleAgc); PLOT_PRINT("\t"); //PLOT_PRINT("sampleAvg:"); PLOT_PRINT(sampleAvg); PLOT_PRINT("\t"); //PLOT_PRINT("sampleReal:"); PLOT_PRINT(sampleReal); PLOT_PRINT("\t"); + #ifdef ARDUINO_ARCH_ESP32 //PLOT_PRINT("micIn:"); PLOT_PRINT(micIn); PLOT_PRINT("\t"); //PLOT_PRINT("sample:"); PLOT_PRINT(sample); PLOT_PRINT("\t"); //PLOT_PRINT("sampleMax:"); PLOT_PRINT(sampleMax); PLOT_PRINT("\t"); //PLOT_PRINT("samplePeak:"); PLOT_PRINT((samplePeak!=0) ? 128:0); PLOT_PRINT("\t"); //PLOT_PRINT("multAgc:"); PLOT_PRINT(multAgc, 4); PLOT_PRINT("\t"); + #endif PLOT_PRINTLN(); #endif @@ -731,6 +782,7 @@ class AudioReactive : public Usermod { } // logAudio() +#ifdef ARDUINO_ARCH_ESP32 ////////////////////// // Audio Processing // ////////////////////// @@ -901,6 +953,7 @@ class AudioReactive : public Usermod { sampleAvg = fabsf(sampleAvg); // make sure we have a positive value } // getSample() +#endif /* Limits the dynamics of volumeSmth (= sampleAvg or sampleAgc). * does not affect FFTResult[] or volumeRaw ( = sample or rawSampleAgc) @@ -947,12 +1000,14 @@ class AudioReactive : public Usermod { if (udpSyncConnected) return; // already connected if (!(apActive || interfacesInited)) return; // neither AP nor other connections availeable if (millis() - last_connection_attempt < 15000) return; // only try once in 15 seconds + if (updateIsRunning) return; // if we arrive here, we need a UDP connection but don't have one last_connection_attempt = millis(); connected(); // try to start UDP } +#ifdef ARDUINO_ARCH_ESP32 void transmitAudioData() { if (!udpSyncConnected) return; @@ -983,11 +1038,13 @@ class AudioReactive : public Usermod { return; } // transmitAudioData() +#endif + static bool isValidUdpSyncVersion(const char *header) { - return strncmp_P(header, PSTR(UDP_SYNC_HEADER), 6) == 0; + return strncmp_P(header, UDP_SYNC_HEADER, 6) == 0; } static bool isValidUdpSyncVersion_v1(const char *header) { - return strncmp_P(header, PSTR(UDP_SYNC_HEADER_v1), 6) == 0; + return strncmp_P(header, UDP_SYNC_HEADER_v1, 6) == 0; } void decodeAudioData(int packetSize, uint8_t *fftBuff) { @@ -995,12 +1052,14 @@ class AudioReactive : public Usermod { // update samples for effects volumeSmth = fmaxf(receivedPacket->sampleSmth, 0.0f); volumeRaw = fmaxf(receivedPacket->sampleRaw, 0.0f); +#ifdef ARDUINO_ARCH_ESP32 // update internal samples sampleRaw = volumeRaw; sampleAvg = volumeSmth; rawSampleAgc = volumeRaw; sampleAgc = volumeSmth; multAgc = 1.0f; +#endif // Only change samplePeak IF it's currently false. // If it's true already, then the animation still needs to respond. autoResetPeak(); @@ -1009,7 +1068,7 @@ class AudioReactive : public Usermod { if (samplePeak) timeOfPeak = millis(); //userVar1 = samplePeak; } - //These values are only available on the ESP32 + //These values are only computed by ESP32 for (int i = 0; i < NUM_GEQ_CHANNELS; i++) fftResult[i] = receivedPacket->fftResult[i]; my_magnitude = fmaxf(receivedPacket->FFT_Magnitude, 0.0f); FFT_Magnitude = my_magnitude; @@ -1021,12 +1080,14 @@ class AudioReactive : public Usermod { // update samples for effects volumeSmth = fmaxf(receivedPacket->sampleAgc, 0.0f); volumeRaw = volumeSmth; // V1 format does not have "raw" AGC sample +#ifdef ARDUINO_ARCH_ESP32 // update internal samples sampleRaw = fmaxf(receivedPacket->sampleRaw, 0.0f); sampleAvg = fmaxf(receivedPacket->sampleAvg, 0.0f);; sampleAgc = volumeSmth; rawSampleAgc = volumeRaw; - multAgc = 1.0f; + multAgc = 1.0f; +#endif // Only change samplePeak IF it's currently false. // If it's true already, then the animation still needs to respond. autoResetPeak(); @@ -1112,6 +1173,9 @@ class AudioReactive : public Usermod { um_data->u_type[7] = UMT_BYTE; } + +#ifdef ARDUINO_ARCH_ESP32 + // Reset I2S peripheral for good measure i2s_driver_uninstall(I2S_NUM_0); // E (696) I2S: i2s_driver_uninstall(2006): I2S port 0 has not installed #if !defined(CONFIG_IDF_TARGET_ESP32C3) @@ -1189,10 +1253,12 @@ class AudioReactive : public Usermod { delay(250); // give microphone enough time to initialise if (!audioSource) enabled = false; // audio failed to initialise - if (enabled) onUpdateBegin(false); // create FFT task - if (FFT_Task == nullptr) enabled = false; // FFT task creation failed - if (enabled) disableSoundProcessing = false; // all good - enable audio processing +#endif + if (enabled) onUpdateBegin(false); // create FFT task, and initailize network + +#ifdef ARDUINO_ARCH_ESP32 + if (FFT_Task == nullptr) enabled = false; // FFT task creation failed if((!audioSource) || (!audioSource->isInitialized())) { // audio source failed to initialize. Still stay "enabled", as there might be input arriving via UDP Sound Sync #ifdef WLED_DEBUG DEBUG_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings.")); @@ -1201,7 +1267,8 @@ class AudioReactive : public Usermod { #endif disableSoundProcessing = true; } - +#endif + if (enabled) disableSoundProcessing = false; // all good - enable audio processing if (enabled) connectUDPSoundSync(); if (enabled && addPalettes) createAudioPalettes(); initDone = true; @@ -1220,7 +1287,7 @@ class AudioReactive : public Usermod { } if (audioSyncPort > 0 && (audioSyncEnabled & 0x03)) { - #ifndef ESP8266 + #ifdef ARDUINO_ARCH_ESP32 udpSyncConnected = fftUdp.beginMulticast(IPAddress(239, 0, 0, 1), audioSyncPort); #else udpSyncConnected = fftUdp.beginMulticast(WiFi.localIP(), IPAddress(239, 0, 0, 1), audioSyncPort); @@ -1259,7 +1326,7 @@ class AudioReactive : public Usermod { ||(realtimeMode == REALTIME_MODE_ADALIGHT) ||(realtimeMode == REALTIME_MODE_ARTNET) ) ) // please add other modes here if needed { - #ifdef WLED_DEBUG + #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG) if ((disableSoundProcessing == false) && (audioSyncEnabled == 0)) { // we just switched to "disabled" DEBUG_PRINTLN(F("[AR userLoop] realtime mode active - audio processing suspended.")); DEBUG_PRINTF_P(PSTR(" RealtimeMode = %d; RealtimeOverride = %d\n"), int(realtimeMode), int(realtimeOverride)); @@ -1267,7 +1334,7 @@ class AudioReactive : public Usermod { #endif disableSoundProcessing = true; } else { - #ifdef WLED_DEBUG + #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG) if ((disableSoundProcessing == true) && (audioSyncEnabled == 0) && audioSource->isInitialized()) { // we just switched to "enabled" DEBUG_PRINTLN(F("[AR userLoop] realtime mode ended - audio processing resumed.")); DEBUG_PRINTF_P(PSTR(" RealtimeMode = %d; RealtimeOverride = %d\n"), int(realtimeMode), int(realtimeOverride)); @@ -1279,6 +1346,7 @@ class AudioReactive : public Usermod { if (audioSyncEnabled & 0x02) disableSoundProcessing = true; // make sure everything is disabled IF in audio Receive mode if (audioSyncEnabled & 0x01) disableSoundProcessing = false; // keep running audio IF we're in audio Transmit mode +#ifdef ARDUINO_ARCH_ESP32 if (!audioSource->isInitialized()) disableSoundProcessing = true; // no audio source @@ -1318,6 +1386,7 @@ class AudioReactive : public Usermod { limitSampleDynamics(); } // if (!disableSoundProcessing) +#endif autoResetPeak(); // auto-reset sample peak after strip minShowDelay if (!udpSyncConnected) udpSamplePeak = false; // reset UDP samplePeak while UDP is unconnected @@ -1351,6 +1420,7 @@ class AudioReactive : public Usermod { #endif // Info Page: keep max sample from last 5 seconds +#ifdef ARDUINO_ARCH_ESP32 if ((millis() - sampleMaxTimer) > CYCLE_SAMPLEMAX) { sampleMaxTimer = millis(); maxSample5sec = (0.15f * maxSample5sec) + 0.85f *((soundAgc) ? sampleAgc : sampleAvg); // reset, and start with some smoothing @@ -1358,13 +1428,25 @@ class AudioReactive : public Usermod { } else { if ((sampleAvg >= 1)) maxSample5sec = fmaxf(maxSample5sec, (soundAgc) ? rawSampleAgc : sampleRaw); // follow maximum volume } +#else // similar functionality for 8266 receive only - use VolumeSmth instead of raw sample data + if ((millis() - sampleMaxTimer) > CYCLE_SAMPLEMAX) { + sampleMaxTimer = millis(); + maxSample5sec = (0.15 * maxSample5sec) + 0.85 * volumeSmth; // reset, and start with some smoothing + if (volumeSmth < 1.0f) maxSample5sec = 0; // noise gate + if (maxSample5sec < 0.0f) maxSample5sec = 0; // avoid negative values + } else { + if (volumeSmth >= 1.0f) maxSample5sec = fmaxf(maxSample5sec, volumeRaw); // follow maximum volume + } +#endif +#ifdef ARDUINO_ARCH_ESP32 //UDP Microphone Sync - transmit mode if ((audioSyncEnabled & 0x01) && (millis() - lastTime > 20)) { // Only run the transmit code IF we're in Transmit mode transmitAudioData(); lastTime = millis(); } +#endif fillAudioPalettes(); } @@ -1377,7 +1459,7 @@ class AudioReactive : public Usermod { return true; } - +#ifdef ARDUINO_ARCH_ESP32 void onUpdateBegin(bool init) override { #ifdef WLED_DEBUG @@ -1426,9 +1508,32 @@ class AudioReactive : public Usermod { } micDataReal = 0.0f; // just to be sure if (enabled) disableSoundProcessing = false; + updateIsRunning = init; } +#else // reduced function for 8266 + void onUpdateBegin(bool init) + { + // gracefully suspend audio (if running) + disableSoundProcessing = true; + // reset sound data + volumeRaw = 0; volumeSmth = 0; + for(int i=(init?0:1); i don't process audio + updateIsRunning = init; + } +#endif +#ifdef ARDUINO_ARCH_ESP32 /** * handleButton() can be used to override default button behaviour. Returning true * will prevent button working in a default way. @@ -1446,7 +1551,7 @@ class AudioReactive : public Usermod { return false; } - +#endif //////////////////////////// // Settings and Info Page // //////////////////////////// @@ -1458,7 +1563,9 @@ class AudioReactive : public Usermod { */ void addToJsonInfo(JsonObject& root) override { - char myStringBuffer[16]; // buffer for snprintf() +#ifdef ARDUINO_ARCH_ESP32 + char myStringBuffer[16]; // buffer for snprintf() - not used yet on 8266 +#endif JsonObject user = root["u"]; if (user.isNull()) user = root.createNestedObject("u"); @@ -1476,6 +1583,7 @@ class AudioReactive : public Usermod { infoArr.add(uiDomString); if (enabled) { +#ifdef ARDUINO_ARCH_ESP32 // Input Level Slider if (disableSoundProcessing == false) { // only show slider when audio processing is running if (soundAgc > 0) { @@ -1492,7 +1600,7 @@ class AudioReactive : public Usermod { uiDomString += F(" />
"); // infoArr.add(uiDomString); } - +#endif // The following can be used for troubleshooting user errors and is so not enclosed in #ifdef WLED_DEBUG // current Audio input @@ -1508,6 +1616,11 @@ class AudioReactive : public Usermod { } else { infoArr.add(F(" - no connection")); } +#ifndef ARDUINO_ARCH_ESP32 // substitute for 8266 + } else { + infoArr.add(F("sound sync Off")); + } +#else // ESP32 only } else { // Analog or I2S digital input if (audioSource && (audioSource->isInitialized())) { @@ -1552,7 +1665,7 @@ class AudioReactive : public Usermod { infoArr.add(roundf(multAgc*100.0f) / 100.0f); infoArr.add("x"); } - +#endif // UDP Sound Sync status infoArr = user.createNestedArray(F("UDP Sound Sync")); if (audioSyncEnabled) { @@ -1571,6 +1684,7 @@ class AudioReactive : public Usermod { } #if defined(WLED_DEBUG) || defined(SR_DEBUG) + #ifdef ARDUINO_ARCH_ESP32 infoArr = user.createNestedArray(F("Sampling time")); infoArr.add(float(sampleTime)/100.0f); infoArr.add(" ms"); @@ -1587,6 +1701,7 @@ class AudioReactive : public Usermod { DEBUGSR_PRINTF("AR Sampling time: %5.2f ms\n", float(sampleTime)/100.0f); DEBUGSR_PRINTF("AR FFT time : %5.2f ms\n", float(fftTime)/100.0f); #endif + #endif } } @@ -1625,9 +1740,11 @@ class AudioReactive : public Usermod { if (!prevEnabled && enabled) createAudioPalettes(); } } +#ifdef ARDUINO_ARCH_ESP32 if (usermod[FPSTR(_inputLvl)].is()) { inputLevel = min(255,max(0,usermod[FPSTR(_inputLvl)].as())); } +#endif } if (root.containsKey(F("rmcpal")) && root[F("rmcpal")].as()) { // handle removal of custom palettes from JSON call so we don't break things @@ -1683,6 +1800,7 @@ class AudioReactive : public Usermod { top[FPSTR(_enabled)] = enabled; top[FPSTR(_addPalettes)] = addPalettes; +#ifdef ARDUINO_ARCH_ESP32 #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) JsonObject amic = top.createNestedObject(FPSTR(_analogmic)); amic["pin"] = audioPin; @@ -1701,13 +1819,15 @@ class AudioReactive : public Usermod { cfg[F("gain")] = sampleGain; cfg[F("AGC")] = soundAgc; + JsonObject freqScale = top.createNestedObject(FPSTR(_frequency)); + freqScale[F("scale")] = FFTScalingMode; +#endif + JsonObject dynLim = top.createNestedObject(FPSTR(_dynamics)); dynLim[F("limiter")] = limiterOn; dynLim[F("rise")] = attackTime; dynLim[F("fall")] = decayTime; - JsonObject freqScale = top.createNestedObject(FPSTR(_frequency)); - freqScale[F("scale")] = FFTScalingMode; JsonObject sync = top.createNestedObject("sync"); sync["port"] = audioSyncPort; @@ -1740,6 +1860,7 @@ class AudioReactive : public Usermod { configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled); configComplete &= getJsonValue(top[FPSTR(_addPalettes)], addPalettes); +#ifdef ARDUINO_ARCH_ESP32 #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) configComplete &= getJsonValue(top[FPSTR(_analogmic)]["pin"], audioPin); #else @@ -1763,12 +1884,12 @@ class AudioReactive : public Usermod { configComplete &= getJsonValue(top[FPSTR(_config)][F("gain")], sampleGain); configComplete &= getJsonValue(top[FPSTR(_config)][F("AGC")], soundAgc); + configComplete &= getJsonValue(top[FPSTR(_frequency)][F("scale")], FFTScalingMode); + configComplete &= getJsonValue(top[FPSTR(_dynamics)][F("limiter")], limiterOn); configComplete &= getJsonValue(top[FPSTR(_dynamics)][F("rise")], attackTime); configComplete &= getJsonValue(top[FPSTR(_dynamics)][F("fall")], decayTime); - - configComplete &= getJsonValue(top[FPSTR(_frequency)][F("scale")], FFTScalingMode); - +#endif configComplete &= getJsonValue(top["sync"]["port"], audioSyncPort); configComplete &= getJsonValue(top["sync"]["mode"], audioSyncEnabled); @@ -1783,6 +1904,7 @@ class AudioReactive : public Usermod { void appendConfigData() override { +#ifdef ARDUINO_ARCH_ESP32 oappend(SET_F("dd=addDropdown('AudioReactive','digitalmic:type');")); #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) oappend(SET_F("addOption(dd,'Generic Analog',0);")); @@ -1814,11 +1936,15 @@ class AudioReactive : public Usermod { oappend(SET_F("addOption(dd,'Linear (Amplitude)',2);")); oappend(SET_F("addOption(dd,'Square Root (Energy)',3);")); oappend(SET_F("addOption(dd,'Logarithmic (Loudness)',1);")); +#endif oappend(SET_F("dd=addDropdown('AudioReactive','sync:mode');")); oappend(SET_F("addOption(dd,'Off',0);")); +#ifdef ARDUINO_ARCH_ESP32 oappend(SET_F("addOption(dd,'Send',1);")); +#endif oappend(SET_F("addOption(dd,'Receive',2);")); +#ifdef ARDUINO_ARCH_ESP32 oappend(SET_F("addInfo('AudioReactive:digitalmic:type',1,'requires reboot!');")); // 0 is field type, 1 is actual field oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',0,'sd/data/dout','I2S SD');")); oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',1,'ws/clk/lrck','I2S WS');")); @@ -1828,6 +1954,7 @@ class AudioReactive : public Usermod { #else oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',3,'master clock','I2S MCLK');")); #endif +#endif } From cec72419863a0ce8f19a02aa05bf9073b1302dcd Mon Sep 17 00:00:00 2001 From: gaaat Date: Wed, 8 May 2024 15:42:41 +0200 Subject: [PATCH 002/142] removed commented variables --- usermods/audioreactive/audio_reactive.h | 26 ------------------------- 1 file changed, 26 deletions(-) diff --git a/usermods/audioreactive/audio_reactive.h b/usermods/audioreactive/audio_reactive.h index a0a657d68..a718011fe 100644 --- a/usermods/audioreactive/audio_reactive.h +++ b/usermods/audioreactive/audio_reactive.h @@ -119,10 +119,6 @@ static uint8_t inputLevel = 128; // UI slider value #else uint8_t sampleGain = SR_GAIN; // sample gain (config value) #endif -//static uint8_t soundAgc = 1; // Automagic gain control: 0 - none, 1 - normal, 2 - vivid, 3 - lazy (config value) -//static uint8_t audioSyncEnabled = 0; // bit field: bit 0 - send, bit 1 - receive (config value) -//static bool udpSyncConnected = false; // UDP connection status -> true if connected to multicast group - // user settable options for FFTResult scaling static uint8_t FFTScalingMode = 3; // 0 none; 1 optimized logarithmic; 2 optimized linear; 3 optimized square root @@ -146,25 +142,8 @@ const float agcSampleSmooth[AGC_NUM_PRESETS] = { 1/12.f, 1/6.f, 1/16.f}; // // AGC presets end static AudioSource *audioSource = nullptr; -//static volatile bool disableSoundProcessing = false; // if true, sound processing (FFT, filters, AGC) will be suspended. "volatile" as its shared between tasks. static bool useBandPassFilter = false; // if true, enables a bandpass filter 80Hz-16Khz to remove noise. Applies before FFT. -// audioreactive variables shared with FFT task -// static float micDataReal = 0.0f; // MicIn data with full 24bit resolution - lowest 8bit after decimal point -// static float multAgc = 1.0f; // sample * multAgc = sampleAgc. Our AGC multiplier -// static float sampleAvg = 0.0f; // Smoothed Average sample - sampleAvg < 1 means "quiet" (simple noise gate) -// static float sampleAgc = 0.0f; // Smoothed AGC sample - -// peak detection -// static bool samplePeak = false; // Boolean flag for peak - used in effects. Responding routine may reset this flag. Auto-reset after strip.getMinShowDelay() -// static uint8_t maxVol = 31; // Reasonable value for constant volume for 'peak detector', as it won't always trigger (deprecated) -// static uint8_t binNum = 8; // Used to select the bin for FFT based beat detection (deprecated) -// static bool udpSamplePeak = false; // Boolean flag for peak. Set at the same time as samplePeak, but reset by transmitAudioData -// static unsigned long timeOfPeak = 0; // time of last sample peak detection. -// static void detectSamplePeak(void); // peak detection function (needs scaled FFT results in vReal[]) -// static void autoResetPeak(void); // peak auto-reset function - - //////////////////// // Begin FFT Code // //////////////////// @@ -176,17 +155,12 @@ void FFTcode(void * parameter); // audio processing task: read samples, run static void runMicFilter(uint16_t numSamples, float *sampleBuffer); // pre-filtering of raw samples (band-pass) static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels); // post-processing and post-amp of GEQ channels -//#define NUM_GEQ_CHANNELS 16 // number of frequency channels. Don't change !! - static TaskHandle_t FFT_Task = nullptr; // Table of multiplication factors so that we can even out the frequency response. static float fftResultPink[NUM_GEQ_CHANNELS] = { 1.70f, 1.71f, 1.73f, 1.78f, 1.68f, 1.56f, 1.55f, 1.63f, 1.79f, 1.62f, 1.80f, 2.06f, 2.47f, 3.35f, 6.83f, 9.55f }; // globals and FFT Output variables shared with animations -//static float FFT_MajorPeak = 1.0f; // FFT: strongest (peak) frequency -//static float FFT_Magnitude = 0.0f; // FFT: volume (magnitude) of peak frequency -//static uint8_t fftResult[NUM_GEQ_CHANNELS]= {0};// Our calculated freq. channel result table to be used by effects #if defined(WLED_DEBUG) || defined(SR_DEBUG) static uint64_t fftTime = 0; static uint64_t sampleTime = 0; From f825cab54af025ea55a71246011cdc6e8494d6cf Mon Sep 17 00:00:00 2001 From: Adam Matthews Date: Wed, 26 Jun 2024 20:27:53 +0100 Subject: [PATCH 003/142] Usermod Updated: Internal Temperature V2 # Added high temperature indicator/action... - A configurable preset is activated when the internal temperature raises above a configurable threshold temperature. - When the internal temperature falls back below the threshold, the previously active preset is re-activated. - To prevent frequent toggling between states when the temperature is close to the threshold, the reset threshold is slightly lower than the activation threshold to provide a small buffer. - Reset threshold is automatically calculated to be two degrees lower than whatever the activation threshold is set to. - To prevent the user setting the loop interval too low, a minimum allowable interval has been added. --- .../assets/screenshot-info.png | Bin 0 -> 164382 bytes .../assets/screenshot-settings.png | Bin 0 -> 34073 bytes usermods/Internal_Temperature_v2/readme.md | 49 ++++++++++++--- .../usermod_internal_temperature.h | 57 +++++++++++++++++- 4 files changed, 96 insertions(+), 10 deletions(-) create mode 100644 usermods/Internal_Temperature_v2/assets/screenshot-info.png create mode 100644 usermods/Internal_Temperature_v2/assets/screenshot-settings.png diff --git a/usermods/Internal_Temperature_v2/assets/screenshot-info.png b/usermods/Internal_Temperature_v2/assets/screenshot-info.png new file mode 100644 index 0000000000000000000000000000000000000000..0faf729a485099f84fc35156a36ec2579cc0e29f GIT binary patch literal 164382 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%0A7DKW7(dDp8^T^+Clj+ZCR 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

8b?FLmvs@uXNR7m7yVv|1Hyw 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 literal 0 HcmV?d00001 diff --git a/usermods/Internal_Temperature_v2/assets/screenshot-settings.png b/usermods/Internal_Temperature_v2/assets/screenshot-settings.png new file mode 100644 index 0000000000000000000000000000000000000000..e8d42acc205cd519141faa2e40e8cfaddb7b4735 GIT binary patch literal 34073 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УEZlA{cJSFHvTGBdQyOSIPQ>IvT!`VaW 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 literal 0 HcmV?d00001 diff --git a/usermods/Internal_Temperature_v2/readme.md b/usermods/Internal_Temperature_v2/readme.md index 58a9e1939..53d549e71 100644 --- a/usermods/Internal_Temperature_v2/readme.md +++ b/usermods/Internal_Temperature_v2/readme.md @@ -1,17 +1,48 @@ # Internal Temperature Usermod -This usermod adds the temperature readout to the Info tab and also publishes that over the topic `mcutemp` topic. -## Important -A shown temp of 53,33°C might indicate that the internal temp is not supported. -ESP8266 does not have a internal temp sensor +

+ +

-ESP32S2 seems to crash on reading the sensor -> disabled +

+ +

+ +## Features + -  🌡️  Adds the internal temperature readout of the chip to the `Info` tab + - 🥵 High temperature indicator/action. (Configurable threshold and preset) + - 📣 Publishes the internal temperature over the MQTT topic: `mcutemp` +

+ +## Use Examples +- Warn of excessive/damaging temperatures by the triggering of a 'warning' preset +- Activate a cooling fan (when used with the multi-relay usermod) +

+ +## Compatibility +- A shown temp of 53,33°C might indicate that the internal temp is not supported +- ESP8266 does not have a internal temp sensor -> Disabled (Indicated with a readout of '-1') +- ESP32S2 seems to crash on reading the sensor -> Disabled (Indicated with a readout of '-1') +

## Installation -Add a build flag `-D USERMOD_INTERNAL_TEMPERATURE` to your `platformio.ini` (or `platformio_override.ini`). +- Add a build flag `-D USERMOD_INTERNAL_TEMPERATURE` to your `platformio.ini` (or `platformio_override.ini`). +

+ +## 📝 Change Log + +2024-06-26 + +- Added "high-temperature-indication" feature +- Documentation updated + +2023-09-01 + +* "Internal Temperature" usermod created +

## Authors -Soeren Willrodt [@lost-hope](https://github.com/lost-hope) - -Dimitry Zhemkov [@dima-zhemkov](https://github.com/dima-zhemkov) \ No newline at end of file +- Soeren Willrodt [@lost-hope](https://github.com/lost-hope) +- Dimitry Zhemkov [@dima-zhemkov](https://github.com/dima-zhemkov) +- Adam Matthews [@adamsthws](https://github.com/adamsthws) diff --git a/usermods/Internal_Temperature_v2/usermod_internal_temperature.h b/usermods/Internal_Temperature_v2/usermod_internal_temperature.h index 3989e7668..159752466 100644 --- a/usermods/Internal_Temperature_v2/usermod_internal_temperature.h +++ b/usermods/Internal_Temperature_v2/usermod_internal_temperature.h @@ -6,21 +6,34 @@ class InternalTemperatureUsermod : public Usermod { private: + static const unsigned long minLoopInterval = 1000; // minimum allowable interval (ms) unsigned long loopInterval = 10000; unsigned long lastTime = 0; bool isEnabled = false; float temperature = 0; + int presetToActivate = -1; // Preset to activate when temp goes above threshold (-1 = disabled) + float activationThreshold = 95.0; // Temperature threshold to trigger high-temperature actions + float resetMargin = 2.0; // Margin below the activation threshold (Prevents frequent toggling when close to threshold) + bool isAboveThreshold = false; // Flag to track if the high temperature preset is currently active static const char _name[]; static const char _enabled[]; static const char _loopInterval[]; + static const char _activationThreshold[]; + static const char _presetToActivate[]; // any private methods should go here (non-inline method should be defined out of class) void publishMqtt(const char *state, bool retain = false); // example for publishing MQTT message + // Makes sure the measurement interval can't be set too low + void setSafeLoopInterval(unsigned long newInterval) { + loopInterval = max(newInterval, minLoopInterval); + } + public: void setup() { + setSafeLoopInterval(loopInterval); // Initialize with a safe loop interval } void loop() @@ -32,6 +45,7 @@ public: lastTime = millis(); +// Measure the temperature #ifdef ESP8266 // ESP8266 // does not seem possible temperature = -1; @@ -41,6 +55,30 @@ public: temperature = roundf(temperatureRead() * 10) / 10; #endif + // Check if temperature has gone above the threshold + if (temperature >= activationThreshold) { + // Update the state flag if not already set + if (!isAboveThreshold){ + isAboveThreshold = true; + } + // Activate the 'over-threshold' preset if it's not already active + if (presetToActivate != -1 && currentPreset != presetToActivate) { + saveTemporaryPreset(); // Save the current preset to allow re-activation later + applyPreset(presetToActivate); + } + } + // Check if temperature is back below the threshold + else if (temperature <= (activationThreshold - resetMargin)) { + // Update the state flag if not already set + if (isAboveThreshold){ + isAboveThreshold = false; + } + // Revert back to the original preset + if (currentPreset == presetToActivate){ + applyTemporaryPreset(); // Restore the previously stored active preset + } + } + #ifndef WLED_DISABLE_MQTT if (WLED_MQTT_CONNECTED) { @@ -80,15 +118,30 @@ public: JsonObject top = root.createNestedObject(FPSTR(_name)); top[FPSTR(_enabled)] = isEnabled; top[FPSTR(_loopInterval)] = loopInterval; + top[FPSTR(_activationThreshold)] = activationThreshold; + top[FPSTR(_presetToActivate)] = presetToActivate; } + // Append useful info to the usermod settings gui + void appendConfigData() + { + // Display 'ms' next to the 'Loop Interval' setting + oappend(SET_F("addInfo('Internal Temperature:Loop Interval', 1, 'ms');")); + // Display '°C' next to the 'Activation Threshold' setting + oappend(SET_F("addInfo('Internal Temperature:Activation Threshold', 1, '°C');")); + // Display '-1 = Disabled' next to the 'Preset To Activate' setting + oappend(SET_F("addInfo('Internal Temperature:Preset To Activate', 1, '-1 = disabled');")); + } + bool readFromConfig(JsonObject &root) { JsonObject top = root[FPSTR(_name)]; bool configComplete = !top.isNull(); configComplete &= getJsonValue(top[FPSTR(_enabled)], isEnabled); configComplete &= getJsonValue(top[FPSTR(_loopInterval)], loopInterval); - + setSafeLoopInterval(loopInterval); // Makes sure the loop interval isn't too small. + configComplete &= getJsonValue(top[FPSTR(_presetToActivate)], presetToActivate); + configComplete &= getJsonValue(top[FPSTR(_activationThreshold)], activationThreshold); return configComplete; } @@ -101,6 +154,8 @@ public: const char InternalTemperatureUsermod::_name[] PROGMEM = "Internal Temperature"; const char InternalTemperatureUsermod::_enabled[] PROGMEM = "Enabled"; const char InternalTemperatureUsermod::_loopInterval[] PROGMEM = "Loop Interval"; +const char InternalTemperatureUsermod::_activationThreshold[] PROGMEM = "Activation Threshold"; +const char InternalTemperatureUsermod::_presetToActivate[] PROGMEM = "Preset To Activate"; void InternalTemperatureUsermod::publishMqtt(const char *state, bool retain) { From bc4a6138b1c36600106fd4b78b159b71b064b8c3 Mon Sep 17 00:00:00 2001 From: Adam Matthews Date: Thu, 27 Jun 2024 17:33:37 +0100 Subject: [PATCH 004/142] Fixes to feature update for Internal Temperature usermod Applied various fixes as advised by @blazoncek Thankyou for the advice! - Updated float: 95.0 > 95.0f - Updated type: const > constexpr - Comments clarified - Preset setting: `-1` > `0` --- usermods/Internal_Temperature_v2/readme.md | 9 ++------- .../usermod_internal_temperature.h | 17 ++++++++--------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/usermods/Internal_Temperature_v2/readme.md b/usermods/Internal_Temperature_v2/readme.md index 53d549e71..f8472a6a3 100644 --- a/usermods/Internal_Temperature_v2/readme.md +++ b/usermods/Internal_Temperature_v2/readme.md @@ -1,13 +1,8 @@ # Internal Temperature Usermod +![Screenshot of WLED info page](assets/screenshot-info.png =700x) -

- -

- -

- -

+![Screenshot of WLED usermod settings page](assets/screenshot-settings.png =700x) ## Features -  🌡️  Adds the internal temperature readout of the chip to the `Info` tab diff --git a/usermods/Internal_Temperature_v2/usermod_internal_temperature.h b/usermods/Internal_Temperature_v2/usermod_internal_temperature.h index 159752466..3fa9c4bb1 100644 --- a/usermods/Internal_Temperature_v2/usermod_internal_temperature.h +++ b/usermods/Internal_Temperature_v2/usermod_internal_temperature.h @@ -6,13 +6,13 @@ class InternalTemperatureUsermod : public Usermod { private: - static const unsigned long minLoopInterval = 1000; // minimum allowable interval (ms) + static constexpr unsigned long minLoopInterval = 1000; // minimum allowable interval (ms) unsigned long loopInterval = 10000; unsigned long lastTime = 0; bool isEnabled = false; float temperature = 0; - int presetToActivate = -1; // Preset to activate when temp goes above threshold (-1 = disabled) - float activationThreshold = 95.0; // Temperature threshold to trigger high-temperature actions + int presetToActivate = 0; // Preset to activate when temp goes above threshold (0 = disabled) + float activationThreshold = 95.0f; // Temperature threshold to trigger high-temperature actions float resetMargin = 2.0; // Margin below the activation threshold (Prevents frequent toggling when close to threshold) bool isAboveThreshold = false; // Flag to track if the high temperature preset is currently active @@ -33,7 +33,6 @@ private: public: void setup() { - setSafeLoopInterval(loopInterval); // Initialize with a safe loop interval } void loop() @@ -62,8 +61,8 @@ public: isAboveThreshold = true; } // Activate the 'over-threshold' preset if it's not already active - if (presetToActivate != -1 && currentPreset != presetToActivate) { - saveTemporaryPreset(); // Save the current preset to allow re-activation later + if (presetToActivate != 0 && currentPreset != presetToActivate) { + saveTemporaryPreset(); // Save current state to a temporary preset to allow re-activation later applyPreset(presetToActivate); } } @@ -75,7 +74,7 @@ public: } // Revert back to the original preset if (currentPreset == presetToActivate){ - applyTemporaryPreset(); // Restore the previously stored active preset + applyTemporaryPreset(); // Restore the previous state which was stored to the temporary preset } } @@ -129,8 +128,8 @@ public: oappend(SET_F("addInfo('Internal Temperature:Loop Interval', 1, 'ms');")); // Display '°C' next to the 'Activation Threshold' setting oappend(SET_F("addInfo('Internal Temperature:Activation Threshold', 1, '°C');")); - // Display '-1 = Disabled' next to the 'Preset To Activate' setting - oappend(SET_F("addInfo('Internal Temperature:Preset To Activate', 1, '-1 = disabled');")); + // Display '0 = Disabled' next to the 'Preset To Activate' setting + oappend(SET_F("addInfo('Internal Temperature:Preset To Activate', 1, '0 = unused');")); } bool readFromConfig(JsonObject &root) From 9877e899e0a851a91f7caa4b2980f9eadcecb42e Mon Sep 17 00:00:00 2001 From: Adam Matthews Date: Thu, 27 Jun 2024 17:52:41 +0100 Subject: [PATCH 005/142] Fixed images in readme Github flavoured markdown didn't work as expected. --- .../assets/screenshot-info.png | Bin 164382 -> 0 bytes .../assets/screenshot-settings.png | Bin 34073 -> 0 bytes .../assets/screenshot_info.png | Bin 0 -> 135724 bytes .../assets/screenshot_settings.png | Bin 0 -> 60867 bytes usermods/Internal_Temperature_v2/readme.md | 4 ++-- 5 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 usermods/Internal_Temperature_v2/assets/screenshot-info.png delete mode 100644 usermods/Internal_Temperature_v2/assets/screenshot-settings.png create mode 100644 usermods/Internal_Temperature_v2/assets/screenshot_info.png create mode 100644 usermods/Internal_Temperature_v2/assets/screenshot_settings.png diff --git a/usermods/Internal_Temperature_v2/assets/screenshot-info.png b/usermods/Internal_Temperature_v2/assets/screenshot-info.png deleted file mode 100644 index 0faf729a485099f84fc35156a36ec2579cc0e29f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 164382 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
hn(0(yoj;>;%RyX{P2pjEp==}%p>;lp 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%0A7DKW7(dDp8^T^+Clj+ZCR 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

8b?FLmvs@uXNR7m7yVv|1Hyw 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 diff --git a/usermods/Internal_Temperature_v2/assets/screenshot-settings.png b/usermods/Internal_Temperature_v2/assets/screenshot-settings.png deleted file mode 100644 index e8d42acc205cd519141faa2e40e8cfaddb7b4735..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34073 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УEZlA{cJSFHvTGBdQyOSIPQ>IvT!`VaW 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 diff --git a/usermods/Internal_Temperature_v2/assets/screenshot_info.png b/usermods/Internal_Temperature_v2/assets/screenshot_info.png new file mode 100644 index 0000000000000000000000000000000000000000..7990f2e8ff09fe3708b8a7f48f0dd1c183f7de1c GIT binary patch literal 135724 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 literal 0 HcmV?d00001 diff --git a/usermods/Internal_Temperature_v2/assets/screenshot_settings.png b/usermods/Internal_Temperature_v2/assets/screenshot_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..d97dc2beab6bdfea89d8a0f131f33f1c30454bba GIT binary patch literal 60867 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[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 Date: Thu, 27 Jun 2024 21:30:05 -0400 Subject: [PATCH 006/142] Fix PWM crashes on ESP8266 Vendor in the ESP8266 Arduino core PWM library, with a fix for a nasty NMI crash bug. Sometimes the NMI return instruction seems to fail, resulting in an infinite loop as the PC gets stuck. Work around this by backing up and restoring the PC if needed. --- .../src/core_esp8266_waveform_pwm.cpp | 711 ++++++++++++++++++ platformio.ini | 1 + wled00/wled.cpp | 6 + 3 files changed, 718 insertions(+) create mode 100644 lib/ESP8266PWM/src/core_esp8266_waveform_pwm.cpp diff --git a/lib/ESP8266PWM/src/core_esp8266_waveform_pwm.cpp b/lib/ESP8266PWM/src/core_esp8266_waveform_pwm.cpp new file mode 100644 index 000000000..c76ee174d --- /dev/null +++ b/lib/ESP8266PWM/src/core_esp8266_waveform_pwm.cpp @@ -0,0 +1,711 @@ +/* esp8266_waveform imported from platform source code + Modified for WLED to work around a fault in the NMI handling, + which can result in the system locking up and hard WDT crashes. +*/ + +/* + esp8266_waveform - General purpose waveform generation and control, + supporting outputs on all pins in parallel. + + Copyright (c) 2018 Earle F. Philhower, III. All rights reserved. + + The core idea is to have a programmable waveform generator with a unique + high and low period (defined in microseconds or CPU clock cycles). TIMER1 + is set to 1-shot mode and is always loaded with the time until the next + edge of any live waveforms. + + Up to one waveform generator per pin supported. + + Each waveform generator is synchronized to the ESP clock cycle counter, not + the timer. This allows for removing interrupt jitter and delay as the + counter always increments once per 80MHz clock. Changes to a waveform are + contiguous and only take effect on the next waveform transition, + allowing for smooth transitions. + + This replaces older tone(), analogWrite(), and the Servo classes. + + Everywhere in the code where "cycles" is used, it means ESP.getCycleCount() + clock cycle count, or an interval measured in CPU clock cycles, but not + TIMER1 cycles (which may be 2 CPU clock cycles @ 160MHz). + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +*/ + + +#include +#include +#include "ets_sys.h" +#include "core_esp8266_waveform.h" +#include "user_interface.h" + +extern "C" { + +// Linker magic +void usePWMFixedNMI() {}; + +// Maximum delay between IRQs +#define MAXIRQUS (10000) + +// Waveform generator can create tones, PWM, and servos +typedef struct { + uint32_t nextServiceCycle; // ESP cycle timer when a transition required + uint32_t expiryCycle; // For time-limited waveform, the cycle when this waveform must stop + uint32_t timeHighCycles; // Actual running waveform period (adjusted using desiredCycles) + uint32_t timeLowCycles; // + uint32_t desiredHighCycles; // Ideal waveform period to drive the error signal + uint32_t desiredLowCycles; // + uint32_t lastEdge; // Cycle when this generator last changed +} Waveform; + +class WVFState { +public: + Waveform waveform[17]; // State of all possible pins + uint32_t waveformState = 0; // Is the pin high or low, updated in NMI so no access outside the NMI code + uint32_t waveformEnabled = 0; // Is it actively running, updated in NMI so no access outside the NMI code + + // Enable lock-free by only allowing updates to waveformState and waveformEnabled from IRQ service routine + uint32_t waveformToEnable = 0; // Message to the NMI handler to start a waveform on a inactive pin + uint32_t waveformToDisable = 0; // Message to the NMI handler to disable a pin from waveform generation + + uint32_t waveformToChange = 0; // Mask of pin to change. One bit set in main app, cleared when effected in the NMI + uint32_t waveformNewHigh = 0; + uint32_t waveformNewLow = 0; + + uint32_t (*timer1CB)() = NULL; + + // Optimize the NMI inner loop by keeping track of the min and max GPIO that we + // are generating. In the common case (1 PWM) these may be the same pin and + // we can avoid looking at the other pins. + uint16_t startPin = 0; + uint16_t endPin = 0; +}; +static WVFState wvfState; + + +// Ensure everything is read/written to RAM +#define MEMBARRIER() { __asm__ volatile("" ::: "memory"); } + +// Non-speed critical bits +#pragma GCC optimize ("Os") + +// Interrupt on/off control +static IRAM_ATTR void timer1Interrupt(); +static bool timerRunning = false; + +static __attribute__((noinline)) void initTimer() { + if (!timerRunning) { + timer1_disable(); + ETS_FRC_TIMER1_INTR_ATTACH(NULL, NULL); + ETS_FRC_TIMER1_NMI_INTR_ATTACH(timer1Interrupt); + timer1_enable(TIM_DIV1, TIM_EDGE, TIM_SINGLE); + timerRunning = true; + timer1_write(microsecondsToClockCycles(10)); + } +} + +static IRAM_ATTR void forceTimerInterrupt() { + if (T1L > microsecondsToClockCycles(10)) { + T1L = microsecondsToClockCycles(10); + } +} + +// PWM implementation using special purpose state machine +// +// Keep an ordered list of pins with the delta in cycles between each +// element, with a terminal entry making up the remainder of the PWM +// period. With this method sum(all deltas) == PWM period clock cycles. +// +// At t=0 set all pins high and set the timeout for the 1st edge. +// On interrupt, if we're at the last element reset to t=0 state +// Otherwise, clear that pin down and set delay for next element +// and so forth. + +constexpr int maxPWMs = 8; + +// PWM machine state +typedef struct PWMState { + uint32_t mask; // Bitmask of active pins + uint32_t cnt; // How many entries + uint32_t idx; // Where the state machine is along the list + uint8_t pin[maxPWMs + 1]; + uint32_t delta[maxPWMs + 1]; + uint32_t nextServiceCycle; // Clock cycle for next step + struct PWMState *pwmUpdate; // Set by main code, cleared by ISR +} PWMState; + +static PWMState pwmState; +static uint32_t _pwmFreq = 1000; +static uint32_t _pwmPeriod = microsecondsToClockCycles(1000000UL) / _pwmFreq; + + +// If there are no more scheduled activities, shut down Timer 1. +// Otherwise, do nothing. +static IRAM_ATTR void disableIdleTimer() { + if (timerRunning && !wvfState.waveformEnabled && !pwmState.cnt && !wvfState.timer1CB) { + ETS_FRC_TIMER1_NMI_INTR_ATTACH(NULL); + timer1_disable(); + timer1_isr_init(); + timerRunning = false; + } +} + +// Notify the NMI that a new PWM state is available through the mailbox. +// Wait for mailbox to be emptied (either busy or delay() as needed) +static IRAM_ATTR void _notifyPWM(PWMState *p, bool idle) { + p->pwmUpdate = nullptr; + pwmState.pwmUpdate = p; + MEMBARRIER(); + forceTimerInterrupt(); + while (pwmState.pwmUpdate) { + if (idle) { + esp_yield(); + } + MEMBARRIER(); + } +} + +static void _addPWMtoList(PWMState &p, int pin, uint32_t val, uint32_t range); + + +// Called when analogWriteFreq() changed to update the PWM total period +//extern void _setPWMFreq_weak(uint32_t freq) __attribute__((weak)); +void _setPWMFreq_weak(uint32_t freq) { + _pwmFreq = freq; + + // Convert frequency into clock cycles + uint32_t cc = microsecondsToClockCycles(1000000UL) / freq; + + // Simple static adjustment to bring period closer to requested due to overhead + // Empirically determined as a constant PWM delay and a function of the number of PWMs +#if F_CPU == 80000000 + cc -= ((microsecondsToClockCycles(pwmState.cnt) * 13) >> 4) + 110; +#else + cc -= ((microsecondsToClockCycles(pwmState.cnt) * 10) >> 4) + 75; +#endif + + if (cc == _pwmPeriod) { + return; // No change + } + + _pwmPeriod = cc; + + if (pwmState.cnt) { + PWMState p; // The working copy since we can't edit the one in use + p.mask = 0; + p.cnt = 0; + for (uint32_t i = 0; i < pwmState.cnt; i++) { + auto pin = pwmState.pin[i]; + _addPWMtoList(p, pin, wvfState.waveform[pin].desiredHighCycles, wvfState.waveform[pin].desiredLowCycles); + } + // Update and wait for mailbox to be emptied + initTimer(); + _notifyPWM(&p, true); + disableIdleTimer(); + } +} +/* +static void _setPWMFreq_bound(uint32_t freq) __attribute__((weakref("_setPWMFreq_weak"))); +void _setPWMFreq(uint32_t freq) { + _setPWMFreq_bound(freq); +} +*/ + +// Helper routine to remove an entry from the state machine +// and clean up any marked-off entries +static void _cleanAndRemovePWM(PWMState *p, int pin) { + uint32_t leftover = 0; + uint32_t in, out; + for (in = 0, out = 0; in < p->cnt; in++) { + if ((p->pin[in] != pin) && (p->mask & (1<pin[in]))) { + p->pin[out] = p->pin[in]; + p->delta[out] = p->delta[in] + leftover; + leftover = 0; + out++; + } else { + leftover += p->delta[in]; + p->mask &= ~(1<pin[in]); + } + } + p->cnt = out; + // Final pin is never used: p->pin[out] = 0xff; + p->delta[out] = p->delta[in] + leftover; +} + + +// Disable PWM on a specific pin (i.e. when a digitalWrite or analogWrite(0%/100%)) +//extern bool _stopPWM_weak(uint8_t pin) __attribute__((weak)); +IRAM_ATTR bool _stopPWM_weak(uint8_t pin) { + if (!((1<= _pwmPeriod) { + cc = _pwmPeriod - 1; + } + + if (p.cnt == 0) { + // Starting up from scratch, special case 1st element and PWM period + p.pin[0] = pin; + p.delta[0] = cc; + // Final pin is never used: p.pin[1] = 0xff; + p.delta[1] = _pwmPeriod - cc; + } else { + uint32_t ttl = 0; + uint32_t i; + // Skip along until we're at the spot to insert + for (i=0; (i <= p.cnt) && (ttl + p.delta[i] < cc); i++) { + ttl += p.delta[i]; + } + // Shift everything out by one to make space for new edge + for (int32_t j = p.cnt; j >= (int)i; j--) { + p.pin[j + 1] = p.pin[j]; + p.delta[j + 1] = p.delta[j]; + } + int off = cc - ttl; // The delta from the last edge to the one we're inserting + p.pin[i] = pin; + p.delta[i] = off; // Add the delta to this new pin + p.delta[i + 1] -= off; // And subtract it from the follower to keep sum(deltas) constant + } + p.cnt++; + p.mask |= 1<= maxPWMs) { + return false; // No space left + } + + // Sanity check for all-on/off + uint32_t cc = (_pwmPeriod * val) / range; + if ((cc == 0) || (cc >= _pwmPeriod)) { + digitalWrite(pin, cc ? HIGH : LOW); + return true; + } + + _addPWMtoList(p, pin, val, range); + + // Set mailbox and wait for ISR to copy it over + initTimer(); + _notifyPWM(&p, true); + disableIdleTimer(); + + // Potentially recalculate the PWM period if we've added another pin + _setPWMFreq(_pwmFreq); + + return true; +} +/* +static bool _setPWM_bound(int pin, uint32_t val, uint32_t range) __attribute__((weakref("_setPWM_weak"))); +bool _setPWM(int pin, uint32_t val, uint32_t range) { + return _setPWM_bound(pin, val, range); +} +*/ + +// Start up a waveform on a pin, or change the current one. Will change to the new +// waveform smoothly on next low->high transition. For immediate change, stopWaveform() +// first, then it will immediately begin. +//extern int startWaveformClockCycles_weak(uint8_t pin, uint32_t timeHighCycles, uint32_t timeLowCycles, uint32_t runTimeCycles, int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) __attribute__((weak)); +int startWaveformClockCycles_weak(uint8_t pin, uint32_t timeHighCycles, uint32_t timeLowCycles, uint32_t runTimeCycles, + int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) { + (void) alignPhase; + (void) phaseOffsetUS; + (void) autoPwm; + + if ((pin > 16) || isFlashInterfacePin(pin) || (timeHighCycles == 0)) { + return false; + } + Waveform *wave = &wvfState.waveform[pin]; + wave->expiryCycle = runTimeCycles ? ESP.getCycleCount() + runTimeCycles : 0; + if (runTimeCycles && !wave->expiryCycle) { + wave->expiryCycle = 1; // expiryCycle==0 means no timeout, so avoid setting it + } + + _stopPWM(pin); // Make sure there's no PWM live here + + uint32_t mask = 1<timeHighCycles = timeHighCycles; + wave->desiredHighCycles = timeHighCycles; + wave->timeLowCycles = timeLowCycles; + wave->desiredLowCycles = timeLowCycles; + wave->lastEdge = 0; + wave->nextServiceCycle = ESP.getCycleCount() + microsecondsToClockCycles(1); + wvfState.waveformToEnable |= mask; + MEMBARRIER(); + initTimer(); + forceTimerInterrupt(); + while (wvfState.waveformToEnable) { + esp_yield(); // Wait for waveform to update + MEMBARRIER(); + } + } + + return true; +} +/* +static int startWaveformClockCycles_bound(uint8_t pin, uint32_t timeHighCycles, uint32_t timeLowCycles, uint32_t runTimeCycles, int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) __attribute__((weakref("startWaveformClockCycles_weak"))); +int startWaveformClockCycles(uint8_t pin, uint32_t timeHighCycles, uint32_t timeLowCycles, uint32_t runTimeCycles, int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) { + return startWaveformClockCycles_bound(pin, timeHighCycles, timeLowCycles, runTimeCycles, alignPhase, phaseOffsetUS, autoPwm); +} + + +// This version falls-thru to the proper startWaveformClockCycles call and is invariant across waveform generators +int startWaveform(uint8_t pin, uint32_t timeHighUS, uint32_t timeLowUS, uint32_t runTimeUS, + int8_t alignPhase, uint32_t phaseOffsetUS, bool autoPwm) { + return startWaveformClockCycles_bound(pin, + microsecondsToClockCycles(timeHighUS), microsecondsToClockCycles(timeLowUS), + microsecondsToClockCycles(runTimeUS), alignPhase, microsecondsToClockCycles(phaseOffsetUS), autoPwm); +} +*/ + +// Set a callback. Pass in NULL to stop it +//extern void setTimer1Callback_weak(uint32_t (*fn)()) __attribute__((weak)); +void setTimer1Callback_weak(uint32_t (*fn)()) { + wvfState.timer1CB = fn; + if (fn) { + initTimer(); + forceTimerInterrupt(); + } + disableIdleTimer(); +} +/* +static void setTimer1Callback_bound(uint32_t (*fn)()) __attribute__((weakref("setTimer1Callback_weak"))); +void setTimer1Callback(uint32_t (*fn)()) { + setTimer1Callback_bound(fn); +} +*/ + +// Stops a waveform on a pin +//extern int stopWaveform_weak(uint8_t pin) __attribute__((weak)); +IRAM_ATTR int stopWaveform_weak(uint8_t pin) { + // Can't possibly need to stop anything if there is no timer active + if (!timerRunning) { + return false; + } + // If user sends in a pin >16 but <32, this will always point to a 0 bit + // If they send >=32, then the shift will result in 0 and it will also return false + uint32_t mask = 1<= (uintptr_t) &_UserExceptionVector_1)) { + // Address is good; save backup + epc3_backup = epc3; + eps3_backup = eps3; + } else { + // Address is inside the NMI handler -- restore from backup + __asm__ __volatile__("wsr %0,epc3; wsr %1,eps3"::"a"(epc3_backup),"a"(eps3_backup)); + } +} + + +// The SDK and hardware take some time to actually get to our NMI code, so +// decrement the next IRQ's timer value by a bit so we can actually catch the +// real CPU cycle counter we want for the waveforms. + +// The SDK also sometimes is running at a different speed the the Arduino core +// so the ESP cycle counter is actually running at a variable speed. +// adjust(x) takes care of adjusting a delta clock cycle amount accordingly. +#if F_CPU == 80000000 + #define DELTAIRQ (microsecondsToClockCycles(9)/4) + #define adjust(x) ((x) << (turbo ? 1 : 0)) +#else + #define DELTAIRQ (microsecondsToClockCycles(9)/8) + #define adjust(x) ((x) >> 0) +#endif + +// When the time to the next edge is greater than this, RTI and set another IRQ to minimize CPU usage +#define MINIRQTIME microsecondsToClockCycles(6) + +static IRAM_ATTR void timer1Interrupt() { + nmiCrashWorkaround(); + + // Flag if the core is at 160 MHz, for use by adjust() + bool turbo = (*(uint32_t*)0x3FF00014) & 1 ? true : false; + + uint32_t nextEventCycle = GetCycleCountIRQ() + microsecondsToClockCycles(MAXIRQUS); + uint32_t timeoutCycle = GetCycleCountIRQ() + microsecondsToClockCycles(14); + + if (wvfState.waveformToEnable || wvfState.waveformToDisable) { + // Handle enable/disable requests from main app + wvfState.waveformEnabled = (wvfState.waveformEnabled & ~wvfState.waveformToDisable) | wvfState.waveformToEnable; // Set the requested waveforms on/off + wvfState.waveformState &= ~wvfState.waveformToEnable; // And clear the state of any just started + wvfState.waveformToEnable = 0; + wvfState.waveformToDisable = 0; + // No mem barrier. Globals must be written to RAM on ISR exit. + // Find the first GPIO being generated by checking GCC's find-first-set (returns 1 + the bit of the first 1 in an int32_t) + wvfState.startPin = __builtin_ffs(wvfState.waveformEnabled) - 1; + // Find the last bit by subtracting off GCC's count-leading-zeros (no offset in this one) + wvfState.endPin = 32 - __builtin_clz(wvfState.waveformEnabled); + } else if (!pwmState.cnt && pwmState.pwmUpdate) { + // Start up the PWM generator by copying from the mailbox + pwmState.cnt = 1; + pwmState.idx = 1; // Ensure copy this cycle, cause it to start at t=0 + pwmState.nextServiceCycle = GetCycleCountIRQ(); // Do it this loop! + // No need for mem barrier here. Global must be written by IRQ exit + } + + bool done = false; + if (wvfState.waveformEnabled || pwmState.cnt) { + do { + nextEventCycle = GetCycleCountIRQ() + microsecondsToClockCycles(MAXIRQUS); + + // PWM state machine implementation + if (pwmState.cnt) { + int32_t cyclesToGo; + do { + cyclesToGo = pwmState.nextServiceCycle - GetCycleCountIRQ(); + if (cyclesToGo < 0) { + if (pwmState.idx == pwmState.cnt) { // Start of pulses, possibly copy new + if (pwmState.pwmUpdate) { + // Do the memory copy from temp to global and clear mailbox + pwmState = *(PWMState*)pwmState.pwmUpdate; + } + GPOS = pwmState.mask; // Set all active pins high + if (pwmState.mask & (1<<16)) { + GP16O = 1; + } + pwmState.idx = 0; + } else { + do { + // Drop the pin at this edge + if (pwmState.mask & (1<expiryCycle) { + int32_t expiryToGo = wave->expiryCycle - now; + if (expiryToGo < 0) { + // Done, remove! + if (i == 16) { + GP16O = 0; + } + GPOC = mask; + wvfState.waveformEnabled &= ~mask; + continue; + } + } + + // Check for toggles + int32_t cyclesToGo = wave->nextServiceCycle - now; + if (cyclesToGo < 0) { + uint32_t nextEdgeCycles; + uint32_t desired = 0; + uint32_t *timeToUpdate; + wvfState.waveformState ^= mask; + if (wvfState.waveformState & mask) { + if (i == 16) { + GP16O = 1; + } + GPOS = mask; + + if (wvfState.waveformToChange & mask) { + // Copy over next full-cycle timings + wave->timeHighCycles = wvfState.waveformNewHigh; + wave->desiredHighCycles = wvfState.waveformNewHigh; + wave->timeLowCycles = wvfState.waveformNewLow; + wave->desiredLowCycles = wvfState.waveformNewLow; + wave->lastEdge = 0; + wvfState.waveformToChange = 0; + } + if (wave->lastEdge) { + desired = wave->desiredLowCycles; + timeToUpdate = &wave->timeLowCycles; + } + nextEdgeCycles = wave->timeHighCycles; + } else { + if (i == 16) { + GP16O = 0; + } + GPOC = mask; + desired = wave->desiredHighCycles; + timeToUpdate = &wave->timeHighCycles; + nextEdgeCycles = wave->timeLowCycles; + } + if (desired) { + desired = adjust(desired); + int32_t err = desired - (now - wave->lastEdge); + if (abs(err) < desired) { // If we've lost > the entire phase, ignore this error signal + err /= 2; + *timeToUpdate += err; + } + } + nextEdgeCycles = adjust(nextEdgeCycles); + wave->nextServiceCycle = now + nextEdgeCycles; + wave->lastEdge = now; + } + nextEventCycle = earliest(nextEventCycle, wave->nextServiceCycle); + } + + // Exit the loop if we've hit the fixed runtime limit or the next event is known to be after that timeout would occur + uint32_t now = GetCycleCountIRQ(); + int32_t cycleDeltaNextEvent = nextEventCycle - now; + int32_t cyclesLeftTimeout = timeoutCycle - now; + done = (cycleDeltaNextEvent > MINIRQTIME) || (cyclesLeftTimeout < 0); + } while (!done); + } // if (wvfState.waveformEnabled) + + if (wvfState.timer1CB) { + nextEventCycle = earliest(nextEventCycle, GetCycleCountIRQ() + wvfState.timer1CB()); + } + + int32_t nextEventCycles = nextEventCycle - GetCycleCountIRQ(); + + if (nextEventCycles < MINIRQTIME) { + nextEventCycles = MINIRQTIME; + } + nextEventCycles -= DELTAIRQ; + + // Do it here instead of global function to save time and because we know it's edge-IRQ + T1L = nextEventCycles >> (turbo ? 1 : 0); +} + +}; diff --git a/platformio.ini b/platformio.ini index 34cea4944..5ebc43f55 100644 --- a/platformio.ini +++ b/platformio.ini @@ -202,6 +202,7 @@ lib_deps = #https://github.com/lorol/LITTLEFS.git ESPAsyncTCP @ 1.2.2 ESPAsyncUDP + ESP8266PWM ${env.lib_deps} [esp32] diff --git a/wled00/wled.cpp b/wled00/wled.cpp index ec83d4583..41a2d6ea8 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -8,6 +8,8 @@ #include "soc/rtc_cntl_reg.h" #endif +extern "C" void usePWMFixedNMI(); + /* * Main WLED class implementation. Mostly initialization and connection logic */ @@ -408,6 +410,10 @@ void WLED::setup() DEBUG_PRINTF_P(PSTR("TX power: %d/%d\n"), WiFi.getTxPower(), txPower); #endif +#ifdef ESP8266 + usePWMFixedNMI(); // link the NMI fix +#endif + #if defined(WLED_DEBUG) && !defined(WLED_DEBUG_HOST) pinManager.allocatePin(hardwareTX, true, PinOwner::DebugOut); // TX (GPIO1 on ESP32) reserved for debug output #endif From a1dfdced3178b9b6434e6412f427083e375a365c Mon Sep 17 00:00:00 2001 From: Adam Matthews Date: Fri, 28 Jun 2024 15:47:54 +0100 Subject: [PATCH 007/142] Fixes to feature update for Internal Temperature usermod - Updated all doubles to floating-point literals by adding explicit `f` suffix - Removed all remaining html from readme markdown documentation. --- usermods/Internal_Temperature_v2/readme.md | 13 +++++++------ .../usermod_internal_temperature.h | 6 +++--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/usermods/Internal_Temperature_v2/readme.md b/usermods/Internal_Temperature_v2/readme.md index 8829691ee..d574f3abf 100644 --- a/usermods/Internal_Temperature_v2/readme.md +++ b/usermods/Internal_Temperature_v2/readme.md @@ -4,26 +4,27 @@ ![Screenshot of WLED usermod settings page](assets/screenshot_settings.png) + ## Features - -  🌡️  Adds the internal temperature readout of the chip to the `Info` tab + - 🌡️ Adds the internal temperature readout of the chip to the `Info` tab - 🥵 High temperature indicator/action. (Configurable threshold and preset) - 📣 Publishes the internal temperature over the MQTT topic: `mcutemp` -

+ ## Use Examples - Warn of excessive/damaging temperatures by the triggering of a 'warning' preset - Activate a cooling fan (when used with the multi-relay usermod) -

+ ## Compatibility - A shown temp of 53,33°C might indicate that the internal temp is not supported - ESP8266 does not have a internal temp sensor -> Disabled (Indicated with a readout of '-1') - ESP32S2 seems to crash on reading the sensor -> Disabled (Indicated with a readout of '-1') -

+ ## Installation - Add a build flag `-D USERMOD_INTERNAL_TEMPERATURE` to your `platformio.ini` (or `platformio_override.ini`). -

+ ## 📝 Change Log @@ -35,7 +36,7 @@ 2023-09-01 * "Internal Temperature" usermod created -

+ ## Authors - Soeren Willrodt [@lost-hope](https://github.com/lost-hope) diff --git a/usermods/Internal_Temperature_v2/usermod_internal_temperature.h b/usermods/Internal_Temperature_v2/usermod_internal_temperature.h index 3fa9c4bb1..09f4ba250 100644 --- a/usermods/Internal_Temperature_v2/usermod_internal_temperature.h +++ b/usermods/Internal_Temperature_v2/usermod_internal_temperature.h @@ -10,10 +10,10 @@ private: unsigned long loopInterval = 10000; unsigned long lastTime = 0; bool isEnabled = false; - float temperature = 0; - int presetToActivate = 0; // Preset to activate when temp goes above threshold (0 = disabled) + float temperature = 0.0f; + uint8_t presetToActivate = 0; // Preset to activate when temp goes above threshold (0 = disabled) float activationThreshold = 95.0f; // Temperature threshold to trigger high-temperature actions - float resetMargin = 2.0; // Margin below the activation threshold (Prevents frequent toggling when close to threshold) + float resetMargin = 2.0f; // Margin below the activation threshold (Prevents frequent toggling when close to threshold) bool isAboveThreshold = false; // Flag to track if the high temperature preset is currently active static const char _name[]; From 3815516022f495e73ae9f9316704f05231e47b3d Mon Sep 17 00:00:00 2001 From: Adam Matthews Date: Fri, 28 Jun 2024 16:12:56 +0100 Subject: [PATCH 008/142] Fixes to feature update for Internal Temperature usermod Simplified the code by removing an unnecessary function definition and instead using direct assignment in the place where the function was previously called. --- .../Internal_Temperature_v2/usermod_internal_temperature.h | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/usermods/Internal_Temperature_v2/usermod_internal_temperature.h b/usermods/Internal_Temperature_v2/usermod_internal_temperature.h index 09f4ba250..198c4e4eb 100644 --- a/usermods/Internal_Temperature_v2/usermod_internal_temperature.h +++ b/usermods/Internal_Temperature_v2/usermod_internal_temperature.h @@ -25,11 +25,6 @@ private: // any private methods should go here (non-inline method should be defined out of class) void publishMqtt(const char *state, bool retain = false); // example for publishing MQTT message - // Makes sure the measurement interval can't be set too low - void setSafeLoopInterval(unsigned long newInterval) { - loopInterval = max(newInterval, minLoopInterval); - } - public: void setup() { @@ -138,7 +133,7 @@ public: bool configComplete = !top.isNull(); configComplete &= getJsonValue(top[FPSTR(_enabled)], isEnabled); configComplete &= getJsonValue(top[FPSTR(_loopInterval)], loopInterval); - setSafeLoopInterval(loopInterval); // Makes sure the loop interval isn't too small. + loopInterval = max(loopInterval, minLoopInterval); // Makes sure the loop interval isn't too small. configComplete &= getJsonValue(top[FPSTR(_presetToActivate)], presetToActivate); configComplete &= getJsonValue(top[FPSTR(_activationThreshold)], activationThreshold); return configComplete; From 78e7312adfb0e6d7a2f4fa9361a9428da5687a4c Mon Sep 17 00:00:00 2001 From: Adam Matthews Date: Mon, 1 Jul 2024 00:22:52 +0100 Subject: [PATCH 009/142] Update usermod internal temperature Enabled the storing the currently active preset or playlist for it to be restored later --- .../usermod_internal_temperature.h | 43 ++++++++++++++++--- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/usermods/Internal_Temperature_v2/usermod_internal_temperature.h b/usermods/Internal_Temperature_v2/usermod_internal_temperature.h index 198c4e4eb..2236bfeab 100644 --- a/usermods/Internal_Temperature_v2/usermod_internal_temperature.h +++ b/usermods/Internal_Temperature_v2/usermod_internal_temperature.h @@ -11,6 +11,8 @@ private: unsigned long lastTime = 0; bool isEnabled = false; float temperature = 0.0f; + uint8_t previousPlaylist = 0; // Stores the playlist that was active before high-temperature activation + uint8_t previousPreset = 0; // Stores the preset that was active before high-temperature activation uint8_t presetToActivate = 0; // Preset to activate when temp goes above threshold (0 = disabled) float activationThreshold = 95.0f; // Temperature threshold to trigger high-temperature actions float resetMargin = 2.0f; // Margin below the activation threshold (Prevents frequent toggling when close to threshold) @@ -49,15 +51,26 @@ public: temperature = roundf(temperatureRead() * 10) / 10; #endif - // Check if temperature has gone above the threshold + // Check if temperature has exceeded the activation threshold if (temperature >= activationThreshold) { // Update the state flag if not already set - if (!isAboveThreshold){ + if (!isAboveThreshold) { isAboveThreshold = true; } - // Activate the 'over-threshold' preset if it's not already active + // Check if a 'high temperature' preset is configured and it's not already active if (presetToActivate != 0 && currentPreset != presetToActivate) { - saveTemporaryPreset(); // Save current state to a temporary preset to allow re-activation later + // If a playlist is active, store it for reactivation later + if (currentPlaylist > 0) { + previousPlaylist = currentPlaylist; + } + // If a preset is active, store it for reactivation later + else if (currentPreset > 0) { + previousPreset = currentPreset; + // If no playlist or preset is active, save current state for reactivation later + } else { + saveTemporaryPreset(); + } + // Activate the 'high temperature' preset applyPreset(presetToActivate); } } @@ -67,9 +80,25 @@ public: if (isAboveThreshold){ isAboveThreshold = false; } - // Revert back to the original preset - if (currentPreset == presetToActivate){ - applyTemporaryPreset(); // Restore the previous state which was stored to the temporary preset + // Check if the 'high temperature' preset is active + if (currentPreset == presetToActivate) { + // Check if a previous playlist was stored + if (previousPlaylist > 0) { + // Reactivate the stored playlist + applyPreset(previousPlaylist); + // Clear the stored playlist + previousPlaylist = 0; + } + // Check if a previous preset was stored + else if (previousPreset > 0) { + // Reactivate the stored preset + applyPreset(previousPreset); + // Clear the stored preset + previousPreset = 0; + // If no previous playlist or preset was stored, revert to the stored state + } else { + applyTemporaryPreset(); + } } } From 5874b78349d8fa604dfbce9da3c6f1c962c197e6 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Fri, 5 Jul 2024 21:22:05 +0200 Subject: [PATCH 010/142] Bugfixes - FX: Breathe, Meteor - IR: use Segment - UI: palette search, LED settings --- wled00/FX.cpp | 42 ++++++++++++++++++++++------------- wled00/data/index.js | 2 +- wled00/data/settings_leds.htm | 4 ++-- wled00/ir.cpp | 4 ++-- 4 files changed, 31 insertions(+), 21 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index c50b9383f..078b87101 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -336,14 +336,14 @@ static const char _data_FX_MODE_DYNAMIC_SMOOTH[] PROGMEM = "Dynamic Smooth@!,!;; */ uint16_t mode_breath(void) { unsigned var = 0; - unsigned counter = (strip.now * ((SEGMENT.speed >> 3) +10)); - counter = ((counter >> 2) + (counter >> 4)) & 0xFFFFU; //0-16384 + 0-2048 + unsigned counter = (strip.now * ((SEGMENT.speed >> 3) +10)) & 0xFFFFU; + counter = (counter >> 2) + (counter >> 4); //0-16384 + 0-2048 if (counter < 16384) { if (counter > 8192) counter = 8192 - (counter - 8192); var = sin16(counter) / 103; //close to parabolic in range 0-8192, max val. 23170 } - uint8_t lum = 30 + var; + unsigned lum = 30 + var; for (int i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), lum)); } @@ -358,7 +358,7 @@ static const char _data_FX_MODE_BREATH[] PROGMEM = "Breathe@!;!,!;!;01"; */ uint16_t mode_fade(void) { unsigned counter = (strip.now * ((SEGMENT.speed >> 3) +10)); - uint8_t lum = triwave16(counter) >> 8; + unsigned lum = triwave16(counter) >> 8; for (int i = 0; i < SEGLEN; i++) { SEGMENT.setPixelColor(i, color_blend(SEGCOLOR(1), SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0), lum)); @@ -1779,8 +1779,8 @@ typedef struct Oscillator { / Oscillating bars of color, updated with standard framerate */ uint16_t mode_oscillate(void) { - unsigned numOscillators = 3; - unsigned dataSize = sizeof(oscillator) * numOscillators; + constexpr unsigned numOscillators = 3; + constexpr unsigned dataSize = sizeof(oscillator) * numOscillators; if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed @@ -2347,31 +2347,41 @@ uint16_t mode_meteor() { unsigned counter = strip.now * ((SEGMENT.speed >> 2) +8); uint16_t in = counter * SEGLEN >> 16; - const int max = SEGMENT.palette==5 || !SEGMENT.check1 ? 240 : 255; + const int max = SEGMENT.palette==5 ? 239 : 255; // "* Colors only" palette blends end with start // fade all leds to colors[1] in LEDs one step for (int i = 0; i < SEGLEN; i++) { if (random8() <= 255 - SEGMENT.intensity) { - byte meteorTrailDecay = 162 + random8(92); + int meteorTrailDecay = 128 + random8(127); trail[i] = scale8(trail[i], meteorTrailDecay); - uint32_t col = SEGMENT.check1 ? SEGMENT.color_from_palette(i, true, false, 0, trail[i]) : SEGMENT.color_from_palette(trail[i], false, true, 255); + int index = trail[i]; + int idx = 255; + int bri = SEGMENT.palette==35 || SEGMENT.palette==36 ? 255 : trail[i]; + if (!SEGMENT.check1) { + idx = 0; + index = map(i,0,SEGLEN,0,max); + bri = trail[i]; + } + uint32_t col = SEGMENT.color_from_palette(index, false, false, idx, bri); // full brightness for Fire SEGMENT.setPixelColor(i, col); } } // draw meteor - for (unsigned j = 0; j < meteorSize; j++) { - unsigned index = in + j; - if (index >= SEGLEN) { - index -= SEGLEN; + for (int j = 0; j < meteorSize; j++) { + int index = (in + j) % SEGLEN; + int idx = 255; + int i = trail[index] = max; + if (!SEGMENT.check1) { + i = map(index,0,SEGLEN,0,max); + idx = 0; } - trail[index] = max; - uint32_t col = SEGMENT.check1 ? SEGMENT.color_from_palette(index, true, false, 0, trail[index]) : SEGMENT.color_from_palette(trail[index], false, true, 255); + uint32_t col = SEGMENT.color_from_palette(i, false, false, idx, 255); // full brightness SEGMENT.setPixelColor(index, col); } return FRAMETIME; } -static const char _data_FX_MODE_METEOR[] PROGMEM = "Meteor@!,Trail,,,,Gradient;;!;1"; +static const char _data_FX_MODE_METEOR[] PROGMEM = "Meteor@!,Trail,,,,Gradient;!;!;1"; // smooth meteor effect diff --git a/wled00/data/index.js b/wled00/data/index.js index adddc38fb..4e5a1eb0e 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -2837,7 +2837,7 @@ function search(field, listId = null) { if (gId("filters").querySelectorAll("input[type=checkbox]:checked").length) return; // filter list items but leave (Default & Solid) always visible - const listItems = gId("fxlist").querySelectorAll('.lstI'); + const listItems = gId(listId).querySelectorAll('.lstI'); listItems.forEach((listItem,i)=>{ if (listId!=='pcont' && i===0) return; const listItemName = listItem.querySelector('.lstIname').innerText.toUpperCase(); diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 164dc5a77..2ce5be148 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -290,7 +290,7 @@ // do we have a led count field if (nm=="LC") { let c = parseInt(LC.value,10); //get LED count - if (c > 300 && i < 8) maxB = oMaxB - max(maxD-7,0); //TODO: hard limit for buses when using ESP32 parallel I2S + if (c > 300 && i < 8) maxB = oMaxB - Math.max(maxD-7,0); //TODO: hard limit for buses when using ESP32 parallel I2S if (!customStarts || !startsDirty[n]) gId("ls"+n).value=sLC; //update start value gId("ls"+n).disabled = !customStarts; //enable/disable field editing if (c) { @@ -864,7 +864,7 @@ Swap:
- Default brightness: (0-255)

+ Default brightness: (1-255)

Apply preset at boot (0 uses defaults)

Use Gamma correction for color: (strongly recommended)
diff --git a/wled00/ir.cpp b/wled00/ir.cpp index e475198f6..9e1974366 100644 --- a/wled00/ir.cpp +++ b/wled00/ir.cpp @@ -84,11 +84,11 @@ static void changeEffect(uint8_t fx) for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (!seg.isActive() || !seg.isSelected()) continue; - strip.setMode(i, fx); + seg.setMode(fx); } setValuesFromFirstSelectedSeg(); } else { - strip.setMode(strip.getMainSegmentId(), fx); + strip.getSegment(strip.getMainSegmentId()).setMode(fx); setValuesFromMainSeg(); } stateChanged = true; From 887254f5da3209b3c3533aa8347dfef5868294c0 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Sun, 7 Jul 2024 14:18:51 +0200 Subject: [PATCH 011/142] Bugfixes - LED memory calculation (not UI) - potential fix for #4040 - compiler warning in FX --- wled00/FX.cpp | 2 +- wled00/bus_manager.cpp | 12 ++++-- wled00/bus_manager.h | 1 + wled00/button.cpp | 2 +- wled00/cfg.cpp | 87 +++++++++++++++++++++++------------------- wled00/set.cpp | 14 ++++--- wled00/wled.cpp | 34 ++++++++--------- wled00/wled.h | 2 +- 8 files changed, 87 insertions(+), 67 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 078b87101..cce3098e1 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -2367,7 +2367,7 @@ uint16_t mode_meteor() { } // draw meteor - for (int j = 0; j < meteorSize; j++) { + for (unsigned j = 0; j < meteorSize; j++) { int index = (in + j) % SEGLEN; int idx = 255; int i = trail[index] = max; diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 1f70bb257..46e909edd 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -670,9 +670,9 @@ void BusNetwork::cleanup() { uint32_t BusManager::memUsage(BusConfig &bc) { if (bc.type == TYPE_ONOFF || IS_PWM(bc.type)) return 5; - uint16_t len = bc.count + bc.skipAmount; - uint16_t channels = Bus::getNumberOfChannels(bc.type); - uint16_t multiplier = 1; + unsigned len = bc.count + bc.skipAmount; + unsigned channels = Bus::getNumberOfChannels(bc.type); + unsigned multiplier = 1; if (IS_DIGITAL(bc.type)) { // digital types if (IS_16BIT(bc.type)) len *= 2; // 16-bit LEDs #ifdef ESP8266 @@ -686,6 +686,12 @@ uint32_t BusManager::memUsage(BusConfig &bc) { return (len * multiplier + bc.doubleBuffer * (bc.count + bc.skipAmount)) * channels; } +uint32_t BusManager::memUsage(unsigned maxChannels, unsigned maxCount, unsigned minBuses) { + //ESP32 RMT uses double buffer, parallel I2S uses 8x buffer (3 times) + unsigned multiplier = PolyBus::isParallelI2S1Output() ? 3 : 2; + return (maxChannels * maxCount * minBuses * multiplier); +} + int BusManager::add(BusConfig &bc) { if (getNumBusses() - getNumVirtualBusses() >= WLED_MAX_BUSSES) return -1; if (IS_VIRTUAL(bc.type)) { diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index e2c4239d8..e1bacd555 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -356,6 +356,7 @@ class BusManager { //utility to get the approx. memory usage of a given BusConfig static uint32_t memUsage(BusConfig &bc); + static uint32_t memUsage(unsigned channels, unsigned count, unsigned buses = 1); static uint16_t currentMilliamps(void) { return _milliAmpsUsed; } static uint16_t ablMilliampsMax(void) { return _milliAmpsMax; } diff --git a/wled00/button.cpp b/wled00/button.cpp index 519cd08e1..8594c868b 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -178,7 +178,7 @@ void handleAnalog(uint8_t b) #ifdef ESP8266 rawReading = analogRead(A0) << 2; // convert 10bit read to 12bit #else - if ((btnPin[b] < 0) || (digitalPinToAnalogChannel(btnPin[b]) < 0)) return; // pin must support analog ADC - newer esp32 frameworks throw lots of warnings otherwise + if ((btnPin[b] < 0) /*|| (digitalPinToAnalogChannel(btnPin[b]) < 0)*/) return; // pin must support analog ADC - newer esp32 frameworks throw lots of warnings otherwise rawReading = analogRead(btnPin[b]); // collect at full 12bit resolution #endif yield(); // keep WiFi task running - analog read may take several millis on ESP8266 diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 57ea556b3..1fc3f2425 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -160,44 +160,46 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { DEBUG_PRINTF_P(PSTR("Heap before buses: %d\n"), ESP.getFreeHeap()); int s = 0; // bus iterator if (fromFS) BusManager::removeAll(); // can't safely manipulate busses directly in network callback - uint32_t mem = 0; - bool busesChanged = false; + unsigned mem = 0; + // determine if it is sensible to use parallel I2S outputs on ESP32 (i.e. more than 5 outputs = 1 I2S + 4 RMT) bool useParallel = false; #if defined(ARDUINO_ARCH_ESP32) && !defined(ARDUINO_ARCH_ESP32S2) && !defined(ARDUINO_ARCH_ESP32S3) && !defined(ARDUINO_ARCH_ESP32C3) unsigned digitalCount = 0; - unsigned maxLeds = 0; - int oldType = 0; - int j = 0; + unsigned maxLedsOnBus = 0; + unsigned maxChannels = 0; for (JsonObject elm : ins) { unsigned type = elm["type"] | TYPE_WS2812_RGB; - unsigned len = elm["len"] | 30; - if (IS_DIGITAL(type) && !IS_2PIN(type)) digitalCount++; - if (len > maxLeds) maxLeds = len; - // we need to have all LEDs of the same type for parallel - if (j++ < 8 && oldType > 0 && oldType != type) oldType = -1; - else if (oldType == 0) oldType = type; + unsigned len = elm["len"] | DEFAULT_LED_COUNT; + if (!IS_DIGITAL(type)) continue; + if (!IS_2PIN(type)) { + digitalCount++; + unsigned channels = Bus::getNumberOfChannels(type); + if (len > maxLedsOnBus) maxLedsOnBus = len; + if (channels > maxChannels) maxChannels = channels; + } } - DEBUG_PRINTF_P(PSTR("Maximum LEDs on a bus: %u\nDigital buses: %u\nDifferent types: %d\n"), maxLeds, digitalCount, (int)(oldType == -1)); + DEBUG_PRINTF_P(PSTR("Maximum LEDs on a bus: %u\nDigital buses: %u\n"), maxLedsOnBus, digitalCount); // we may remove 300 LEDs per bus limit when NeoPixelBus is updated beyond 2.9.0 - if (/*oldType != -1 && */maxLeds <= 300 && digitalCount > 5) { + if (maxLedsOnBus <= 300 && digitalCount > 5) { + DEBUG_PRINTLN(F("Switching to parallel I2S.")); useParallel = true; BusManager::useParallelOutput(); - DEBUG_PRINTF_P(PSTR("Switching to parallel I2S with max. %d LEDs per ouptut.\n"), maxLeds); + mem = BusManager::memUsage(maxChannels, maxLedsOnBus, 8); // use alternate memory calculation } #endif + for (JsonObject elm : ins) { if (s >= WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES) break; uint8_t pins[5] = {255, 255, 255, 255, 255}; JsonArray pinArr = elm["pin"]; if (pinArr.size() == 0) continue; - pins[0] = pinArr[0]; + //pins[0] = pinArr[0]; unsigned i = 0; for (int p : pinArr) { pins[i++] = p; if (i>4) break; } - uint16_t length = elm["len"] | 1; uint8_t colorOrder = (int)elm[F("order")]; // contains white channel swap option in upper nibble uint8_t skipFirst = elm[F("skip")]; @@ -208,7 +210,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { bool refresh = elm["ref"] | false; uint16_t freqkHz = elm[F("freq")] | 0; // will be in kHz for DotStar and Hz for PWM uint8_t AWmode = elm[F("rgbwm")] | RGBW_MODE_MANUAL_ONLY; - uint8_t maPerLed = elm[F("ledma")] | 55; + uint8_t maPerLed = elm[F("ledma")] | LED_MILLIAMPS_DEFAULT; uint16_t maMax = elm[F("maxpwr")] | (ablMilliampsMax * length) / total; // rough (incorrect?) per strip ABL calculation when no config exists // To disable brightness limiter we either set output max current to 0 or single LED current to 0 (we choose output max current) if (IS_PWM(ledType) || IS_ONOFF(ledType) || IS_VIRTUAL(ledType)) { // analog and virtual @@ -219,26 +221,21 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { if (fromFS) { BusConfig bc = BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, useGlobalLedBuffer, maPerLed, maMax); if (useParallel && s < 8) { - // we are using parallel I2S and memUsage() will include x8 allocation into account - if (s == 0) - mem = BusManager::memUsage(bc); // includes x8 memory allocation for parallel I2S - else - if (BusManager::memUsage(bc) > mem) - mem = BusManager::memUsage(bc); // if we have unequal LED count use the largest + // if for some unexplained reason the above pre-calculation was wrong, update + unsigned memT = BusManager::memUsage(bc); // includes x8 memory allocation for parallel I2S + if (memT > mem) mem = memT; // if we have unequal LED count use the largest } else mem += BusManager::memUsage(bc); // includes global buffer if (mem <= MAX_LED_MEMORY) if (BusManager::add(bc) == -1) break; // finalization will be done in WLED::beginStrip() } else { if (busConfigs[s] != nullptr) delete busConfigs[s]; busConfigs[s] = new BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst, AWmode, freqkHz, useGlobalLedBuffer, maPerLed, maMax); - busesChanged = true; + doInitBusses = true; // finalization done in beginStrip() } s++; } DEBUG_PRINTF_P(PSTR("LED buffer size: %uB\n"), mem); DEBUG_PRINTF_P(PSTR("Heap after buses: %d\n"), ESP.getFreeHeap()); - doInitBusses = busesChanged; - // finalization done in beginStrip() } if (hw_led["rev"]) BusManager::getBus(0)->setReversed(true); //set 0.11 global reversed setting for first bus @@ -275,22 +272,34 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { btnPin[s] = pin; #ifdef ARDUINO_ARCH_ESP32 // ESP32 only: check that analog button pin is a valid ADC gpio - if (((buttonType[s] == BTN_TYPE_ANALOG) || (buttonType[s] == BTN_TYPE_ANALOG_INVERTED)) && (digitalPinToAnalogChannel(btnPin[s]) < 0)) - { - // not an ADC analog pin - DEBUG_PRINT(F("PIN ALLOC error: GPIO")); DEBUG_PRINT(btnPin[s]); - DEBUG_PRINT(F("for analog button #")); DEBUG_PRINT(s); - DEBUG_PRINTLN(F(" is not an analog pin!")); - btnPin[s] = -1; - pinManager.deallocatePin(pin,PinOwner::Button); + if ((buttonType[s] == BTN_TYPE_ANALOG) || (buttonType[s] == BTN_TYPE_ANALOG_INVERTED)) { + if (digitalPinToAnalogChannel(btnPin[s]) < 0) { + // not an ADC analog pin + DEBUG_PRINT(F("PIN ALLOC error: GPIO")); DEBUG_PRINT(btnPin[s]); + DEBUG_PRINT(F("for analog button #")); DEBUG_PRINT(s); + DEBUG_PRINTLN(F(" is not an analog pin!")); + btnPin[s] = -1; + pinManager.deallocatePin(pin,PinOwner::Button); + } else { + analogReadResolution(12); // see #4040 + } } - //if touch pin, enable the touch interrupt on ESP32 S2 & S3 - #ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a fucntion to check touch state but need to attach an interrupt to do so - if ((buttonType[s] == BTN_TYPE_TOUCH || buttonType[s] == BTN_TYPE_TOUCH_SWITCH)) + else if ((buttonType[s] == BTN_TYPE_TOUCH || buttonType[s] == BTN_TYPE_TOUCH_SWITCH)) { - touchAttachInterrupt(btnPin[s], touchButtonISR, 256 + (touchThreshold << 4)); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000) + if (digitalPinToTouchChannel(btnPin[s]) < 0) { + // not a touch pin + DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not an touch pin!\n"), btnPin[s], s); + btnPin[s] = -1; + pinManager.deallocatePin(pin,PinOwner::Button); + } + //if touch pin, enable the touch interrupt on ESP32 S2 & S3 + #ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a function to check touch state but need to attach an interrupt to do so + else + { + touchAttachInterrupt(btnPin[s], touchButtonISR, 256 + (touchThreshold << 4)); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000) + } + #endif } - #endif else #endif { diff --git a/wled00/set.cpp b/wled00/set.cpp index c4422e05a..651e5b2e0 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -266,12 +266,16 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) buttonType[i] = request->arg(be).toInt(); #ifdef ARDUINO_ARCH_ESP32 // ESP32 only: check that button pin is a valid gpio - if (((buttonType[i] == BTN_TYPE_ANALOG) || (buttonType[i] == BTN_TYPE_ANALOG_INVERTED)) && (digitalPinToAnalogChannel(btnPin[i]) < 0)) + if ((buttonType[i] == BTN_TYPE_ANALOG) || (buttonType[i] == BTN_TYPE_ANALOG_INVERTED)) { - // not an ADC analog pin - DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), btnPin[i], i); - btnPin[i] = -1; - pinManager.deallocatePin(hw_btn_pin,PinOwner::Button); + if (digitalPinToAnalogChannel(btnPin[i]) < 0) { + // not an ADC analog pin + DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), btnPin[i], i); + btnPin[i] = -1; + pinManager.deallocatePin(hw_btn_pin,PinOwner::Button); + } else { + analogReadResolution(12); // see #4040 + } } else if ((buttonType[i] == BTN_TYPE_TOUCH || buttonType[i] == BTN_TYPE_TOUCH_SWITCH)) { diff --git a/wled00/wled.cpp b/wled00/wled.cpp index ec83d4583..deb65a89b 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -175,39 +175,39 @@ void WLED::loop() DEBUG_PRINTLN(F("Re-init busses.")); bool aligned = strip.checkSegmentAlignment(); //see if old segments match old bus(ses) BusManager::removeAll(); + unsigned mem = 0; // determine if it is sensible to use parallel I2S outputs on ESP32 (i.e. more than 5 outputs = 1 I2S + 4 RMT) bool useParallel = false; #if defined(ARDUINO_ARCH_ESP32) && !defined(ARDUINO_ARCH_ESP32S2) && !defined(ARDUINO_ARCH_ESP32S3) && !defined(ARDUINO_ARCH_ESP32C3) unsigned digitalCount = 0; - unsigned maxLeds = 0; - int oldType = 0; + unsigned maxLedsOnBus = 0; + unsigned maxChannels = 0; for (unsigned i = 0; i < WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES; i++) { if (busConfigs[i] == nullptr) break; - if (IS_DIGITAL(busConfigs[i]->type) && !IS_2PIN(busConfigs[i]->type)) digitalCount++; - if (busConfigs[i]->count > maxLeds) maxLeds = busConfigs[i]->count; - // we need to have all LEDs of the same type for parallel - if (i < 8 && oldType > 0 && oldType != busConfigs[i]->type) oldType = -1; - else if (oldType == 0) oldType = busConfigs[i]->type; + if (!IS_DIGITAL(busConfigs[i]->type)) continue; + if (!IS_2PIN(busConfigs[i]->type)) { + digitalCount++; + unsigned channels = Bus::getNumberOfChannels(busConfigs[i]->type); + if (busConfigs[i]->count > maxLedsOnBus) maxLedsOnBus = busConfigs[i]->count; + if (channels > maxChannels) maxChannels = channels; + } } - DEBUG_PRINTF_P(PSTR("Maximum LEDs on a bus: %u\nDigital buses: %u\nDifferent types: %d\n"), maxLeds, digitalCount, (int)(oldType == -1)); + DEBUG_PRINTF_P(PSTR("Maximum LEDs on a bus: %u\nDigital buses: %u\n"), maxLedsOnBus, digitalCount); // we may remove 300 LEDs per bus limit when NeoPixelBus is updated beyond 2.9.0 - if (/*oldType != -1 && */maxLeds <= 300 && digitalCount > 5) { + if (maxLedsOnBus <= 300 && digitalCount > 5) { + DEBUG_PRINTF_P(PSTR("Switching to parallel I2S.")); useParallel = true; BusManager::useParallelOutput(); - DEBUG_PRINTF_P(PSTR("Switching to parallel I2S with max. %d LEDs per ouptut.\n"), maxLeds); + mem = BusManager::memUsage(maxChannels, maxLedsOnBus, 8); // use alternate memory calculation (hse to be used *after* useParallelOutput()) } #endif // create buses/outputs - unsigned mem = 0; for (unsigned i = 0; i < WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES; i++) { if (busConfigs[i] == nullptr || (!useParallel && i > 10)) break; if (useParallel && i < 8) { - // we are using parallel I2S and memUsage() will include x8 allocation into account - if (i == 0) - mem = BusManager::memUsage(*busConfigs[i]); // includes x8 memory allocation for parallel I2S - else - if (BusManager::memUsage(*busConfigs[i]) > mem) - mem = BusManager::memUsage(*busConfigs[i]); // if we have unequal LED count use the largest + // if for some unexplained reason the above pre-calculation was wrong, update + unsigned memT = BusManager::memUsage(*busConfigs[i]); // includes x8 memory allocation for parallel I2S + if (memT > mem) mem = memT; // if we have unequal LED count use the largest } else mem += BusManager::memUsage(*busConfigs[i]); // includes global buffer if (mem <= MAX_LED_MEMORY) BusManager::add(*busConfigs[i]); diff --git a/wled00/wled.h b/wled00/wled.h index f1fbc5e05..f761b970d 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -8,7 +8,7 @@ */ // version code in format yymmddb (b = daily build) -#define VERSION 2406290 +#define VERSION 2407070 //uncomment this if you have a "my_config.h" file you'd like to use //#define WLED_USE_MY_CONFIG From 551b8af76d9a82d34c66abd5e29d9e10089be6ef Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Tue, 9 Jul 2024 19:00:32 +0200 Subject: [PATCH 012/142] Hide 2D effects on 1D segments --- wled00/data/index.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/wled00/data/index.js b/wled00/data/index.js index 4e5a1eb0e..447bf03a6 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -1,7 +1,7 @@ //page js var loc = false, locip, locproto = "http:"; var isOn = false, nlA = false, isLv = false, isInfo = false, isNodes = false, syncSend = false/*, syncTglRecv = true*/; -var hasWhite = false, hasRGB = false, hasCCT = false; +var hasWhite = false, hasRGB = false, hasCCT = false, has2D = false; var nlDur = 60, nlTar = 0; var nlMode = false; var segLmax = 0; // size (in pixels) of largest selected segment @@ -1339,7 +1339,7 @@ function updateSelectedFx() if (ds.id>0) { if (segLmax==0) fx.classList.add('hide'); // none of the segments selected (hide all effects) else { - if ((segLmax==1 && (!opts[3] || opts[3].indexOf("0")<0)) || (!isM && opts[3] && ((opts[3].indexOf("2")>=0 && opts[3].indexOf("1")<0)))) fx.classList.add('hide'); + if ((segLmax==1 && (!opts[3] || opts[3].indexOf("0")<0)) || (!has2D && opts[3] && ((opts[3].indexOf("2")>=0 && opts[3].indexOf("1")<0)))) fx.classList.add('hide'); else fx.classList.remove('hide'); } } @@ -1441,7 +1441,7 @@ function readState(s,command=false) populateSegments(s); var selc=0; var sellvl=0; // 0: selc is invalid, 1: selc is mainseg, 2: selc is first selected - hasRGB = hasWhite = hasCCT = false; + hasRGB = hasWhite = hasCCT = has2D = false; segLmax = 0; for (let i = 0; i < (s.seg||[]).length; i++) { @@ -1452,20 +1452,23 @@ function readState(s,command=false) if (s.seg[i].sel) { if (sellvl < 2) selc = i; // get first selected segment sellvl = 2; - var lc = lastinfo.leds.seglc[i]; + let w = (s.seg[i].stop - s.seg[i].start); + let h = s.seg[i].stopY ? (s.seg[i].stopY - s.seg[i].startY) : 1; + let lc = lastinfo.leds.seglc[i]; hasRGB |= !!(lc & 0x01); hasWhite |= !!(lc & 0x02); hasCCT |= !!(lc & 0x04); - let sLen = (s.seg[i].stop - s.seg[i].start)*(s.seg[i].stopY?(s.seg[i].stopY - s.seg[i].startY):1); - segLmax = segLmax < sLen ? sLen : segLmax; + has2D |= w > 1 && h > 1; + if (w*h > segLmax) segLmax = w*h; } } var i=s.seg[selc]; if (sellvl == 1) { - var lc = lastinfo.leds.seglc[selc]; + let lc = lastinfo.leds.seglc[selc]; hasRGB = !!(lc & 0x01); hasWhite = !!(lc & 0x02); hasCCT = !!(lc & 0x04); + has2D = (i.stop - i.start) > 1 && (i.stopY ? (i.stopY - i.startY) : 1) > 1; } if (!i) { showToast('No Segments!', true); From efa32ed4f6b350512f306cae7e4ee47528416dab Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Tue, 9 Jul 2024 21:50:27 +0200 Subject: [PATCH 013/142] Size optimisations --- wled00/bus_manager.cpp | 46 +++++++++++++------------- wled00/button.cpp | 8 ++--- wled00/colors.cpp | 4 +-- wled00/e131.cpp | 75 +++++++++++++++++++++--------------------- wled00/file.cpp | 2 +- wled00/improv.cpp | 34 +++++++++---------- wled00/json.cpp | 20 +++++------ wled00/led.cpp | 6 ++-- wled00/set.cpp | 32 +++++++++--------- wled00/udp.cpp | 36 ++++++++++---------- wled00/util.cpp | 14 ++++---- wled00/wled_serial.cpp | 12 +++---- 12 files changed, 144 insertions(+), 145 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 46e909edd..7aa3351cf 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -80,15 +80,15 @@ uint8_t IRAM_ATTR ColorOrderMap::getPixelColorOrder(uint16_t pix, uint8_t defaul uint32_t Bus::autoWhiteCalc(uint32_t c) { - uint8_t aWM = _autoWhiteMode; + unsigned aWM = _autoWhiteMode; if (_gAWM < AW_GLOBAL_DISABLED) aWM = _gAWM; if (aWM == RGBW_MODE_MANUAL_ONLY) return c; - uint8_t w = W(c); + unsigned w = W(c); //ignore auto-white calculation if w>0 and mode DUAL (DUAL behaves as BRIGHTER if w==0) if (w > 0 && aWM == RGBW_MODE_DUAL) return c; - uint8_t r = R(c); - uint8_t g = G(c); - uint8_t b = B(c); + unsigned r = R(c); + unsigned g = G(c); + unsigned b = B(c); if (aWM == RGBW_MODE_MAX) return RGBW32(r, g, b, r > g ? (r > b ? r : b) : (g > b ? g : b)); // brightest RGB channel w = r < g ? (r < b ? r : b) : (g < b ? g : b); if (aWM == RGBW_MODE_AUTO_ACCURATE) { r -= w; g -= w; b -= w; } //subtract w in ACCURATE mode @@ -207,7 +207,7 @@ void BusDigital::show() { if (!_valid) return; uint8_t cctWW = 0, cctCW = 0; - uint8_t newBri = estimateCurrentAndLimitBri(); // will fill _milliAmpsTotal + unsigned newBri = estimateCurrentAndLimitBri(); // will fill _milliAmpsTotal if (newBri < _bri) PolyBus::setBrightness(_busPtr, _iType, newBri); // limit brightness to stay within current limits if (_data) { @@ -215,7 +215,7 @@ void BusDigital::show() { int16_t oldCCT = Bus::_cct; // temporarily save bus CCT for (size_t i=0; i<_len; i++) { size_t offset = i * channels; - uint8_t co = _colorOrderMap.getPixelColorOrder(i+_start, _colorOrder); + unsigned co = _colorOrderMap.getPixelColorOrder(i+_start, _colorOrder); uint32_t c; if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs (_len is always a multiple of 3) switch (i%3) { @@ -234,7 +234,7 @@ void BusDigital::show() { Bus::_cct = _data[offset+channels-1]; Bus::calculateCCT(c, cctWW, cctCW); } - uint16_t pix = i; + unsigned pix = i; if (_reversed) pix = _len - pix -1; pix += _skip; PolyBus::setPixelColor(_busPtr, _iType, pix, c, co, (cctCW<<8) | cctWW); @@ -246,7 +246,7 @@ void BusDigital::show() { Bus::_cct = oldCCT; } else { if (newBri < _bri) { - uint16_t hwLen = _len; + unsigned hwLen = _len; if (_type == TYPE_WS2812_1CH_X3) hwLen = NUM_ICS_WS2812_1CH_3X(_len); // only needs a third of "RGB" LEDs for NeoPixelBus for (unsigned i = 0; i < hwLen; i++) { // use 0 as color order, actual order does not matter here as we just update the channel values as-is @@ -302,9 +302,9 @@ void IRAM_ATTR BusDigital::setPixelColor(uint16_t pix, uint32_t c) { } else { if (_reversed) pix = _len - pix -1; pix += _skip; - uint8_t co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); + unsigned co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs - uint16_t pOld = pix; + unsigned pOld = pix; pix = IC_INDEX_WS2812_1CH_3X(pix); uint32_t cOld = restoreColorLossy(PolyBus::getPixelColor(_busPtr, _iType, pix, co),_bri); switch (pOld % 3) { // change only the single channel (TODO: this can cause loss because of get/set) @@ -333,12 +333,12 @@ uint32_t IRAM_ATTR BusDigital::getPixelColor(uint16_t pix) { } else { if (_reversed) pix = _len - pix -1; pix += _skip; - uint8_t co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); + unsigned co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); uint32_t c = restoreColorLossy(PolyBus::getPixelColor(_busPtr, _iType, (_type==TYPE_WS2812_1CH_X3) ? IC_INDEX_WS2812_1CH_3X(pix) : pix, co),_bri); if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs - uint8_t r = R(c); - uint8_t g = _reversed ? B(c) : G(c); // should G and B be switched if _reversed? - uint8_t b = _reversed ? G(c) : B(c); + unsigned r = R(c); + unsigned g = _reversed ? B(c) : G(c); // should G and B be switched if _reversed? + unsigned b = _reversed ? G(c) : B(c); switch (pix % 3) { // get only the single channel case 0: c = RGBW32(g, g, g, g); break; case 1: c = RGBW32(r, r, r, r); break; @@ -350,7 +350,7 @@ uint32_t IRAM_ATTR BusDigital::getPixelColor(uint16_t pix) { } uint8_t BusDigital::getPins(uint8_t* pinArray) { - uint8_t numPins = IS_2PIN(_type) ? 2 : 1; + unsigned numPins = IS_2PIN(_type) ? 2 : 1; for (unsigned i = 0; i < numPins; i++) pinArray[i] = _pins[i]; return numPins; } @@ -382,7 +382,7 @@ BusPwm::BusPwm(BusConfig &bc) : Bus(bc.type, bc.start, bc.autoWhite, 1, bc.reversed) { if (!IS_PWM(bc.type)) return; - uint8_t numPins = NUM_PWM_PINS(bc.type); + unsigned numPins = NUM_PWM_PINS(bc.type); _frequency = bc.frequency ? bc.frequency : WLED_PWM_FREQ; #ifdef ESP8266 @@ -512,7 +512,7 @@ static const uint16_t cieLUT[256] = { void BusPwm::show() { if (!_valid) return; - uint8_t numPins = NUM_PWM_PINS(_type); + unsigned numPins = NUM_PWM_PINS(_type); unsigned maxBri = (1<<_depth) - 1; #ifdef ESP8266 unsigned pwmBri = (unsigned)(roundf(powf((float)_bri / 255.0f, 1.7f) * (float)maxBri)); // using gamma 1.7 to extrapolate PWM duty cycle @@ -532,7 +532,7 @@ void BusPwm::show() { uint8_t BusPwm::getPins(uint8_t* pinArray) { if (!_valid) return 0; - uint8_t numPins = NUM_PWM_PINS(_type); + unsigned numPins = NUM_PWM_PINS(_type); for (unsigned i = 0; i < numPins; i++) { pinArray[i] = _pins[i]; } @@ -540,7 +540,7 @@ uint8_t BusPwm::getPins(uint8_t* pinArray) { } void BusPwm::deallocatePins() { - uint8_t numPins = NUM_PWM_PINS(_type); + unsigned numPins = NUM_PWM_PINS(_type); for (unsigned i = 0; i < numPins; i++) { pinManager.deallocatePin(_pins[i], PinOwner::BusPwm); if (!pinManager.isPinOk(_pins[i])) continue; @@ -632,7 +632,7 @@ void BusNetwork::setPixelColor(uint16_t pix, uint32_t c) { if (!_valid || pix >= _len) return; if (_rgbw) c = autoWhiteCalc(c); if (Bus::_cct >= 1900) c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT - uint16_t offset = pix * _UDPchannels; + unsigned offset = pix * _UDPchannels; _data[offset] = R(c); _data[offset+1] = G(c); _data[offset+2] = B(c); @@ -641,7 +641,7 @@ void BusNetwork::setPixelColor(uint16_t pix, uint32_t c) { uint32_t BusNetwork::getPixelColor(uint16_t pix) { if (!_valid || pix >= _len) return 0; - uint16_t offset = pix * _UDPchannels; + unsigned offset = pix * _UDPchannels; return RGBW32(_data[offset], _data[offset+1], _data[offset+2], (_rgbw ? _data[offset+3] : 0)); } @@ -854,7 +854,7 @@ Bus* BusManager::getBus(uint8_t busNr) { //semi-duplicate of strip.getLengthTotal() (though that just returns strip._length, calculated in finalizeInit()) uint16_t BusManager::getTotalLength() { - uint16_t len = 0; + unsigned len = 0; for (unsigned i=0; igetLength(); return len; } diff --git a/wled00/button.cpp b/wled00/button.cpp index 8594c868b..23d7b8a90 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -93,7 +93,7 @@ void doublePressAction(uint8_t b) bool isButtonPressed(uint8_t i) { if (btnPin[i]<0) return false; - uint8_t pin = btnPin[i]; + unsigned pin = btnPin[i]; switch (buttonType[i]) { case BTN_TYPE_NONE: @@ -171,7 +171,7 @@ void handleAnalog(uint8_t b) { 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 + unsigned rawReading; // raw value from analogRead, scaled to 12bit DEBUG_PRINT(F("Analog: Reading button ")); DEBUG_PRINTLN(b); @@ -184,7 +184,7 @@ void handleAnalog(uint8_t b) 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 + unsigned 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; @@ -260,7 +260,7 @@ void handleButton() if (strip.isUpdating() && (now - lastRun < ANALOG_BTN_READ_CYCLE+1)) return; // don't interfere with strip update (unless strip is updating continuously, e.g. very long strips) lastRun = now; - for (uint8_t b=0; b max) max = g; if (b > max) max = b; if (w > max) max = w; diff --git a/wled00/e131.cpp b/wled00/e131.cpp index ee8fa3949..2d172e072 100644 --- a/wled00/e131.cpp +++ b/wled00/e131.cpp @@ -26,22 +26,21 @@ void handleDDPPacket(e131_packet_t* p) { } } - uint8_t ddpChannelsPerLed = ((p->dataType & 0b00111000)>>3 == 0b011) ? 4 : 3; // data type 0x1B (formerly 0x1A) is RGBW (type 3, 8 bit/channel) + unsigned ddpChannelsPerLed = ((p->dataType & 0b00111000)>>3 == 0b011) ? 4 : 3; // data type 0x1B (formerly 0x1A) is RGBW (type 3, 8 bit/channel) uint32_t start = htonl(p->channelOffset) / ddpChannelsPerLed; start += DMXAddress / ddpChannelsPerLed; - uint16_t stop = start + htons(p->dataLen) / ddpChannelsPerLed; + unsigned stop = start + htons(p->dataLen) / ddpChannelsPerLed; uint8_t* data = p->data; - uint16_t c = 0; + unsigned c = 0; if (p->flags & DDP_TIMECODE_FLAG) c = 4; //packet has timecode flag, we do not support it, but data starts 4 bytes later if (realtimeMode != REALTIME_MODE_DDP) ddpSeenPush = false; // just starting, no push yet realtimeLock(realtimeTimeoutMs, REALTIME_MODE_DDP); if (!realtimeOverride || (realtimeMode && useMainSegmentOnly)) { - for (uint16_t i = start; i < stop; i++) { + for (unsigned i = start; i < stop; i++, c += ddpChannelsPerLed) { setRealtimePixel(i, data[c], data[c+1], data[c+2], ddpChannelsPerLed >3 ? data[c+3] : 0); - c += ddpChannelsPerLed; } } @@ -49,7 +48,7 @@ void handleDDPPacket(e131_packet_t* p) { ddpSeenPush |= push; if (!ddpSeenPush || push) { // if we've never seen a push, or this is one, render display e131NewData = true; - byte sn = p->sequenceNum & 0xF; + int sn = p->sequenceNum & 0xF; if (sn) e131LastSequenceNumber[0] = sn; } } @@ -57,9 +56,9 @@ void handleDDPPacket(e131_packet_t* p) { //E1.31 and Art-Net protocol support void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ - uint16_t uni = 0, dmxChannels = 0; + unsigned uni = 0, dmxChannels = 0; uint8_t* e131_data = nullptr; - uint8_t seq = 0, mde = REALTIME_MODE_E131; + unsigned seq = 0, mde = REALTIME_MODE_E131; if (protocol == P_ARTNET) { @@ -105,7 +104,7 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ // only listen for universes we're handling & allocated memory if (uni < e131Universe || uni >= (e131Universe + E131_MAX_UNIVERSE_COUNT)) return; - uint8_t previousUniverses = uni - e131Universe; + unsigned previousUniverses = uni - e131Universe; if (e131SkipOutOfSequence) if (seq < e131LastSequenceNumber[previousUniverses] && seq > 20 && e131LastSequenceNumber[previousUniverses] < 250){ @@ -123,12 +122,12 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ // update status info realtimeIP = clientIP; byte wChannel = 0; - uint16_t totalLen = strip.getLengthTotal(); - uint16_t availDMXLen = 0; - uint16_t dataOffset = DMXAddress; + unsigned totalLen = strip.getLengthTotal(); + unsigned availDMXLen = 0; + unsigned dataOffset = DMXAddress; // For legacy DMX start address 0 the available DMX length offset is 0 - const uint16_t dmxLenOffset = (DMXAddress == 0) ? 0 : 1; + const unsigned dmxLenOffset = (DMXAddress == 0) ? 0 : 1; // Check if DMX start address fits in available channels if (dmxChannels >= DMXAddress) { @@ -154,7 +153,7 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ if (realtimeOverride && !(realtimeMode && useMainSegmentOnly)) return; wChannel = (availDMXLen > 3) ? e131_data[dataOffset+3] : 0; - for (uint16_t i = 0; i < totalLen; i++) + for (unsigned i = 0; i < totalLen; i++) setRealtimePixel(i, e131_data[dataOffset+0], e131_data[dataOffset+1], e131_data[dataOffset+2], wChannel); break; @@ -171,7 +170,7 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ strip.setBrightness(bri, true); } - for (uint16_t i = 0; i < totalLen; i++) + for (unsigned i = 0; i < totalLen; i++) setRealtimePixel(i, e131_data[dataOffset+1], e131_data[dataOffset+2], e131_data[dataOffset+3], wChannel); break; @@ -180,7 +179,7 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ if (uni != e131Universe || availDMXLen < 2) return; // limit max. selectable preset to 250, even though DMX max. val is 255 - uint8_t dmxValPreset = (e131_data[dataOffset+1] > 250 ? 250 : e131_data[dataOffset+1]); + unsigned dmxValPreset = (e131_data[dataOffset+1] > 250 ? 250 : e131_data[dataOffset+1]); // only apply preset if value changed if (dmxValPreset != 0 && dmxValPreset != currentPreset && @@ -207,8 +206,8 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ { if (uni != e131Universe) return; bool isSegmentMode = DMXMode == DMX_MODE_EFFECT_SEGMENT || DMXMode == DMX_MODE_EFFECT_SEGMENT_W; - uint8_t dmxEffectChannels = (DMXMode == DMX_MODE_EFFECT || DMXMode == DMX_MODE_EFFECT_SEGMENT) ? 15 : 18; - for (uint8_t id = 0; id < strip.getSegmentsNum(); id++) { + unsigned dmxEffectChannels = (DMXMode == DMX_MODE_EFFECT || DMXMode == DMX_MODE_EFFECT_SEGMENT) ? 15 : 18; + for (unsigned id = 0; id < strip.getSegmentsNum(); id++) { Segment& seg = strip.getSegment(id); if (isSegmentMode) dataOffset = DMXAddress + id * (dmxEffectChannels + DMXSegmentSpacing); @@ -271,10 +270,10 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ case DMX_MODE_MULTIPLE_RGBW: { bool is4Chan = (DMXMode == DMX_MODE_MULTIPLE_RGBW); - const uint16_t dmxChannelsPerLed = is4Chan ? 4 : 3; - const uint16_t ledsPerUniverse = is4Chan ? MAX_4_CH_LEDS_PER_UNIVERSE : MAX_3_CH_LEDS_PER_UNIVERSE; + const unsigned dmxChannelsPerLed = is4Chan ? 4 : 3; + const unsigned ledsPerUniverse = is4Chan ? MAX_4_CH_LEDS_PER_UNIVERSE : MAX_3_CH_LEDS_PER_UNIVERSE; uint8_t stripBrightness = bri; - uint16_t previousLeds, dmxOffset, ledsTotal; + unsigned previousLeds, dmxOffset, ledsTotal; if (previousUniverses == 0) { if (availDMXLen < 1) return; @@ -290,8 +289,8 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ } else { // All subsequent universes start at the first channel. dmxOffset = (protocol == P_ARTNET) ? 0 : 1; - const uint16_t dimmerOffset = (DMXMode == DMX_MODE_MULTIPLE_DRGB) ? 1 : 0; - uint16_t ledsInFirstUniverse = (((MAX_CHANNELS_PER_UNIVERSE - DMXAddress) + dmxLenOffset) - dimmerOffset) / dmxChannelsPerLed; + const unsigned dimmerOffset = (DMXMode == DMX_MODE_MULTIPLE_DRGB) ? 1 : 0; + unsigned ledsInFirstUniverse = (((MAX_CHANNELS_PER_UNIVERSE - DMXAddress) + dmxLenOffset) - dimmerOffset) / dmxChannelsPerLed; previousLeds = ledsInFirstUniverse + (previousUniverses - 1) * ledsPerUniverse; ledsTotal = previousLeds + (dmxChannels / dmxChannelsPerLed); } @@ -316,12 +315,12 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ } if (!is4Chan) { - for (uint16_t i = previousLeds; i < ledsTotal; i++) { + for (unsigned i = previousLeds; i < ledsTotal; i++) { setRealtimePixel(i, e131_data[dmxOffset], e131_data[dmxOffset+1], e131_data[dmxOffset+2], 0); dmxOffset+=3; } } else { - for (uint16_t i = previousLeds; i < ledsTotal; i++) { + for (unsigned i = previousLeds; i < ledsTotal; i++) { setRealtimePixel(i, e131_data[dmxOffset], e131_data[dmxOffset+1], e131_data[dmxOffset+2], e131_data[dmxOffset+3]); dmxOffset+=4; } @@ -341,8 +340,8 @@ void handleArtnetPollReply(IPAddress ipAddress) { ArtPollReply artnetPollReply; prepareArtnetPollReply(&artnetPollReply); - uint16_t startUniverse = e131Universe; - uint16_t endUniverse = e131Universe; + unsigned startUniverse = e131Universe; + unsigned endUniverse = e131Universe; switch (DMXMode) { case DMX_MODE_DISABLED: @@ -362,15 +361,15 @@ void handleArtnetPollReply(IPAddress ipAddress) { case DMX_MODE_MULTIPLE_RGBW: { bool is4Chan = (DMXMode == DMX_MODE_MULTIPLE_RGBW); - const uint16_t dmxChannelsPerLed = is4Chan ? 4 : 3; - const uint16_t dimmerOffset = (DMXMode == DMX_MODE_MULTIPLE_DRGB) ? 1 : 0; - const uint16_t dmxLenOffset = (DMXAddress == 0) ? 0 : 1; // For legacy DMX start address 0 - const uint16_t ledsInFirstUniverse = (((MAX_CHANNELS_PER_UNIVERSE - DMXAddress) + dmxLenOffset) - dimmerOffset) / dmxChannelsPerLed; - const uint16_t totalLen = strip.getLengthTotal(); + const unsigned dmxChannelsPerLed = is4Chan ? 4 : 3; + const unsigned dimmerOffset = (DMXMode == DMX_MODE_MULTIPLE_DRGB) ? 1 : 0; + const unsigned dmxLenOffset = (DMXAddress == 0) ? 0 : 1; // For legacy DMX start address 0 + const unsigned ledsInFirstUniverse = (((MAX_CHANNELS_PER_UNIVERSE - DMXAddress) + dmxLenOffset) - dimmerOffset) / dmxChannelsPerLed; + const unsigned totalLen = strip.getLengthTotal(); if (totalLen > ledsInFirstUniverse) { - const uint16_t ledsPerUniverse = is4Chan ? MAX_4_CH_LEDS_PER_UNIVERSE : MAX_3_CH_LEDS_PER_UNIVERSE; - const uint16_t remainLED = totalLen - ledsInFirstUniverse; + const unsigned ledsPerUniverse = is4Chan ? MAX_4_CH_LEDS_PER_UNIVERSE : MAX_3_CH_LEDS_PER_UNIVERSE; + const unsigned remainLED = totalLen - ledsInFirstUniverse; endUniverse += (remainLED / ledsPerUniverse); @@ -391,7 +390,7 @@ void handleArtnetPollReply(IPAddress ipAddress) { } if (DMXMode != DMX_MODE_DISABLED) { - for (uint16_t i = startUniverse; i <= endUniverse; ++i) { + for (unsigned i = startUniverse; i <= endUniverse; ++i) { sendArtnetPollReply(&artnetPollReply, ipAddress, i); } } @@ -417,7 +416,7 @@ void prepareArtnetPollReply(ArtPollReply *reply) { reply->reply_opcode = ARTNET_OPCODE_OPPOLLREPLY; IPAddress localIP = Network.localIP(); - for (uint8_t i = 0; i < 4; i++) { + for (unsigned i = 0; i < 4; i++) { reply->reply_ip[i] = localIP[i]; } @@ -493,7 +492,7 @@ void prepareArtnetPollReply(ArtPollReply *reply) { Network.localMAC(reply->reply_mac); - for (uint8_t i = 0; i < 4; i++) { + for (unsigned i = 0; i < 4; i++) { reply->reply_bind_ip[i] = localIP[i]; } @@ -517,7 +516,7 @@ void prepareArtnetPollReply(ArtPollReply *reply) { // Node does not support fail-over reply->reply_status_3 = 0x00; - for (uint8_t i = 0; i < 21; i++) { + for (unsigned i = 0; i < 21; i++) { reply->reply_filler[i] = 0x00; } } diff --git a/wled00/file.cpp b/wled00/file.cpp index 814aa77e6..69e1e692c 100644 --- a/wled00/file.cpp +++ b/wled00/file.cpp @@ -138,7 +138,7 @@ static bool bufferedFindObjectEnd() { if (!f || !f.size()) return false; - uint16_t objDepth = 0; //num of '{' minus num of '}'. return once 0 + unsigned objDepth = 0; //num of '{' minus num of '}'. return once 0 //size_t start = f.position(); byte buf[FS_BUFSIZE]; diff --git a/wled00/improv.cpp b/wled00/improv.cpp index 34af6487a..d18061ba2 100644 --- a/wled00/improv.cpp +++ b/wled00/improv.cpp @@ -42,12 +42,12 @@ void handleImprovPacket() { uint8_t header[6] = {'I','M','P','R','O','V'}; bool timeout = false; - uint8_t waitTime = 25; - uint16_t packetByte = 0; - uint8_t packetLen = 9; - uint8_t checksum = 0; + unsigned waitTime = 25; + unsigned packetByte = 0; + unsigned packetLen = 9; + unsigned checksum = 0; - uint8_t rpcCommandType = 0; + unsigned rpcCommandType = 0; char rpcData[128]; rpcData[0] = 0; @@ -92,7 +92,7 @@ void handleImprovPacket() { switch (rpcCommandType) { case ImprovRPCType::Command_Wifi: parseWiFiCommand(rpcData); break; case ImprovRPCType::Request_State: { - uint8_t improvState = 0x02; //authorized + unsigned improvState = 0x02; //authorized if (WLED_WIFI_CONFIGURED) improvState = 0x03; //provisioning if (Network.isConnected()) improvState = 0x04; //provisioned sendImprovStateResponse(improvState, false); @@ -136,8 +136,8 @@ void sendImprovStateResponse(uint8_t state, bool error) { out[8] = 1; out[9] = state; - uint8_t checksum = 0; - for (uint8_t i = 0; i < 10; i++) checksum += out[i]; + unsigned checksum = 0; + for (unsigned i = 0; i < 10; i++) checksum += out[i]; out[10] = checksum; Serial.write((uint8_t*)out, 11); Serial.write('\n'); @@ -146,16 +146,16 @@ void sendImprovStateResponse(uint8_t state, bool error) { // used by sendImprovIPRPCResult(), sendImprovInfoResponse(), and handleImprovWifiScan() void sendImprovRPCResult(ImprovRPCType type, uint8_t n_strings, const char **strings) { if (improvError > 0 && improvError < 3) sendImprovStateResponse(0x00, true); - uint8_t packetLen = 12; + unsigned packetLen = 12; char out[256] = {'I','M','P','R','O','V'}; out[6] = IMPROV_VERSION; out[7] = ImprovPacketType::RPC_Response; //out[8] = 2; //Length (set below) out[9] = type; //out[10] = 0; //Data len (set below) - uint16_t pos = 11; + unsigned pos = 11; - for (uint8_t s = 0; s < n_strings; s++) { + for (unsigned s = 0; s < n_strings; s++) { size_t len = strlen(strings[s]); if (pos + len > 254) continue; // simple buffer overflow guard out[pos++] = len; @@ -167,8 +167,8 @@ void sendImprovRPCResult(ImprovRPCType type, uint8_t n_strings, const char **str out[8] = pos -9; // Length of packet (excluding first 9 header bytes and final checksum byte) out[10] = pos -11; // Data len - uint8_t checksum = 0; - for (uint8_t i = 0; i < packetLen -1; i++) checksum += out[i]; + unsigned checksum = 0; + for (unsigned i = 0; i < packetLen -1; i++) checksum += out[i]; out[packetLen -1] = checksum; Serial.write((uint8_t*)out, packetLen); Serial.write('\n'); @@ -181,7 +181,7 @@ void sendImprovIPRPCResult(ImprovRPCType type) { { char urlStr[64]; IPAddress localIP = Network.localIP(); - uint8_t len = sprintf(urlStr, "http://%d.%d.%d.%d", localIP[0], localIP[1], localIP[2], localIP[3]); + unsigned len = sprintf(urlStr, "http://%d.%d.%d.%d", localIP[0], localIP[1], localIP[2], localIP[3]); if (len > 24) return; //sprintf fail? const char *str[1] = {urlStr}; sendImprovRPCResult(type, 1, str); @@ -254,17 +254,17 @@ void handleImprovWifiScan() {} #endif void parseWiFiCommand(char* rpcData) { - uint8_t len = rpcData[0]; + unsigned len = rpcData[0]; if (!len || len > 126) return; - uint8_t ssidLen = rpcData[1]; + unsigned ssidLen = rpcData[1]; if (ssidLen > len -1 || ssidLen > 32) return; memset(multiWiFi[0].clientSSID, 0, 32); memcpy(multiWiFi[0].clientSSID, rpcData+2, ssidLen); memset(multiWiFi[0].clientPass, 0, 64); if (len > ssidLen +1) { - uint8_t passLen = rpcData[2+ssidLen]; + unsigned passLen = rpcData[2+ssidLen]; memset(multiWiFi[0].clientPass, 0, 64); memcpy(multiWiFi[0].clientPass, rpcData+3+ssidLen, passLen); } diff --git a/wled00/json.cpp b/wled00/json.cpp index 9e3e39bb4..895709680 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -37,14 +37,14 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) Segment prev = seg; //make a backup so we can tell if something changed (calling copy constructor) //DEBUG_PRINTF_P(PSTR("-- Duplicate segment: %p (%p)\n"), &prev, prev.data); - uint16_t start = elem["start"] | seg.start; + unsigned start = elem["start"] | seg.start; if (stop < 0) { int len = elem["len"]; stop = (len > 0) ? start + len : seg.stop; } // 2D segments - uint16_t startY = elem["startY"] | seg.startY; - uint16_t stopY = elem["stopY"] | seg.stopY; + unsigned startY = elem["startY"] | seg.startY; + unsigned stopY = elem["stopY"] | seg.stopY; //repeat, multiplies segment until all LEDs are used, or max segments reached bool repeat = elem["rpt"] | false; @@ -52,7 +52,7 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) elem.remove("id"); // remove for recursive call elem.remove("rpt"); // remove for recursive call elem.remove("n"); // remove for recursive call - uint16_t len = stop - start; + unsigned len = stop - start; for (size_t i=id+1; i= strip.getLengthTotal()) break; @@ -105,7 +105,7 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) uint8_t set = elem[F("set")] | seg.set; seg.set = constrain(set, 0, 3); - uint16_t len = 1; + unsigned len = 1; if (stop > start) len = stop - start; int offset = elem[F("of")] | INT32_MAX; if (offset != INT32_MAX) { @@ -659,12 +659,12 @@ void serializeInfo(JsonObject root) } #endif - uint8_t totalLC = 0; + unsigned totalLC = 0; JsonArray lcarr = leds.createNestedArray(F("seglc")); size_t nSegs = strip.getSegmentsNum(); for (size_t s = 0; s < nSegs; s++) { if (!strip.getSegment(s).isActive()) continue; - uint8_t lc = strip.getSegment(s).getLightCapabilities(); + unsigned lc = strip.getSegment(s).getLightCapabilities(); totalLC |= lc; lcarr.add(lc); } @@ -847,7 +847,7 @@ void setPaletteColors(JsonArray json, byte* tcp) TRGBGradientPaletteEntryUnion u; // Count entries - uint16_t count = 0; + unsigned count = 0; do { u = *(ent + count); count++; @@ -1166,8 +1166,8 @@ bool serveLiveLeds(AsyncWebServerRequest* request, uint32_t wsClient) } #endif - uint16_t used = strip.getLengthTotal(); - uint16_t n = (used -1) /MAX_LIVE_LEDS +1; //only serve every n'th LED if count over MAX_LIVE_LEDS + unsigned used = strip.getLengthTotal(); + unsigned n = (used -1) /MAX_LIVE_LEDS +1; //only serve every n'th LED if count over MAX_LIVE_LEDS #ifndef WLED_DISABLE_2D if (strip.isMatrix) { // ignore anything behid matrix (i.e. extra strip) diff --git a/wled00/led.cpp b/wled00/led.cpp index dc3f96e67..704296461 100644 --- a/wled00/led.cpp +++ b/wled00/led.cpp @@ -29,9 +29,9 @@ void setValuesFromSegment(uint8_t s) void applyValuesToSelectedSegs() { // copy of first selected segment to tell if value was updated - uint8_t firstSel = strip.getFirstSelectedSegId(); + unsigned firstSel = strip.getFirstSelectedSegId(); Segment selsegPrev = strip.getSegment(firstSel); - for (uint8_t i = 0; i < strip.getSegmentsNum(); i++) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (i != firstSel && (!seg.isActive() || !seg.isSelected())) continue; @@ -70,7 +70,7 @@ void toggleOnOff() //scales the brightness with the briMultiplier factor byte scaledBri(byte in) { - uint16_t val = ((uint16_t)in*briMultiplier)/100; + unsigned val = ((uint16_t)in*briMultiplier)/100; if (val > 255) val = 255; return (byte)val; } diff --git a/wled00/set.cpp b/wled00/set.cpp index 651e5b2e0..13295df21 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -113,7 +113,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) pinManager.deallocatePin(irPin, PinOwner::IR); } #endif - for (uint8_t s=0; s=0 && pinManager.isPinAllocated(btnPin[s], PinOwner::Button)) { pinManager.deallocatePin(btnPin[s], PinOwner::Button); #ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a function to check touch state, detach interrupt @@ -123,11 +123,11 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) } } - uint8_t colorOrder, type, skip, awmode, channelSwap, maPerLed; - uint16_t length, start, maMax; + unsigned colorOrder, type, skip, awmode, channelSwap, maPerLed; + unsigned length, start, maMax; uint8_t pins[5] = {255, 255, 255, 255, 255}; - uint16_t ablMilliampsMax = request->arg(F("MA")).toInt(); + unsigned ablMilliampsMax = request->arg(F("MA")).toInt(); BusManager::setMilliampsMax(ablMilliampsMax); autoSegments = request->hasArg(F("MS")); @@ -505,7 +505,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) macroAlexaOff = request->arg(F("A1")).toInt(); macroCountdown = request->arg(F("MC")).toInt(); macroNl = request->arg(F("MN")).toInt(); - for (uint8_t i=0; ihasArg(F("PIN"))) { const char *pin = request->arg(F("PIN")).c_str(); - uint8_t pinLen = strlen(pin); + unsigned pinLen = strlen(pin); if (pinLen == 4 || pinLen == 0) { - uint8_t numZeros = 0; - for (uint8_t i = 0; i < pinLen; i++) numZeros += (pin[i] == '0'); + unsigned numZeros = 0; + for (unsigned i = 0; i < pinLen; i++) numZeros += (pin[i] == '0'); if (numZeros < pinLen || pinLen == 0) { // ignore 0000 input (placeholder) strlcpy(settingsPIN, pin, 5); } @@ -682,7 +682,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) JsonObject um = pDoc->createNestedObject("um"); size_t args = request->args(); - uint16_t j=0; + unsigned j=0; for (size_t i=0; iargName(i); String value = request->arg(i); @@ -763,12 +763,12 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) if (strip.isMatrix) { strip.panels = MAX(1,MIN(WLED_MAX_PANELS,request->arg(F("MPC")).toInt())); strip.panel.reserve(strip.panels); // pre-allocate memory - for (uint8_t i=0; ihasArg(pO)) break; pO[l] = 'B'; p.bottomStart = request->arg(pO).toInt(); @@ -822,7 +822,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) pos = req.indexOf(F("SS=")); if (pos > 0) { - byte t = getNumVal(&req, pos); + unsigned t = getNumVal(&req, pos); if (t < strip.getSegmentsNum()) { selectedSeg = t; singleSegment = true; @@ -832,8 +832,8 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) Segment& selseg = strip.getSegment(selectedSeg); pos = req.indexOf(F("SV=")); //segment selected if (pos > 0) { - byte t = getNumVal(&req, pos); - if (t == 2) for (uint8_t i = 0; i < strip.getSegmentsNum(); i++) strip.getSegment(i).selected = false; // unselect other segments + unsigned t = getNumVal(&req, pos); + if (t == 2) for (unsigned i = 0; i < strip.getSegmentsNum(); i++) strip.getSegment(i).selected = false; // unselect other segments selseg.selected = t; } @@ -1009,7 +1009,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) pos = req.indexOf(F("SC")); if (pos > 0) { byte temp; - for (uint8_t i=0; i<4; i++) { + for (unsigned i=0; i<4; i++) { temp = colIn[i]; colIn[i] = colInSec[i]; colInSec[i] = temp; @@ -1050,7 +1050,7 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) stateChanged |= (fxModeChanged || speedChanged || intensityChanged || paletteChanged || custom1Changed || custom2Changed || custom3Changed || check1Changed || check2Changed || check3Changed); // apply to main and all selected segments to prevent #1618. - for (uint8_t i = 0; i < strip.getSegmentsNum(); i++) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { Segment& seg = strip.getSegment(i); if (i != selectedSeg && (singleSegment || !seg.isActive() || !seg.isSelected())) continue; // skip non main segments if not applying to all if (fxModeChanged) seg.setMode(effectIn, req.indexOf(F("FXD="))>0); // apply defaults if FXD= is specified diff --git a/wled00/udp.cpp b/wled00/udp.cpp index 6f4dfeaa4..c2221e2cf 100644 --- a/wled00/udp.cpp +++ b/wled00/udp.cpp @@ -106,7 +106,7 @@ void notify(byte callMode, bool followUp) for (size_t i = 0; i < nsegs; i++) { Segment &selseg = strip.getSegment(i); if (!selseg.isActive()) continue; - uint16_t ofs = 41 + s*UDP_SEG_SIZE; //start of segment offset byte + unsigned ofs = 41 + s*UDP_SEG_SIZE; //start of segment offset byte udpOut[0 +ofs] = s; udpOut[1 +ofs] = selseg.start >> 8; udpOut[2 +ofs] = selseg.start & 0xFF; @@ -241,7 +241,7 @@ void parseNotifyPacket(uint8_t *udpIn) { if (version > 6) { strip.setColor(2, RGBW32(udpIn[20], udpIn[21], udpIn[22], udpIn[23])); // tertiary color if (version > 9 && udpIn[37] < 255) { // valid CCT/Kelvin value - uint16_t cct = udpIn[38]; + unsigned cct = udpIn[38]; if (udpIn[37] > 0) { //Kelvin cct |= (udpIn[37] << 8); } @@ -255,7 +255,7 @@ void parseNotifyPacket(uint8_t *udpIn) { bool applyEffects = (receiveNotificationEffects || !someSel); if (applyEffects && currentPlaylist >= 0) unloadPlaylist(); if (version > 10 && (receiveSegmentOptions || receiveSegmentBounds)) { - uint8_t numSrcSegs = udpIn[39]; + unsigned numSrcSegs = udpIn[39]; DEBUG_PRINT(F("UDP segments: ")); DEBUG_PRINTLN(numSrcSegs); // are we syncing bounds and slave has more active segments than master? if (receiveSegmentBounds && numSrcSegs < strip.getActiveSegmentsNum()) { @@ -268,8 +268,8 @@ void parseNotifyPacket(uint8_t *udpIn) { } size_t inactiveSegs = 0; for (size_t i = 0; i < numSrcSegs && i < strip.getMaxSegments(); i++) { - uint16_t ofs = 41 + i*udpIn[40]; //start of segment offset byte - uint8_t id = udpIn[0 +ofs]; + unsigned ofs = 41 + i*udpIn[40]; //start of segment offset byte + unsigned id = udpIn[0 +ofs]; DEBUG_PRINT(F("UDP segment received: ")); DEBUG_PRINTLN(id); if (id > strip.getSegmentsNum()) break; else if (id == strip.getSegmentsNum()) { @@ -405,7 +405,7 @@ void parseNotifyPacket(uint8_t *udpIn) { void realtimeLock(uint32_t timeoutMs, byte md) { if (!realtimeMode && !realtimeOverride) { - uint16_t stop, start; + unsigned stop, start; if (useMainSegmentOnly) { Segment& mainseg = strip.getMainSegment(); start = mainseg.start; @@ -505,8 +505,8 @@ void handleNotifications() rgbUdp.read(lbuf, packetSize); realtimeLock(realtimeTimeoutMs, REALTIME_MODE_HYPERION); if (realtimeOverride && !(realtimeMode && useMainSegmentOnly)) return; - uint16_t id = 0; - uint16_t totalLen = strip.getLengthTotal(); + unsigned id = 0; + unsigned totalLen = strip.getLengthTotal(); for (size_t i = 0; i < packetSize -2; i += 3) { setRealtimePixel(id, lbuf[i], lbuf[i+1], lbuf[i+2], 0); @@ -523,7 +523,7 @@ void handleNotifications() if (!isSupp && notifierUdp.remoteIP() == localIP) return; //don't process broadcasts we send ourselves uint8_t udpIn[packetSize +1]; - uint16_t len; + unsigned len; if (isSupp) len = notifier2Udp.read(udpIn, packetSize); else len = notifierUdp.read(udpIn, packetSize); @@ -531,7 +531,7 @@ void handleNotifications() if (isSupp && udpIn[0] == 255 && udpIn[1] == 1 && len >= 40) { if (!nodeListEnabled || notifier2Udp.remoteIP() == localIP) return; - uint8_t unit = udpIn[39]; + unsigned unit = udpIn[39]; NodesMap::iterator it = Nodes.find(unit); if (it == Nodes.end() && Nodes.size() < WLED_MAX_NODES) { // Create a new element when not present Nodes[unit].age = 0; @@ -588,8 +588,8 @@ void handleNotifications() byte packetNum = udpIn[4]; //starts with 1! byte numPackets = udpIn[5]; - uint16_t id = (tpmPayloadFrameSize/3)*(packetNum-1); //start LED - uint16_t totalLen = strip.getLengthTotal(); + unsigned id = (tpmPayloadFrameSize/3)*(packetNum-1); //start LED + unsigned totalLen = strip.getLengthTotal(); for (size_t i = 6; i < tpmPayloadFrameSize + 4U; i += 3) { if (id < totalLen) @@ -623,7 +623,7 @@ void handleNotifications() } if (realtimeOverride && !(realtimeMode && useMainSegmentOnly)) return; - uint16_t totalLen = strip.getLengthTotal(); + unsigned totalLen = strip.getLengthTotal(); if (udpIn[0] == 1 && packetSize > 5) //warls { for (size_t i = 2; i < packetSize -3; i += 4) @@ -632,7 +632,7 @@ void handleNotifications() } } else if (udpIn[0] == 2 && packetSize > 4) //drgb { - uint16_t id = 0; + unsigned id = 0; for (size_t i = 2; i < packetSize -2; i += 3) { setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], 0); @@ -641,7 +641,7 @@ void handleNotifications() } } else if (udpIn[0] == 3 && packetSize > 6) //drgbw { - uint16_t id = 0; + unsigned id = 0; for (size_t i = 2; i < packetSize -3; i += 4) { setRealtimePixel(id, udpIn[i], udpIn[i+1], udpIn[i+2], udpIn[i+3]); @@ -650,7 +650,7 @@ void handleNotifications() } } else if (udpIn[0] == 4 && packetSize > 7) //dnrgb { - uint16_t id = ((udpIn[3] << 0) & 0xFF) + ((udpIn[2] << 8) & 0xFF00); + unsigned id = ((udpIn[3] << 0) & 0xFF) + ((udpIn[2] << 8) & 0xFF00); for (size_t i = 4; i < packetSize -2; i += 3) { if (id >= totalLen) break; @@ -659,7 +659,7 @@ void handleNotifications() } } else if (udpIn[0] == 5 && packetSize > 8) //dnrgbw { - uint16_t id = ((udpIn[3] << 0) & 0xFF) + ((udpIn[2] << 8) & 0xFF00); + unsigned id = ((udpIn[3] << 0) & 0xFF) + ((udpIn[2] << 8) & 0xFF00); for (size_t i = 4; i < packetSize -2; i += 4) { if (id >= totalLen) break; @@ -691,7 +691,7 @@ void handleNotifications() void setRealtimePixel(uint16_t i, byte r, byte g, byte b, byte w) { - uint16_t pix = i + arlsOffset; + unsigned pix = i + arlsOffset; if (pix < strip.getLengthTotal()) { if (!arlsDisableGammaCorrection && gammaCorrectCol) { r = gamma8(r); diff --git a/wled00/util.cpp b/wled00/util.cpp index 1a5c03c27..3834939dc 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -156,7 +156,7 @@ bool oappendi(int i) bool oappend(const char* txt) { - uint16_t len = strlen(txt); + unsigned len = strlen(txt); if ((obuf == nullptr) || (olen + len >= SETTINGS_STACK_BUF_SIZE)) { // sanity checks #ifdef WLED_DEBUG DEBUG_PRINT(F("oappend() buffer overflow. Cannot append ")); @@ -175,7 +175,7 @@ void prepareHostname(char* hostname) { sprintf_P(hostname, PSTR("wled-%*s"), 6, escapedMac.c_str() + 6); const char *pC = serverDescription; - uint8_t pos = 5; // keep "wled-" + unsigned pos = 5; // keep "wled-" while (*pC && pos < 24) { // while !null and not over length if (isalnum(*pC)) { // if the current char is alpha-numeric append it to the hostname hostname[pos] = *pC; @@ -269,9 +269,9 @@ uint8_t extractModeName(uint8_t mode, const char *src, char *dest, uint8_t maxLe return strlen(dest); } - uint8_t qComma = 0; + unsigned qComma = 0; bool insideQuotes = false; - uint8_t printedChars = 0; + unsigned printedChars = 0; char singleJsonSymbol; size_t len = strlen_P(src); @@ -308,11 +308,11 @@ uint8_t extractModeSlider(uint8_t mode, uint8_t slider, char *dest, uint8_t maxL if (mode < strip.getModeCount()) { String lineBuffer = FPSTR(strip.getModeData(mode)); if (lineBuffer.length() > 0) { - int16_t start = lineBuffer.indexOf('@'); - int16_t stop = lineBuffer.indexOf(';', start); + unsigned start = lineBuffer.indexOf('@'); + unsigned stop = lineBuffer.indexOf(';', start); if (start>0 && stop>0) { String names = lineBuffer.substring(start, stop); // include @ - int16_t nameBegin = 1, nameEnd, nameDefault; + unsigned nameBegin = 1, nameEnd, nameDefault; if (slider < 10) { for (size_t i=0; i<=slider; i++) { const char *tmpstr; diff --git a/wled00/wled_serial.cpp b/wled00/wled_serial.cpp index 9cca09db0..3ca7c7f2f 100644 --- a/wled00/wled_serial.cpp +++ b/wled00/wled_serial.cpp @@ -24,7 +24,7 @@ bool continuousSendLED = false; uint32_t lastUpdate = 0; void updateBaudRate(uint32_t rate){ - uint16_t rate100 = rate/100; + unsigned rate100 = rate/100; if (rate100 == currentBaud || rate100 < 96) return; currentBaud = rate100; @@ -39,9 +39,9 @@ void updateBaudRate(uint32_t rate){ // RGB LED data return as JSON array. Slow, but easy to use on the other end. void sendJSON(){ if (!pinManager.isPinAllocated(hardwareTX) || pinManager.getPinOwner(hardwareTX) == PinOwner::DebugOut) { - uint16_t used = strip.getLengthTotal(); + unsigned used = strip.getLengthTotal(); Serial.write('['); - for (uint16_t i=0; i RGB map Serial.write(qadd8(W(c), G(c))); //G From 8632d99341985ec2bc44d41fb8ac92272ad47674 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Tue, 9 Jul 2024 02:22:04 -0400 Subject: [PATCH 014/142] MPU6050: Fix crash when enabling Avoid reconfiguring the device during web server context, which can trigger a yield(). Instead defer the device initialization to loop(). --- usermods/mpu6050_imu/usermod_mpu6050_imu.h | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/usermods/mpu6050_imu/usermod_mpu6050_imu.h b/usermods/mpu6050_imu/usermod_mpu6050_imu.h index 51dd646c7..436d03601 100644 --- a/usermods/mpu6050_imu/usermod_mpu6050_imu.h +++ b/usermods/mpu6050_imu/usermod_mpu6050_imu.h @@ -87,11 +87,11 @@ class MPU6050Driver : public Usermod { int16_t accel_offset[3]; }; config_t config; + bool configDirty = true; // does the configuration need an update? // MPU control/status vars bool irqBound = false; // set true if we have bound the IRQ pin bool dmpReady = false; // set true if DMP init was successful - uint8_t devStatus; // return status after each device operation (0 = success, !0 = error) uint16_t packetSize; // expected DMP packet size (default is 42 bytes) uint16_t fifoCount; // count of all bytes currently in FIFO uint8_t fifoBuffer[64]; // FIFO storage buffer @@ -157,7 +157,10 @@ class MPU6050Driver : public Usermod { um_data.u_type[8] = UMT_UINT32; } + configDirty = false; // we have now accepted the current configuration, success or not + if (!config.enabled) return; + // TODO: notice if these have changed ?? if (i2c_scl<0 || i2c_sda<0) { DEBUG_PRINTLN(F("MPU6050: I2C is no good.")); return; } // Check the interrupt pin if (config.interruptPin >= 0) { @@ -182,7 +185,7 @@ class MPU6050Driver : public Usermod { // load and configure the DMP DEBUG_PRINTLN(F("Initializing DMP...")); - devStatus = mpu.dmpInitialize(); + auto devStatus = mpu.dmpInitialize(); // set offsets (from config) mpu.setXGyroOffset(config.gyro_offset[0]); @@ -241,6 +244,8 @@ class MPU6050Driver : public Usermod { * loop() is called continuously. Here you can check for events, read sensors, etc. */ void loop() { + if (configDirty) setup(); + // if programming failed, don't try to do anything if (!config.enabled || !dmpReady || strip.isUpdating()) return; @@ -407,8 +412,8 @@ class MPU6050Driver : public Usermod { irqBound = false; } - // Just re-init - setup(); + // Re-call setup on the next loop() + configDirty = true; } return configComplete; From f6ed3bc9db60d0602c57c95def1594bde876e4a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niklas=20F=C3=A4th?= Date: Thu, 11 Jul 2024 13:56:33 +0200 Subject: [PATCH 015/142] Fix typo in "Battery" usermod (Build Failed) --- usermods/Battery/usermod_v2_Battery.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h index 35da337e1..f240d55f5 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/usermod_v2_Battery.h @@ -5,7 +5,7 @@ #include "UMBattery.h" #include "types/UnkownUMBattery.h" #include "types/LionUMBattery.h" -#include "types/LiPoUMBattery.h" +#include "types/LipoUMBattery.h" /* * Usermod by Maximilian Mewes From cd1ede38a70ca503d9a4ef675cfc4cab11d424d2 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Thu, 11 Jul 2024 21:22:58 +0200 Subject: [PATCH 016/142] Size & speed optimisations --- usermods/Temperature/usermod_temperature.h | 4 +-- .../usermod_v2_four_line_display_ALT.h | 26 +++++++------- .../usermod_v2_rotary_encoder_ui_ALT.h | 20 +++++------ wled00/FX_fcn.cpp | 6 ++-- wled00/NodeStruct.h | 2 +- wled00/alexa.cpp | 6 ++-- wled00/cfg.cpp | 12 +++---- wled00/dmx.cpp | 2 +- wled00/led.cpp | 4 +-- wled00/ntp.cpp | 2 +- wled00/overlay.cpp | 4 +-- wled00/pin_manager.cpp | 6 ++-- wled00/remote.cpp | 2 +- wled00/um_manager.cpp | 36 +++++++++---------- wled00/util.cpp | 2 +- wled00/xml.cpp | 8 ++--- 16 files changed, 72 insertions(+), 70 deletions(-) diff --git a/usermods/Temperature/usermod_temperature.h b/usermods/Temperature/usermod_temperature.h index 5b6b21d8c..5ac992f95 100644 --- a/usermods/Temperature/usermod_temperature.h +++ b/usermods/Temperature/usermod_temperature.h @@ -113,7 +113,7 @@ float UsermodTemperature::readDallas() { #ifdef WLED_DEBUG if (OneWire::crc8(data,8) != data[8]) { DEBUG_PRINTLN(F("CRC error reading temperature.")); - for (byte i=0; i < 9; i++) DEBUG_PRINTF_P(PSTR("0x%02X "), data[i]); + for (unsigned i=0; i < 9; i++) DEBUG_PRINTF_P(PSTR("0x%02X "), data[i]); DEBUG_PRINT(F(" => ")); DEBUG_PRINTF_P(PSTR("0x%02X\n"), OneWire::crc8(data,8)); } @@ -133,7 +133,7 @@ float UsermodTemperature::readDallas() { break; } } - for (byte i=1; i<9; i++) data[0] &= data[i]; + for (unsigned i=1; i<9; i++) data[0] &= data[i]; return data[0]==0xFF ? -127.0f : retVal; } diff --git a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h index 2cb1507ce..008647fa7 100644 --- a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h +++ b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h @@ -445,8 +445,8 @@ void FourLineDisplayUsermod::setPowerSave(uint8_t save) { void FourLineDisplayUsermod::center(String &line, uint8_t width) { int len = line.length(); - if (len0; i--) line = ' ' + line; - for (byte i=line.length(); i0; i--) line = ' ' + line; + for (unsigned i=line.length(); i127)) { // remove note symbol from effect names - for (byte i=5; i<=printedChars; i++) lineBuffer[i-5] = lineBuffer[i]; //include '\0' + for (unsigned i=5; i<=printedChars; i++) lineBuffer[i-5] = lineBuffer[i]; //include '\0' printedChars -= 5; } if (lineHeight == 2) { // use this code for 8 line display char smallBuffer1[MAX_MODE_LINE_SPACE]; char smallBuffer2[MAX_MODE_LINE_SPACE]; - uint8_t smallChars1 = 0; - uint8_t smallChars2 = 0; + unsigned smallChars1 = 0; + unsigned smallChars2 = 0; if (printedChars < MAX_MODE_LINE_SPACE) { // use big font if the text fits while (printedChars < (MAX_MODE_LINE_SPACE-1)) lineBuffer[printedChars++]=' '; lineBuffer[printedChars] = 0; drawString(1, row*lineHeight, lineBuffer); } else { // for long names divide the text into 2 lines and print them small bool spaceHit = false; - for (uint8_t i = 0; i < printedChars; i++) { + for (unsigned i = 0; i < printedChars; i++) { switch (lineBuffer[i]) { case ' ': if (i > 4 && !spaceHit) { @@ -865,8 +865,8 @@ void FourLineDisplayUsermod::showCurrentEffectOrPalette(int inputEffPal, const c } } else { // use this code for 4 ling displays char smallBuffer3[MAX_MODE_LINE_SPACE+1]; // uses 1x1 icon for mode/palette - uint8_t smallChars3 = 0; - for (uint8_t i = 0; i < MAX_MODE_LINE_SPACE; i++) smallBuffer3[smallChars3++] = (i >= printedChars) ? ' ' : lineBuffer[i]; + unsigned smallChars3 = 0; + for (unsigned i = 0; i < MAX_MODE_LINE_SPACE; i++) smallBuffer3[smallChars3++] = (i >= printedChars) ? ' ' : lineBuffer[i]; smallBuffer3[smallChars3] = 0; drawString(1, row*lineHeight, smallBuffer3, true); } @@ -1265,7 +1265,7 @@ void FourLineDisplayUsermod::addToConfig(JsonObject& root) { bool FourLineDisplayUsermod::readFromConfig(JsonObject& root) { bool needsRedraw = false; DisplayType newType = type; - int8_t oldPin[3]; for (byte i=0; i<3; i++) oldPin[i] = ioPin[i]; + int8_t oldPin[3]; for (unsigned i=0; i<3; i++) oldPin[i] = ioPin[i]; JsonObject top = root[FPSTR(_name)]; if (top.isNull()) { @@ -1276,7 +1276,7 @@ bool FourLineDisplayUsermod::readFromConfig(JsonObject& root) { enabled = top[FPSTR(_enabled)] | enabled; newType = top["type"] | newType; - for (byte i=0; i<3; i++) ioPin[i] = top["pin"][i] | ioPin[i]; + for (unsigned i=0; i<3; i++) ioPin[i] = top["pin"][i] | ioPin[i]; flip = top[FPSTR(_flip)] | flip; contrast = top[FPSTR(_contrast)] | contrast; #ifndef ARDUINO_ARCH_ESP32 @@ -1302,7 +1302,7 @@ bool FourLineDisplayUsermod::readFromConfig(JsonObject& root) { DEBUG_PRINTLN(F(" config (re)loaded.")); // changing parameters from settings page bool pinsChanged = false; - for (byte i=0; i<3; i++) if (ioPin[i] != oldPin[i]) { pinsChanged = true; break; } + for (unsigned i=0; i<3; i++) if (ioPin[i] != oldPin[i]) { pinsChanged = true; break; } if (pinsChanged || type!=newType) { bool isSPI = (type == SSD1306_SPI || type == SSD1306_SPI64 || type == SSD1309_SPI64); bool newSPI = (newType == SSD1306_SPI || newType == SSD1306_SPI64 || newType == SSD1309_SPI64); diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h index e5a5f24f7..5756fbb69 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h @@ -416,7 +416,7 @@ void RotaryEncoderUIUsermod::sortModesAndPalettes() { byte *RotaryEncoderUIUsermod::re_initIndexArray(int numModes) { byte *indexes = (byte *)malloc(sizeof(byte) * numModes); - for (byte i = 0; i < numModes; i++) { + for (unsigned i = 0; i < numModes; i++) { indexes[i] = i; } return indexes; @@ -700,7 +700,7 @@ void RotaryEncoderUIUsermod::findCurrentEffectAndPalette() { effectPaletteIndex = 0; DEBUG_PRINTLN(effectPalette); - for (uint8_t i = 0; i < strip.getPaletteCount()+strip.customPalettes.size(); i++) { + for (unsigned i = 0; i < strip.getPaletteCount()+strip.customPalettes.size(); i++) { if (palettes_alpha_indexes[i] == effectPalette) { effectPaletteIndex = i; DEBUG_PRINTLN(F("Found palette.")); @@ -764,7 +764,7 @@ void RotaryEncoderUIUsermod::changeEffect(bool increase) { effectCurrent = modes_alpha_indexes[effectCurrentIndex]; stateChanged = true; if (applyToAll) { - for (byte i=0; i defNumCounts && defNumCounts > 1 && defNumPins%defNumCounts == 0 ? defNumCounts : defNumPins; + const unsigned defNumCounts = ((sizeof defCounts) / (sizeof defCounts[0])); + // if number of pins is divisible by counts, use number of counts to determine number of buses, otherwise use pins + const unsigned defNumBusses = defNumPins > defNumCounts && defNumPins%defNumCounts == 0 ? defNumCounts : defNumPins; const unsigned pinsPerBus = defNumPins / defNumBusses; unsigned prevLen = 0; for (unsigned i = 0; i < defNumBusses && i < WLED_MAX_BUSSES+WLED_MIN_VIRTUAL_BUSSES; i++) { @@ -1229,6 +1230,7 @@ void WS2812FX::finalizeInit(void) { while (pinManager.isPinAllocated(defPin[0]) && defPin[0] < WLED_NUM_PINS) defPin[0]++; } unsigned start = prevLen; + // if we have less counts than pins and they do not align, use last known count to set current count unsigned count = defCounts[(i < defNumCounts) ? i : defNumCounts -1]; prevLen += count; BusConfig defCfg = BusConfig(DEFAULT_LED_TYPE, defPin, start, count, DEFAULT_LED_COLOR_ORDER, false, 0, RGBW_MODE_MANUAL_ONLY, 0, useGlobalLedBuffer); diff --git a/wled00/NodeStruct.h b/wled00/NodeStruct.h index 2d4d3609b..34f73ab41 100644 --- a/wled00/NodeStruct.h +++ b/wled00/NodeStruct.h @@ -34,7 +34,7 @@ struct NodeStruct NodeStruct() : age(0), nodeType(0), build(0) { - for (uint8_t i = 0; i < 4; ++i) { ip[i] = 0; } + for (unsigned i = 0; i < 4; ++i) { ip[i] = 0; } } }; typedef std::map NodesMap; diff --git a/wled00/alexa.cpp b/wled00/alexa.cpp index 179a522c0..b108f294b 100644 --- a/wled00/alexa.cpp +++ b/wled00/alexa.cpp @@ -25,7 +25,7 @@ void alexaInit() // names are identical as the preset names, switching off can be done by switching off any of them if (alexaNumPresets) { String name = ""; - for (byte presetIndex = 1; presetIndex <= alexaNumPresets; presetIndex++) + for (unsigned presetIndex = 1; presetIndex <= alexaNumPresets; presetIndex++) { if (!getPresetName(presetIndex, name)) break; // no more presets EspalexaDevice* dev = new EspalexaDevice(name.c_str(), onAlexaChange, EspalexaDeviceType::extendedcolor); @@ -64,7 +64,7 @@ void onAlexaChange(EspalexaDevice* dev) } else // switch-on behavior for preset devices { // turn off other preset devices - for (byte i = 1; i < espalexa.getDeviceCount(); i++) + for (unsigned i = 1; i < espalexa.getDeviceCount(); i++) { if (i == dev->getId()) continue; espalexa.getDevice(i)->setValue(0); // turn off other presets @@ -87,7 +87,7 @@ void onAlexaChange(EspalexaDevice* dev) applyPreset(macroAlexaOff, CALL_MODE_ALEXA); // below for loop stops Alexa from complaining if macroAlexaOff does not actually turn off } - for (byte i = 0; i < espalexa.getDeviceCount(); i++) + for (unsigned i = 0; i < espalexa.getDeviceCount(); i++) { espalexa.getDevice(i)->setValue(0); } diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 1fc3f2425..f9a94e228 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -86,7 +86,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(apBehavior, ap[F("behav")]); /* JsonArray ap_ip = ap["ip"]; - for (byte i = 0; i < 4; i++) { + for (unsigned i = 0; i < 4; i++) { apIP[i] = ap_ip; } */ @@ -565,7 +565,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { JsonArray if_hue_ip = if_hue["ip"]; - for (byte i = 0; i < 4; i++) + for (unsigned i = 0; i < 4; i++) CJSON(hueIP[i], if_hue_ip[i]); #endif @@ -793,7 +793,7 @@ void serializeConfig() { ethernet["type"] = ethernetType; if (ethernetType != WLED_ETH_NONE && ethernetType < WLED_NUM_ETH_TYPES) { JsonArray pins = ethernet.createNestedArray("pin"); - for (uint8_t p=0; p=0) pins.add(ethernetBoards[ethernetType].eth_power); if (ethernetBoards[ethernetType].eth_mdc>=0) pins.add(ethernetBoards[ethernetType].eth_mdc); if (ethernetBoards[ethernetType].eth_mdio>=0) pins.add(ethernetBoards[ethernetType].eth_mdio); @@ -1046,7 +1046,7 @@ void serializeConfig() { if_hue_recv["col"] = hueApplyColor; JsonArray if_hue_ip = if_hue.createNestedArray("ip"); - for (byte i = 0; i < 4; i++) { + for (unsigned i = 0; i < 4; i++) { if_hue_ip.add(hueIP[i]); } #endif @@ -1081,7 +1081,7 @@ void serializeConfig() { JsonArray timers_ins = timers.createNestedArray("ins"); - for (byte i = 0; i < 10; i++) { + for (unsigned i = 0; i < 10; i++) { if (timerMacro[i] == 0 && timerHours[i] == 0 && timerMinutes[i] == 0) continue; // sunrise/sunset get saved always (timerHours=255) JsonObject timers_ins0 = timers_ins.createNestedObject(); timers_ins0["en"] = (timerWeekday[i] & 0x01); @@ -1113,7 +1113,7 @@ void serializeConfig() { dmx[F("start-led")] = DMXStartLED; JsonArray dmx_fixmap = dmx.createNestedArray(F("fixmap")); - for (byte i = 0; i < 15; i++) { + for (unsigned i = 0; i < 15; i++) { dmx_fixmap.add(DMXFixtureMap[i]); } diff --git a/wled00/dmx.cpp b/wled00/dmx.cpp index 6bdf80a79..dbe70f2aa 100644 --- a/wled00/dmx.cpp +++ b/wled00/dmx.cpp @@ -22,7 +22,7 @@ void handleDMX() bool calc_brightness = true; // check if no shutter channel is set - for (byte i = 0; i < DMXChannels; i++) + for (unsigned i = 0; i < DMXChannels; i++) { if (DMXFixtureMap[i] == 5) calc_brightness = false; } diff --git a/wled00/led.cpp b/wled00/led.cpp index 704296461..ba6ed2550 100644 --- a/wled00/led.cpp +++ b/wled00/led.cpp @@ -226,7 +226,7 @@ void handleNightlight() nightlightDelayMs = (unsigned)(nightlightDelayMins*60000); nightlightActiveOld = true; briNlT = bri; - for (byte i=0; i<4; i++) colNlT[i] = col[i]; // remember starting color + for (unsigned i=0; i<4; i++) colNlT[i] = col[i]; // remember starting color if (nightlightMode == NL_MODE_SUN) { //save current @@ -251,7 +251,7 @@ void handleNightlight() bri = briNlT + ((nightlightTargetBri - briNlT)*nper); if (nightlightMode == NL_MODE_COLORFADE) // color fading only is enabled with "NF=2" { - for (byte i=0; i<4; i++) col[i] = colNlT[i]+ ((colSec[i] - colNlT[i])*nper); // fading from actual color to secondary color + for (unsigned i=0; i<4; i++) col[i] = colNlT[i]+ ((colSec[i] - colNlT[i])*nper); // fading from actual color to secondary color } colorUpdated(CALL_MODE_NO_NOTIFY); } diff --git a/wled00/ntp.cpp b/wled00/ntp.cpp index e2c99045a..056110a58 100644 --- a/wled00/ntp.cpp +++ b/wled00/ntp.cpp @@ -377,7 +377,7 @@ void checkTimers() if (!hour(localTime) && minute(localTime)==1) calculateSunriseAndSunset(); DEBUG_PRINTF_P(PSTR("Local time: %02d:%02d\n"), hour(localTime), minute(localTime)); - for (uint8_t i = 0; i < 8; i++) + for (unsigned i = 0; i < 8; i++) { if (timerMacro[i] != 0 && (timerWeekday[i] & 0x01) //timer is enabled diff --git a/wled00/overlay.cpp b/wled00/overlay.cpp index d6d8ba52a..239cff528 100644 --- a/wled00/overlay.cpp +++ b/wled00/overlay.cpp @@ -34,7 +34,7 @@ void _overlayAnalogClock() } if (analogClock5MinuteMarks) { - for (byte i = 0; i <= 12; i++) + for (unsigned i = 0; i <= 12; i++) { unsigned pix = analogClock12pixel + roundf((overlaySize / 12.0f) *i); if (pix > overlayMax) pix -= overlaySize; @@ -91,7 +91,7 @@ void handleOverlayDraw() { usermods.handleOverlayDraw(); if (analogClockSolidBlack) { const Segment* segments = strip.getSegments(); - for (uint8_t i = 0; i < strip.getSegmentsNum(); i++) { + for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { const Segment& segment = segments[i]; if (!segment.isActive()) continue; if (segment.mode > 0 || segment.colors[0] > 0) { diff --git a/wled00/pin_manager.cpp b/wled00/pin_manager.cpp index b6dc9e35a..84101e7cf 100644 --- a/wled00/pin_manager.cpp +++ b/wled00/pin_manager.cpp @@ -295,7 +295,7 @@ byte PinManagerClass::allocateLedc(byte channels) { if (channels > MAX_LED_CHANNELS || channels == 0) return 255; byte ca = 0; - for (byte i = 0; i < MAX_LED_CHANNELS; i++) { + for (unsigned i = 0; i < MAX_LED_CHANNELS; i++) { byte by = i >> 3; byte bi = i - 8*by; if (bitRead(ledcAlloc[by], bi)) { //found occupied pin @@ -305,7 +305,7 @@ byte PinManagerClass::allocateLedc(byte channels) } if (ca >= channels) { //enough free channels byte in = (i + 1) - ca; - for (byte j = 0; j < ca; j++) { + for (unsigned j = 0; j < ca; j++) { byte bChan = in + j; byte byChan = bChan >> 3; byte biChan = bChan - 8*byChan; @@ -319,7 +319,7 @@ byte PinManagerClass::allocateLedc(byte channels) void PinManagerClass::deallocateLedc(byte pos, byte channels) { - for (byte j = pos; j < pos + channels; j++) { + for (unsigned j = pos; j < pos + channels; j++) { if (j > MAX_LED_CHANNELS) return; byte by = j >> 3; byte bi = j - 8*by; diff --git a/wled00/remote.cpp b/wled00/remote.cpp index 54cdf31f6..9c8d67d0d 100644 --- a/wled00/remote.cpp +++ b/wled00/remote.cpp @@ -68,7 +68,7 @@ static bool resetNightMode() { static void brightnessUp() { if (nightModeActive()) return; // dumb incremental search is efficient enough for so few items - for (uint8_t index = 0; index < numBrightnessSteps; ++index) { + for (unsigned index = 0; index < numBrightnessSteps; ++index) { if (brightnessSteps[index] > bri) { bri = brightnessSteps[index]; break; diff --git a/wled00/um_manager.cpp b/wled00/um_manager.cpp index e73b66c38..2db29c3cd 100644 --- a/wled00/um_manager.cpp +++ b/wled00/um_manager.cpp @@ -4,57 +4,57 @@ */ //Usermod Manager internals -void UsermodManager::setup() { for (byte i = 0; i < numMods; i++) ums[i]->setup(); } -void UsermodManager::connected() { for (byte i = 0; i < numMods; i++) ums[i]->connected(); } -void UsermodManager::loop() { for (byte i = 0; i < numMods; i++) ums[i]->loop(); } -void UsermodManager::handleOverlayDraw() { for (byte i = 0; i < numMods; i++) ums[i]->handleOverlayDraw(); } -void UsermodManager::appendConfigData() { for (byte i = 0; i < numMods; i++) ums[i]->appendConfigData(); } +void UsermodManager::setup() { for (unsigned i = 0; i < numMods; i++) ums[i]->setup(); } +void UsermodManager::connected() { for (unsigned i = 0; i < numMods; i++) ums[i]->connected(); } +void UsermodManager::loop() { for (unsigned i = 0; i < numMods; i++) ums[i]->loop(); } +void UsermodManager::handleOverlayDraw() { for (unsigned i = 0; i < numMods; i++) ums[i]->handleOverlayDraw(); } +void UsermodManager::appendConfigData() { for (unsigned i = 0; i < numMods; i++) ums[i]->appendConfigData(); } bool UsermodManager::handleButton(uint8_t b) { bool overrideIO = false; - for (byte i = 0; i < numMods; i++) { + for (unsigned i = 0; i < numMods; i++) { if (ums[i]->handleButton(b)) overrideIO = true; } return overrideIO; } bool UsermodManager::getUMData(um_data_t **data, uint8_t mod_id) { - for (byte i = 0; i < numMods; i++) { + for (unsigned i = 0; i < numMods; i++) { if (mod_id > 0 && ums[i]->getId() != mod_id) continue; // only get data form requested usermod if provided if (ums[i]->getUMData(data)) return true; // if usermod does provide data return immediately (only one usermod can provide data at one time) } return false; } -void UsermodManager::addToJsonState(JsonObject& obj) { for (byte i = 0; i < numMods; i++) ums[i]->addToJsonState(obj); } -void UsermodManager::addToJsonInfo(JsonObject& obj) { for (byte i = 0; i < numMods; i++) ums[i]->addToJsonInfo(obj); } -void UsermodManager::readFromJsonState(JsonObject& obj) { for (byte i = 0; i < numMods; i++) ums[i]->readFromJsonState(obj); } -void UsermodManager::addToConfig(JsonObject& obj) { for (byte i = 0; i < numMods; i++) ums[i]->addToConfig(obj); } +void UsermodManager::addToJsonState(JsonObject& obj) { for (unsigned i = 0; i < numMods; i++) ums[i]->addToJsonState(obj); } +void UsermodManager::addToJsonInfo(JsonObject& obj) { for (unsigned i = 0; i < numMods; i++) ums[i]->addToJsonInfo(obj); } +void UsermodManager::readFromJsonState(JsonObject& obj) { for (unsigned i = 0; i < numMods; i++) ums[i]->readFromJsonState(obj); } +void UsermodManager::addToConfig(JsonObject& obj) { for (unsigned i = 0; i < numMods; i++) ums[i]->addToConfig(obj); } bool UsermodManager::readFromConfig(JsonObject& obj) { bool allComplete = true; - for (byte i = 0; i < numMods; i++) { + for (unsigned i = 0; i < numMods; i++) { if (!ums[i]->readFromConfig(obj)) allComplete = false; } return allComplete; } #ifndef WLED_DISABLE_MQTT -void UsermodManager::onMqttConnect(bool sessionPresent) { for (byte i = 0; i < numMods; i++) ums[i]->onMqttConnect(sessionPresent); } +void UsermodManager::onMqttConnect(bool sessionPresent) { for (unsigned i = 0; i < numMods; i++) ums[i]->onMqttConnect(sessionPresent); } bool UsermodManager::onMqttMessage(char* topic, char* payload) { - for (byte i = 0; i < numMods; i++) if (ums[i]->onMqttMessage(topic, payload)) return true; + for (unsigned i = 0; i < numMods; i++) if (ums[i]->onMqttMessage(topic, payload)) return true; return false; } #endif #ifndef WLED_DISABLE_ESPNOW bool UsermodManager::onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t len) { - for (byte i = 0; i < numMods; i++) if (ums[i]->onEspNowMessage(sender, payload, len)) return true; + for (unsigned i = 0; i < numMods; i++) if (ums[i]->onEspNowMessage(sender, payload, len)) return true; return false; } #endif -void UsermodManager::onUpdateBegin(bool init) { for (byte i = 0; i < numMods; i++) ums[i]->onUpdateBegin(init); } // notify usermods that update is to begin -void UsermodManager::onStateChange(uint8_t mode) { for (byte i = 0; i < numMods; i++) ums[i]->onStateChange(mode); } // notify usermods that WLED state changed +void UsermodManager::onUpdateBegin(bool init) { for (unsigned i = 0; i < numMods; i++) ums[i]->onUpdateBegin(init); } // notify usermods that update is to begin +void UsermodManager::onStateChange(uint8_t mode) { for (unsigned i = 0; i < numMods; i++) ums[i]->onStateChange(mode); } // notify usermods that WLED state changed /* * Enables usermods to lookup another Usermod. */ Usermod* UsermodManager::lookup(uint16_t mod_id) { - for (byte i = 0; i < numMods; i++) { + for (unsigned i = 0; i < numMods; i++) { if (ums[i]->getId() == mod_id) { return ums[i]; } diff --git a/wled00/util.cpp b/wled00/util.cpp index 3834939dc..6bc02234b 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -197,7 +197,7 @@ void prepareHostname(char* hostname) bool isAsterisksOnly(const char* str, byte maxLen) { - for (byte i = 0; i < maxLen; i++) { + for (unsigned i = 0; i < maxLen; i++) { if (str[i] == 0) break; if (str[i] != '*') return false; } diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 255402cea..7da301715 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -168,7 +168,7 @@ void appendGPIOinfo() { #ifdef WLED_USE_ETHERNET if (ethernetType != WLED_ETH_NONE && ethernetType < WLED_NUM_ETH_TYPES) { - for (uint8_t p=0; p=0) { oappend(","); oappend(itoa(ethernetBoards[ethernetType].eth_power,nS,10)); } if (ethernetBoards[ethernetType].eth_mdc>=0) { oappend(","); oappend(itoa(ethernetBoards[ethernetType].eth_mdc,nS,10)); } if (ethernetBoards[ethernetType].eth_mdio>=0) { oappend(","); oappend(itoa(ethernetBoards[ethernetType].eth_mdio,nS,10)); } @@ -632,7 +632,7 @@ void getSettingsJS(byte subPage, char* dest) sappend('v',SET_F("A1"),macroAlexaOff); sappend('v',SET_F("MC"),macroCountdown); sappend('v',SET_F("MN"),macroNl); - for (uint8_t i=0; i Date: Fri, 12 Jul 2024 16:59:08 +0200 Subject: [PATCH 017/142] removed audioreactive usermod build flag --- platformio.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 641f4baf3..fe8b3a278 100644 --- a/platformio.ini +++ b/platformio.ini @@ -196,7 +196,6 @@ build_flags = ; decrease code cache size and increase IRAM to fit all pixel functions -D PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48 ;; in case of linker errors like "section `.text1' will not fit in region `iram1_0_seg'" ; -D PIO_FRAMEWORK_ARDUINO_MMU_CACHE16_IRAM48_SECHEAP_SHARED ;; (experimental) adds some extra heap, but may cause slowdown - -D USERMOD_AUDIOREACTIVE lib_deps = #https://github.com/lorol/LITTLEFS.git From 3a8e19d1b49d7d5210afa496a2041d60bd4189ba Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 12 Jul 2024 22:09:52 +0200 Subject: [PATCH 018/142] audiosync receive improvements (maintainer edit) * fixed a few typo's in comments * fixed 8266 specific warning about 'comparison of integer expressions of different signedness' based on recommendations made by @willmmiles: * make sure that audioSyncPacket is the same size (44bytes) on all platforms * use static buffer for receiving (avoids heap fragmentation) * copy receive buffer to local audioSyncPacket struct - avoids alignment problems * esp32 only: to stay in sync with UDP, Udp.flush() is needed when Udp.parsePacket() is _not_ followed by Udp.read() --- usermods/audioreactive/audio_reactive.h | 64 ++++++++++++++----------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/usermods/audioreactive/audio_reactive.h b/usermods/audioreactive/audio_reactive.h index a718011fe..fe946db9b 100644 --- a/usermods/audioreactive/audio_reactive.h +++ b/usermods/audioreactive/audio_reactive.h @@ -76,7 +76,7 @@ static uint8_t soundAgc = 0; // Automagic gain control: 0 - n static float FFT_MajorPeak = 1.0f; // FFT: strongest (peak) frequency static float FFT_Magnitude = 0.0f; // FFT: volume (magnitude) of peak frequency static bool samplePeak = false; // Boolean flag for peak - used in effects. Responding routine may reset this flag. Auto-reset after strip.getMinShowDelay() -static bool udpSamplePeak = false; // Boolean flag for peak. Set at the same tiem as samplePeak, but reset by transmitAudioData +static bool udpSamplePeak = false; // Boolean flag for peak. Set at the same time as samplePeak, but reset by transmitAudioData static unsigned long timeOfPeak = 0; // time of last sample peak detection. static uint8_t fftResult[NUM_GEQ_CHANNELS]= {0};// Our calculated freq. channel result table to be used by effects @@ -94,7 +94,7 @@ static uint16_t decayTime = 1400; // int: decay time in milliseconds // peak detection #ifdef ARDUINO_ARCH_ESP32 -static void detectSamplePeak(void); // peak detection function (needs scaled FFT reasults in vReal[]) - no used for 8266 receive-only mode +static void detectSamplePeak(void); // peak detection function (needs scaled FFT results in vReal[]) - no used for 8266 receive-only mode #endif static void autoResetPeak(void); // peak auto-reset function static uint8_t maxVol = 31; // (was 10) Reasonable value for constant volume for 'peak detector', as it won't always trigger (deprecated) @@ -587,19 +587,21 @@ class AudioReactive : public Usermod { #endif #endif - // new "V2" audiosync struct - 40 Bytes - struct audioSyncPacket { - char header[6]; // 06 Bytes - float sampleRaw; // 04 Bytes - either "sampleRaw" or "rawSampleAgc" depending on soundAgc setting - float sampleSmth; // 04 Bytes - either "sampleAvg" or "sampleAgc" depending on soundAgc setting - uint8_t samplePeak; // 01 Bytes - 0 no peak; >=1 peak detected. In future, this will also provide peak Magnitude - uint8_t reserved1; // 01 Bytes - for future extensions - not used yet - uint8_t fftResult[16]; // 16 Bytes - float FFT_Magnitude; // 04 Bytes - float FFT_MajorPeak; // 04 Bytes + // new "V2" audiosync struct - 44 Bytes + struct __attribute__ ((packed)) audioSyncPacket { // "packed" ensures that there are no additional gaps + char header[6]; // 06 Bytes offset 0 + uint8_t reserved1[2]; // 02 Bytes, offset 6 - gap required by the compiler - not used yet + float sampleRaw; // 04 Bytes offset 8 - either "sampleRaw" or "rawSampleAgc" depending on soundAgc setting + float sampleSmth; // 04 Bytes offset 12 - either "sampleAvg" or "sampleAgc" depending on soundAgc setting + uint8_t samplePeak; // 01 Bytes offset 16 - 0 no peak; >=1 peak detected. In future, this will also provide peak Magnitude + uint8_t reserved2; // 01 Bytes offset 17 - for future extensions - not used yet + uint8_t fftResult[16]; // 16 Bytes offset 18 + uint16_t reserved3; // 02 Bytes, offset 34 - gap required by the compiler - not used yet + float FFT_Magnitude; // 04 Bytes offset 36 + float FFT_MajorPeak; // 04 Bytes offset 40 }; - // old "V1" audiosync struct - 83 Bytes - for backwards compatibility + // old "V1" audiosync struct - 83 Bytes payload, 88 bytes total (with padding added by compiler) - for backwards compatibility struct audioSyncPacket_v1 { char header[6]; // 06 Bytes uint8_t myVals[32]; // 32 Bytes @@ -612,6 +614,8 @@ class AudioReactive : public Usermod { double FFT_MajorPeak; // 08 Bytes }; + #define UDPSOUND_MAX_PACKET 88 // max packet size for audiosync + // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer) #ifdef UM_AUDIOREACTIVE_ENABLE bool enabled = true; @@ -997,7 +1001,6 @@ class AudioReactive : public Usermod { transmitData.sampleSmth = (soundAgc) ? sampleAgc : sampleAvg; transmitData.samplePeak = udpSamplePeak ? 1:0; udpSamplePeak = false; // Reset udpSamplePeak after we've transmitted it - transmitData.reserved1 = 0; for (int i = 0; i < NUM_GEQ_CHANNELS; i++) { transmitData.fftResult[i] = (uint8_t)constrain(fftResult[i], 0, 254); @@ -1023,10 +1026,13 @@ class AudioReactive : public Usermod { } void decodeAudioData(int packetSize, uint8_t *fftBuff) { - audioSyncPacket *receivedPacket = reinterpret_cast(fftBuff); + audioSyncPacket receivedPacket; + memset(&receivedPacket, 0, sizeof(receivedPacket)); // start clean + memcpy(&receivedPacket, fftBuff, min((unsigned)packetSize, (unsigned)sizeof(receivedPacket))); // don't violate alignment - thanks @willmmiles# + // update samples for effects - volumeSmth = fmaxf(receivedPacket->sampleSmth, 0.0f); - volumeRaw = fmaxf(receivedPacket->sampleRaw, 0.0f); + volumeSmth = fmaxf(receivedPacket.sampleSmth, 0.0f); + volumeRaw = fmaxf(receivedPacket.sampleRaw, 0.0f); #ifdef ARDUINO_ARCH_ESP32 // update internal samples sampleRaw = volumeRaw; @@ -1039,15 +1045,15 @@ class AudioReactive : public Usermod { // If it's true already, then the animation still needs to respond. autoResetPeak(); if (!samplePeak) { - samplePeak = receivedPacket->samplePeak >0 ? true:false; + samplePeak = receivedPacket.samplePeak >0 ? true:false; if (samplePeak) timeOfPeak = millis(); //userVar1 = samplePeak; } //These values are only computed by ESP32 - for (int i = 0; i < NUM_GEQ_CHANNELS; i++) fftResult[i] = receivedPacket->fftResult[i]; - my_magnitude = fmaxf(receivedPacket->FFT_Magnitude, 0.0f); + for (int i = 0; i < NUM_GEQ_CHANNELS; i++) fftResult[i] = receivedPacket.fftResult[i]; + my_magnitude = fmaxf(receivedPacket.FFT_Magnitude, 0.0f); FFT_Magnitude = my_magnitude; - FFT_MajorPeak = constrain(receivedPacket->FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects + FFT_MajorPeak = constrain(receivedPacket.FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects } void decodeAudioData_v1(int packetSize, uint8_t *fftBuff) { @@ -1084,9 +1090,12 @@ class AudioReactive : public Usermod { bool haveFreshData = false; size_t packetSize = fftUdp.parsePacket(); - if (packetSize > 5) { +#ifdef ARDUINO_ARCH_ESP32 + if ((packetSize > 0) && ((packetSize < 5) || (packetSize > UDPSOUND_MAX_PACKET))) fftUdp.flush(); // discard invalid packets (too small or too big) - only works on esp32 +#endif + if ((packetSize > 5) && (packetSize <= UDPSOUND_MAX_PACKET)) { //DEBUGSR_PRINTLN("Received UDP Sync Packet"); - uint8_t fftBuff[packetSize]; + static uint8_t fftBuff[UDPSOUND_MAX_PACKET+1] = { 0 }; // static buffer for receiving, to reuse the same memory and avoid heap fragmentation fftUdp.read(fftBuff, packetSize); // VERIFY THAT THIS IS A COMPATIBLE PACKET @@ -1229,7 +1238,7 @@ class AudioReactive : public Usermod { if (!audioSource) enabled = false; // audio failed to initialise #endif - if (enabled) onUpdateBegin(false); // create FFT task, and initailize network + if (enabled) onUpdateBegin(false); // create FFT task, and initialize network #ifdef ARDUINO_ARCH_ESP32 @@ -1243,7 +1252,7 @@ class AudioReactive : public Usermod { disableSoundProcessing = true; } #endif - if (enabled) disableSoundProcessing = false; // all good - enable audio processing + if (enabled) disableSoundProcessing = false; // all good - enable audio processing if (enabled) connectUDPSoundSync(); if (enabled && addPalettes) createAudioPalettes(); initDone = true; @@ -1803,7 +1812,6 @@ class AudioReactive : public Usermod { dynLim[F("rise")] = attackTime; dynLim[F("fall")] = decayTime; - JsonObject sync = top.createNestedObject("sync"); sync["port"] = audioSyncPort; sync["mode"] = audioSyncEnabled; @@ -2008,8 +2016,8 @@ CRGB AudioReactive::getCRGBForBand(int x, int pal) { void AudioReactive::fillAudioPalettes() { if (!palettes) return; size_t lastCustPalette = strip.customPalettes.size(); - if (lastCustPalette >= palettes) lastCustPalette -= palettes; - for (size_t pal=0; pal= palettes) lastCustPalette -= palettes; + for (int pal=0; pal Date: Fri, 12 Jul 2024 19:16:31 -0400 Subject: [PATCH 019/142] ESP8266PWM: Annotate sources Add additional clarification as to the original source URL and the specific local patches. --- lib/ESP8266PWM/src/core_esp8266_waveform_pwm.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/ESP8266PWM/src/core_esp8266_waveform_pwm.cpp b/lib/ESP8266PWM/src/core_esp8266_waveform_pwm.cpp index c76ee174d..78c7160d9 100644 --- a/lib/ESP8266PWM/src/core_esp8266_waveform_pwm.cpp +++ b/lib/ESP8266PWM/src/core_esp8266_waveform_pwm.cpp @@ -1,6 +1,8 @@ /* esp8266_waveform imported from platform source code Modified for WLED to work around a fault in the NMI handling, which can result in the system locking up and hard WDT crashes. + + Imported from https://github.com/esp8266/Arduino/blob/7e0d20e2b9034994f573a236364e0aef17fd66de/cores/esp8266/core_esp8266_waveform_pwm.cpp */ /* @@ -497,6 +499,7 @@ static inline IRAM_ATTR uint32_t earliest(uint32_t a, uint32_t b) { return (da < db) ? a : b; } +// ----- @willmmiles begin patch ----- // NMI crash workaround // Sometimes the NMI fails to return, stalling the CPU. When this happens, // the next NMI gets a return address /inside the NMI handler function/. @@ -519,6 +522,7 @@ static inline IRAM_ATTR void nmiCrashWorkaround() { __asm__ __volatile__("wsr %0,epc3; wsr %1,eps3"::"a"(epc3_backup),"a"(eps3_backup)); } } +// ----- @willmmiles end patch ----- // The SDK and hardware take some time to actually get to our NMI code, so @@ -540,7 +544,9 @@ static inline IRAM_ATTR void nmiCrashWorkaround() { #define MINIRQTIME microsecondsToClockCycles(6) static IRAM_ATTR void timer1Interrupt() { + // ----- @willmmiles begin patch ----- nmiCrashWorkaround(); + // ----- @willmmiles end patch ----- // Flag if the core is at 160 MHz, for use by adjust() bool turbo = (*(uint32_t*)0x3FF00014) & 1 ? true : false; From 2e266ec945b1e51b22f8668a7d31b32184b58776 Mon Sep 17 00:00:00 2001 From: Frank Date: Sat, 13 Jul 2024 09:55:59 +0200 Subject: [PATCH 020/142] use fixes-size stack buffer ... protected against array overflow due to previous "if (packetSize <= UDPSOUND_MAX_PACKET)" --- usermods/audioreactive/audio_reactive.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usermods/audioreactive/audio_reactive.h b/usermods/audioreactive/audio_reactive.h index fe946db9b..088ac880b 100644 --- a/usermods/audioreactive/audio_reactive.h +++ b/usermods/audioreactive/audio_reactive.h @@ -1095,7 +1095,7 @@ class AudioReactive : public Usermod { #endif if ((packetSize > 5) && (packetSize <= UDPSOUND_MAX_PACKET)) { //DEBUGSR_PRINTLN("Received UDP Sync Packet"); - static uint8_t fftBuff[UDPSOUND_MAX_PACKET+1] = { 0 }; // static buffer for receiving, to reuse the same memory and avoid heap fragmentation + uint8_t fftBuff[UDPSOUND_MAX_PACKET+1] = { 0 }; // fixed-size buffer for receiving (stack), to avoid heap fragmentation caused by variable sized arrays fftUdp.read(fftBuff, packetSize); // VERIFY THAT THIS IS A COMPATIBLE PACKET From ac503ef72e065e7b37495404dde9642b3112ff54 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 17 Jul 2024 18:54:33 +0200 Subject: [PATCH 021/142] adding boot-up delay define use -D WLED_BOOTUPDELAY=500 in platformio env definition to add 500ms of delay before hardware init. --- wled00/wled.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/wled00/wled.cpp b/wled00/wled.cpp index f8aa94c49..a6143eee6 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -343,6 +343,9 @@ void WLED::setup() #ifdef ARDUINO_ARCH_ESP32 pinMode(hardwareRX, INPUT_PULLDOWN); delay(1); // suppress noise in case RX pin is floating (at low noise energy) - see issue #3128 #endif + #ifdef WLED_BOOTUPDELAY + delay(WLED_BOOTUPDELAY); // delay to let voltage stabilize, helps with boot issues on some setups + #endif Serial.begin(115200); #if !ARDUINO_USB_CDC_ON_BOOT Serial.setTimeout(50); // this causes troubles on new MCUs that have a "virtual" USB Serial (HWCDC) From b71467b9bebc71a99811a570d65ae732327f0a33 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Wed, 17 Jul 2024 22:24:08 +0200 Subject: [PATCH 022/142] LED settings remove output bugfix --- wled00/data/settings_leds.htm | 1 + 1 file changed, 1 insertion(+) diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 2ce5be148..1712b360e 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -484,6 +484,7 @@ mA/LED:
`; } } - function pinDropdowns() { + function pinDD() { for (let i of d.Sf.elements) { if (i.type === "number" && (i.name.includes("pin") || ["SDA","SCL","MOSI","MISO","SCLK"].includes(i.name))) { //select all pin select elements let v = parseInt(i.value); - let sel = addDropdown(i.name,0); + let sel = addDD(i.name,0); for (var j = -1; j <= d.max_gpio; j++) { if (d.rsvd.includes(j)) continue; let foundPin = pins.indexOf(j); let txt = (j === -1) ? "unused" : `${j}`; if (foundPin >= 0 && j !== v) txt += ` ${pinO[foundPin]=="if"?"global":pinO[foundPin]}`; // already reserved pin if (d.ro_gpio.includes(j)) txt += " (R/O)"; - let opt = addOption(sel, txt, j); + let opt = addO(sel, txt, j); if (j === v) opt.selected = true; // this is "our" pin else if (pins.includes(j)) opt.disabled = true; // someone else's pin } let um = i.name.split(":")[0]; d.extra.forEach((o)=>{ if (o[um] && o[um].pin) o[um].pin.forEach((e)=>{ - let opt = addOption(sel,e[0],e[1]); + let opt = addO(sel,e[0],e[1]); if (e[1]==v) opt.selected = true; }); }); @@ -219,7 +219,7 @@ } } // https://stackoverflow.com/questions/39729741/javascript-change-input-text-to-select-option - function addDropdown(um,fld) { + function addDD(um,fld) { let sel = d.createElement('select'); if (typeof(fld) === "string") { // parameter from usermod (field name) if (fld.includes("pin")) sel.classList.add("pin"); @@ -255,7 +255,8 @@ } return null; } - function addOption(sel,txt,val) { + var addDropdown = addDD; // backwards compatibility + function addO(sel,txt,val) { if (sel===null) return; // select object missing let opt = d.createElement("option"); opt.value = val; @@ -267,8 +268,9 @@ } return opt; } + var addOption = addO; // backwards compatibility // https://stackoverflow.com/questions/26440494/insert-text-after-this-input-element-with-javascript - function addInfo(name,el,txt, txt2="") { + function addI(name,el,txt, txt2="") { let obj = d.getElementsByName(name); if (!obj.length) return; if (typeof el === "string" && obj[0]) obj[0].placeholder = el; @@ -277,9 +279,10 @@ if (txt2!="") obj[el].insertAdjacentHTML('beforebegin', txt2 + ' '); //add pre texts } } + var addInfo = addI; // backwards compatibility // add Help Button function addHB(um) { - addInfo(um + ':help',0,``); + addI(um + ':help',0,``); } // load settings and insert values into DOM function ldS() { diff --git a/wled00/set.cpp b/wled00/set.cpp index 13295df21..3dd226e00 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -860,20 +860,19 @@ bool handleSet(AsyncWebServerRequest *request, const String& req, bool apply) uint16_t spcI = selseg.spacing; pos = req.indexOf(F("&S=")); //segment start if (pos > 0) { - startI = getNumVal(&req, pos); + startI = std::abs(getNumVal(&req, pos)); } pos = req.indexOf(F("S2=")); //segment stop if (pos > 0) { - stopI = getNumVal(&req, pos); + stopI = std::abs(getNumVal(&req, pos)); } pos = req.indexOf(F("GP=")); //segment grouping if (pos > 0) { - grpI = getNumVal(&req, pos); - if (grpI == 0) grpI = 1; + grpI = std::max(1,getNumVal(&req, pos)); } pos = req.indexOf(F("SP=")); //segment spacing if (pos > 0) { - spcI = getNumVal(&req, pos); + spcI = std::max(0,getNumVal(&req, pos)); } strip.setSegment(selectedSeg, startI, stopI, grpI, spcI, UINT16_MAX, startY, stopY); diff --git a/wled00/util.cpp b/wled00/util.cpp index 6bc02234b..fc19d60bd 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -148,7 +148,7 @@ void sappends(char stype, const char* key, char* val) bool oappendi(int i) { - char s[11]; + char s[12]; // 32bit signed number can have 10 digits plus - sign sprintf(s, "%d", i); return oappend(s); } From d234b4b0f1b96a093e2592cbad131c5923db63dd Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Mon, 5 Aug 2024 20:56:12 +0200 Subject: [PATCH 046/142] SM16825 support - add WW & CW swapping --- wled00/bus_manager.h | 5 +- wled00/bus_wrapper.h | 93 ++++++++++++++++++++++++++++++++--- wled00/const.h | 1 + wled00/data/settings_leds.htm | 12 +++-- 4 files changed, 97 insertions(+), 14 deletions(-) diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index e1bacd555..5e516d2e1 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -157,7 +157,7 @@ class Bus { static bool hasWhite(uint8_t type) { if ((type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || type == TYPE_SK6812_RGBW || type == TYPE_TM1814 || type == TYPE_UCS8904 || - type == TYPE_FW1906 || type == TYPE_WS2805) return true; // digital types with white channel + type == TYPE_FW1906 || type == TYPE_WS2805 || type == TYPE_SM16825) return true; // digital types with white channel if (type > TYPE_ONOFF && type <= TYPE_ANALOG_5CH && type != TYPE_ANALOG_3CH) return true; // analog types with white channel if (type == TYPE_NET_DDP_RGBW || type == TYPE_NET_ARTNET_RGBW) return true; // network types with white channel return false; @@ -166,7 +166,8 @@ class Bus { static bool hasCCT(uint8_t type) { if (type == TYPE_WS2812_2CH_X3 || type == TYPE_WS2812_WWA || type == TYPE_ANALOG_2CH || type == TYPE_ANALOG_5CH || - type == TYPE_FW1906 || type == TYPE_WS2805 ) return true; + type == TYPE_FW1906 || type == TYPE_WS2805 || + type == TYPE_SM16825) return true; return false; } static inline int16_t getCCT() { return _cct; } diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index d619e85af..d1d4f8941 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -84,6 +84,11 @@ #define I_8266_U1_TM1914_3 100 #define I_8266_DM_TM1914_3 101 #define I_8266_BB_TM1914_3 102 +//SM16825 (RGBCW) +#define I_8266_U0_SM16825_5 103 +#define I_8266_U1_SM16825_5 104 +#define I_8266_DM_SM16825_5 105 +#define I_8266_BB_SM16825_5 106 /*** ESP32 Neopixel methods ***/ //RGB @@ -130,7 +135,10 @@ #define I_32_RN_TM1914_3 96 #define I_32_I0_TM1914_3 97 #define I_32_I1_TM1914_3 98 - +//SM16825 (RGBCW) +#define I_32_RN_SM16825_5 107 +#define I_32_I0_SM16825_5 108 +#define I_32_I1_SM16825_5 109 //APA102 #define I_HS_DOT_3 39 //hardware SPI @@ -213,6 +221,11 @@ #define B_8266_U1_TM1914_3 NeoPixelBusLg #define B_8266_DM_TM1914_3 NeoPixelBusLg #define B_8266_BB_TM1914_3 NeoPixelBusLg +//Sm16825 (RGBWC) +#define B_8266_U0_SM16825_5 NeoPixelBusLg +#define B_8266_U1_SM16825_5 NeoPixelBusLg +#define B_8266_DM_SM16825_5 NeoPixelBusLg +#define B_8266_BB_SM16825_5 NeoPixelBusLg #endif /*** ESP32 Neopixel methods ***/ @@ -272,6 +285,11 @@ #define B_32_I0_TM1914_3 NeoPixelBusLg #define B_32_I1_TM1914_3 NeoPixelBusLg #define B_32_I1_TM1914_3P NeoPixelBusLg // parallel I2S +//Sm16825 (RGBWC) +#define B_32_RN_SM16825_5 NeoPixelBusLg +#define B_32_I0_SM16825_5 NeoPixelBusLg +#define B_32_I1_SM16825_5 NeoPixelBusLg +#define B_32_I1_SM16825_5P NeoPixelBusLg // parallel I2S #endif //APA102 @@ -398,6 +416,10 @@ class PolyBus { case I_8266_U1_TM1914_3: beginTM1914(busPtr); break; case I_8266_DM_TM1914_3: beginTM1914(busPtr); break; case I_8266_BB_TM1914_3: beginTM1914(busPtr); break; + case I_8266_U0_SM16825_5: (static_cast(busPtr))->Begin(); break; + case I_8266_U1_SM16825_5: (static_cast(busPtr))->Begin(); break; + case I_8266_DM_SM16825_5: (static_cast(busPtr))->Begin(); break; + case I_8266_BB_SM16825_5: (static_cast(busPtr))->Begin(); break; #endif #ifdef ARDUINO_ARCH_ESP32 // RMT buses @@ -412,6 +434,7 @@ class PolyBus { case I_32_RN_APA106_3: (static_cast(busPtr))->Begin(); break; case I_32_RN_2805_5: (static_cast(busPtr))->Begin(); break; case I_32_RN_TM1914_3: beginTM1914(busPtr); break; + case I_32_RN_SM16825_5: (static_cast(busPtr))->Begin(); break; // I2S1 bus or parellel buses #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_3: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; @@ -425,6 +448,7 @@ class PolyBus { case I_32_I1_APA106_3: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; case I_32_I1_2805_5: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; case I_32_I1_TM1914_3: if (useParallelI2S) beginTM1914(busPtr); else beginTM1914(busPtr); break; + case I_32_I1_SM16825_5: if (useParallelI2S) (static_cast(busPtr))->Begin(); else (static_cast(busPtr))->Begin(); break; #endif // I2S0 bus #ifndef WLED_NO_I2S0_PIXELBUS @@ -439,6 +463,7 @@ class PolyBus { case I_32_I0_APA106_3: (static_cast(busPtr))->Begin(); break; case I_32_I0_2805_5: (static_cast(busPtr))->Begin(); break; case I_32_I0_TM1914_3: beginTM1914(busPtr); break; + case I_32_I0_SM16825_5: (static_cast(busPtr))->Begin(); break; #endif // ESP32 can (and should, to avoid inadvertantly driving the chip select signal) specify the pins used for SPI, but only in begin() case I_HS_DOT_3: beginDotStar(busPtr, pins[1], -1, pins[0], -1, clock_kHz); break; @@ -510,6 +535,10 @@ class PolyBus { case I_8266_U1_TM1914_3: busPtr = new B_8266_U1_TM1914_3(len, pins[0]); break; case I_8266_DM_TM1914_3: busPtr = new B_8266_DM_TM1914_3(len, pins[0]); break; case I_8266_BB_TM1914_3: busPtr = new B_8266_BB_TM1914_3(len, pins[0]); break; + case I_8266_U0_SM16825_5: busPtr = new B_8266_U0_SM16825_5(len, pins[0]); break; + case I_8266_U1_SM16825_5: busPtr = new B_8266_U1_SM16825_5(len, pins[0]); break; + case I_8266_DM_SM16825_5: busPtr = new B_8266_DM_SM16825_5(len, pins[0]); break; + case I_8266_BB_SM16825_5: busPtr = new B_8266_BB_SM16825_5(len, pins[0]); break; #endif #ifdef ARDUINO_ARCH_ESP32 // RMT buses @@ -524,6 +553,7 @@ class PolyBus { case I_32_RN_FW6_5: busPtr = new B_32_RN_FW6_5(len, pins[0], (NeoBusChannel)channel); break; case I_32_RN_2805_5: busPtr = new B_32_RN_2805_5(len, pins[0], (NeoBusChannel)channel); break; case I_32_RN_TM1914_3: busPtr = new B_32_RN_TM1914_3(len, pins[0], (NeoBusChannel)channel); break; + case I_32_RN_SM16825_5: busPtr = new B_32_RN_SM16825_5(len, pins[0], (NeoBusChannel)channel); break; // I2S1 bus or paralell buses #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_3: if (useParallelI2S) busPtr = new B_32_I1_NEO_3P(len, pins[0]); else busPtr = new B_32_I1_NEO_3(len, pins[0]); break; @@ -537,6 +567,7 @@ class PolyBus { case I_32_I1_FW6_5: if (useParallelI2S) busPtr = new B_32_I1_FW6_5P(len, pins[0]); else busPtr = new B_32_I1_FW6_5(len, pins[0]); break; case I_32_I1_2805_5: if (useParallelI2S) busPtr = new B_32_I1_2805_5P(len, pins[0]); else busPtr = new B_32_I1_2805_5(len, pins[0]); break; case I_32_I1_TM1914_3: if (useParallelI2S) busPtr = new B_32_I1_TM1914_3P(len, pins[0]); else busPtr = new B_32_I1_TM1914_3(len, pins[0]); break; + case I_32_I1_SM16825_5: if (useParallelI2S) busPtr = new B_32_I1_SM16825_5P(len, pins[0]); else busPtr = new B_32_I1_SM16825_5(len, pins[0]); break; #endif // I2S0 bus #ifndef WLED_NO_I2S0_PIXELBUS @@ -551,6 +582,7 @@ class PolyBus { case I_32_I0_FW6_5: busPtr = new B_32_I0_FW6_5(len, pins[0]); break; case I_32_I0_2805_5: busPtr = new B_32_I0_2805_5(len, pins[0]); break; case I_32_I0_TM1914_3: busPtr = new B_32_I0_TM1914_3(len, pins[0]); break; + case I_32_I0_SM16825_5: busPtr = new B_32_I0_SM16825_5(len, pins[0]); break; #endif #endif // for 2-wire: pins[1] is clk, pins[0] is dat. begin expects (len, clk, dat) @@ -617,6 +649,10 @@ class PolyBus { case I_8266_U1_TM1914_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_DM_TM1914_3: (static_cast(busPtr))->Show(consistent); break; case I_8266_BB_TM1914_3: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U0_SM16825_5: (static_cast(busPtr))->Show(consistent); break; + case I_8266_U1_SM16825_5: (static_cast(busPtr))->Show(consistent); break; + case I_8266_DM_SM16825_5: (static_cast(busPtr))->Show(consistent); break; + case I_8266_BB_SM16825_5: (static_cast(busPtr))->Show(consistent); break; #endif #ifdef ARDUINO_ARCH_ESP32 // RMT buses @@ -631,6 +667,7 @@ class PolyBus { case I_32_RN_FW6_5: (static_cast(busPtr))->Show(consistent); break; case I_32_RN_2805_5: (static_cast(busPtr))->Show(consistent); break; case I_32_RN_TM1914_3: (static_cast(busPtr))->Show(consistent); break; + case I_32_RN_SM16825_5: (static_cast(busPtr))->Show(consistent); break; // I2S1 bus or paralell buses #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; @@ -644,6 +681,7 @@ class PolyBus { case I_32_I1_FW6_5: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; case I_32_I1_2805_5: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; case I_32_I1_TM1914_3: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; + case I_32_I1_SM16825_5: if (useParallelI2S) (static_cast(busPtr))->Show(consistent); else (static_cast(busPtr))->Show(consistent); break; #endif // I2S0 bus #ifndef WLED_NO_I2S0_PIXELBUS @@ -658,6 +696,7 @@ class PolyBus { case I_32_I0_FW6_5: (static_cast(busPtr))->Show(consistent); break; case I_32_I0_2805_5: (static_cast(busPtr))->Show(consistent); break; case I_32_I0_TM1914_3: (static_cast(busPtr))->Show(consistent); break; + case I_32_I0_SM16825_5: (static_cast(busPtr))->Show(consistent); break; #endif #endif case I_HS_DOT_3: (static_cast(busPtr))->Show(consistent); break; @@ -720,6 +759,10 @@ class PolyBus { case I_8266_U1_TM1914_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_DM_TM1914_3: return (static_cast(busPtr))->CanShow(); break; case I_8266_BB_TM1914_3: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U0_SM16825_5: return (static_cast(busPtr))->CanShow(); break; + case I_8266_U1_SM16825_5: return (static_cast(busPtr))->CanShow(); break; + case I_8266_DM_SM16825_5: return (static_cast(busPtr))->CanShow(); break; + case I_8266_BB_SM16825_5: return (static_cast(busPtr))->CanShow(); break; #endif #ifdef ARDUINO_ARCH_ESP32 // RMT buses @@ -734,6 +777,7 @@ class PolyBus { case I_32_RN_FW6_5: (static_cast(busPtr))->CanShow(); break; case I_32_RN_2805_5: (static_cast(busPtr))->CanShow(); break; case I_32_RN_TM1914_3: (static_cast(busPtr))->CanShow(); break; + case I_32_RN_SM16825_5: (static_cast(busPtr))->CanShow(); break; // I2S1 bus or paralell buses #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_3: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; @@ -747,6 +791,7 @@ class PolyBus { case I_32_I1_FW6_5: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; case I_32_I1_2805_5: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; case I_32_I1_TM1914_3: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; + case I_32_I1_SM16825_5: if (useParallelI2S) (static_cast(busPtr))->CanShow(); else (static_cast(busPtr))->CanShow(); break; #endif // I2S0 bus #ifndef WLED_NO_I2S0_PIXELBUS @@ -761,6 +806,7 @@ class PolyBus { case I_32_I0_FW6_5: (static_cast(busPtr))->CanShow(); break; case I_32_I0_2805_5: (static_cast(busPtr))->CanShow(); break; case I_32_I0_TM1914_3: (static_cast(busPtr))->CanShow(); break; + case I_32_I0_SM16825_5: (static_cast(busPtr))->CanShow(); break; #endif #endif case I_HS_DOT_3: return (static_cast(busPtr))->CanShow(); break; @@ -800,6 +846,7 @@ class PolyBus { case 1: col.W = col.B; col.B = w; break; // swap W & B case 2: col.W = col.G; col.G = w; break; // swap W & G case 3: col.W = col.R; col.R = w; break; // swap W & R + case 4: std::swap(cctWW, cctCW); break; // swap WW & CW } switch (busType) { @@ -849,6 +896,10 @@ class PolyBus { case I_8266_U1_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_8266_DM_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; case I_8266_BB_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_8266_U0_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; + case I_8266_U1_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; + case I_8266_DM_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; + case I_8266_BB_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; #endif #ifdef ARDUINO_ARCH_ESP32 // RMT buses @@ -863,6 +914,7 @@ class PolyBus { case I_32_RN_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; case I_32_RN_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; case I_32_RN_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_RN_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; // I2S1 bus or paralell buses #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; @@ -873,9 +925,10 @@ class PolyBus { case I_32_I1_UCS_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, Rgb48Color(RgbColor(col))); break; case I_32_I1_UCS_4: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, Rgbw64Color(col)); break; case I_32_I1_APA106_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; - case I_32_I1_FW6_5: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; - case I_32_I1_2805_5: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_32_I1_FW6_5: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); else (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; + case I_32_I1_2805_5: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); else (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; case I_32_I1_TM1914_3: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); else (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_I1_SM16825_5: if (useParallelI2S) (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); else (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; #endif // I2S0 bus #ifndef WLED_NO_I2S0_PIXELBUS @@ -890,6 +943,7 @@ class PolyBus { case I_32_I0_FW6_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; case I_32_I0_2805_5: (static_cast(busPtr))->SetPixelColor(pix, RgbwwColor(col.R, col.G, col.B, cctWW, cctCW)); break; case I_32_I0_TM1914_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; + case I_32_I0_SM16825_5: (static_cast(busPtr))->SetPixelColor(pix, Rgbww80Color(col.R*257, col.G*257, col.B*257, cctWW*257, cctCW*257)); break; #endif #endif case I_HS_DOT_3: (static_cast(busPtr))->SetPixelColor(pix, RgbColor(col)); break; @@ -953,6 +1007,10 @@ class PolyBus { case I_8266_U1_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; case I_8266_DM_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; case I_8266_BB_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U0_SM16825_5: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_U1_SM16825_5: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_DM_SM16825_5: (static_cast(busPtr))->SetLuminance(b); break; + case I_8266_BB_SM16825_5: (static_cast(busPtr))->SetLuminance(b); break; #endif #ifdef ARDUINO_ARCH_ESP32 // RMT buses @@ -967,6 +1025,7 @@ class PolyBus { case I_32_RN_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; case I_32_RN_2805_5: (static_cast(busPtr))->SetLuminance(b); break; case I_32_RN_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_RN_SM16825_5: (static_cast(busPtr))->SetLuminance(b); break; // I2S1 bus or paralell buses #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; @@ -980,6 +1039,7 @@ class PolyBus { case I_32_I1_FW6_5: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; case I_32_I1_2805_5: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; case I_32_I1_TM1914_3: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I1_SM16825_5: if (useParallelI2S) (static_cast(busPtr))->SetLuminance(b); else (static_cast(busPtr))->SetLuminance(b); break; #endif // I2S0 bus #ifndef WLED_NO_I2S0_PIXELBUS @@ -994,6 +1054,7 @@ class PolyBus { case I_32_I0_FW6_5: (static_cast(busPtr))->SetLuminance(b); break; case I_32_I0_2805_5: (static_cast(busPtr))->SetLuminance(b); break; case I_32_I0_TM1914_3: (static_cast(busPtr))->SetLuminance(b); break; + case I_32_I0_SM16825_5: (static_cast(busPtr))->SetLuminance(b); break; #endif #endif case I_HS_DOT_3: (static_cast(busPtr))->SetLuminance(b); break; @@ -1058,6 +1119,10 @@ class PolyBus { case I_8266_U1_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_DM_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_8266_BB_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_8266_U0_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_8266_U1_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_8266_DM_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W + case I_8266_BB_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W #endif #ifdef ARDUINO_ARCH_ESP32 // RMT buses @@ -1072,6 +1137,7 @@ class PolyBus { case I_32_RN_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W case I_32_RN_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W case I_32_RN_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_RN_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,max(c.WW,c.CW)/257); } break; // will not return original W // I2S1 bus or paralell buses #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_3: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; @@ -1079,12 +1145,13 @@ class PolyBus { case I_32_I1_400_3: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_I1_TM1_4: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_I1_TM2_3: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I1_UCS_3: { Rgb48Color c = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; - case I_32_I1_UCS_4: { Rgbw64Color c = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; + case I_32_I1_UCS_3: { Rgb48Color c = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,0); } break; + case I_32_I1_UCS_4: { Rgbw64Color c = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,c.W/257); } break; case I_32_I1_APA106_3: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_I1_FW6_5: { RgbwwColor c = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W case I_32_I1_2805_5: { RgbwwColor c = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W case I_32_I1_TM1914_3: col = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I1_SM16825_5: { Rgbww80Color c = (useParallelI2S) ? (static_cast(busPtr))->GetPixelColor(pix) : (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,max(c.WW,c.CW)/257); } break; // will not return original W #endif // I2S0 bus #ifndef WLED_NO_I2S0_PIXELBUS @@ -1093,12 +1160,13 @@ class PolyBus { case I_32_I0_400_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_I0_TM1_4: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_I0_TM2_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; - case I_32_I0_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,0); } break; - case I_32_I0_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R>>8,c.G>>8,c.B>>8,c.W>>8); } break; + case I_32_I0_UCS_3: { Rgb48Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,0); } break; + case I_32_I0_UCS_4: { Rgbw64Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,c.W/257); } break; case I_32_I0_APA106_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; case I_32_I0_FW6_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W case I_32_I0_2805_5: { RgbwwColor c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R,c.G,c.B,max(c.WW,c.CW)); } break; // will not return original W case I_32_I0_TM1914_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; + case I_32_I0_SM16825_5: { Rgbww80Color c = (static_cast(busPtr))->GetPixelColor(pix); col = RGBW32(c.R/257,c.G/257,c.B/257,max(c.WW,c.CW)/257); } break; // will not return original W #endif #endif case I_HS_DOT_3: col = (static_cast(busPtr))->GetPixelColor(pix); break; @@ -1181,6 +1249,10 @@ class PolyBus { case I_8266_U1_TM1914_3: delete (static_cast(busPtr)); break; case I_8266_DM_TM1914_3: delete (static_cast(busPtr)); break; case I_8266_BB_TM1914_3: delete (static_cast(busPtr)); break; + case I_8266_U0_SM16825_5: delete (static_cast(busPtr)); break; + case I_8266_U1_SM16825_5: delete (static_cast(busPtr)); break; + case I_8266_DM_SM16825_5: delete (static_cast(busPtr)); break; + case I_8266_BB_SM16825_5: delete (static_cast(busPtr)); break; #endif #ifdef ARDUINO_ARCH_ESP32 // RMT buses @@ -1195,6 +1267,7 @@ class PolyBus { case I_32_RN_FW6_5: delete (static_cast(busPtr)); break; case I_32_RN_2805_5: delete (static_cast(busPtr)); break; case I_32_RN_TM1914_3: delete (static_cast(busPtr)); break; + case I_32_RN_SM16825_5: delete (static_cast(busPtr)); break; // I2S1 bus or paralell buses #ifndef WLED_NO_I2S1_PIXELBUS case I_32_I1_NEO_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; @@ -1208,6 +1281,7 @@ class PolyBus { case I_32_I1_FW6_5: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; case I_32_I1_2805_5: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; case I_32_I1_TM1914_3: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; + case I_32_I1_SM16825_5: if (useParallelI2S) delete (static_cast(busPtr)); else delete (static_cast(busPtr)); break; #endif // I2S0 bus #ifndef WLED_NO_I2S0_PIXELBUS @@ -1222,6 +1296,7 @@ class PolyBus { case I_32_I0_FW6_5: delete (static_cast(busPtr)); break; case I_32_I0_2805_5: delete (static_cast(busPtr)); break; case I_32_I0_TM1914_3: delete (static_cast(busPtr)); break; + case I_32_I0_SM16825_5: delete (static_cast(busPtr)); break; #endif #endif case I_HS_DOT_3: delete (static_cast(busPtr)); break; @@ -1290,6 +1365,8 @@ class PolyBus { return I_8266_U0_2805_5 + offset; case TYPE_TM1914: return I_8266_U0_TM1914_3 + offset; + case TYPE_SM16825: + return I_8266_U0_SM16825_5 + offset; } #else //ESP32 uint8_t offset = 0; // 0 = RMT (num 1-8), 1 = I2S0 (used by Audioreactive), 2 = I2S1 @@ -1343,6 +1420,8 @@ class PolyBus { return I_32_RN_2805_5 + offset; case TYPE_TM1914: return I_32_RN_TM1914_3 + offset; + case TYPE_SM16825: + return I_32_RN_SM16825_5 + offset; } #endif } diff --git a/wled00/const.h b/wled00/const.h index 0ff70e47d..8a5983213 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -295,6 +295,7 @@ #define TYPE_TM1814 31 #define TYPE_WS2805 32 //RGB + WW + CW #define TYPE_TM1914 33 //RGB +#define TYPE_SM16825 34 //RGB + WW + CW //"Analog" types (40-47) #define TYPE_ONOFF 40 //binary output (relays etc.; NOT PWM) #define TYPE_ANALOG_1CH 41 //single channel PWM. Uses value of brightest RGBW channel diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 1712b360e..b7d2d18a7 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -22,10 +22,10 @@ function isAna(t) { return t == 40 || isPWM(t); } // is analog type function isDig(t) { return (t > 15 && t < 40) || isD2P(t); } // is digital type function isD2P(t) { return t > 47 && t < 64; } // is digital 2 pin type - function is16b(t) { return t == 26 || t == 29 } // is digital 16 bit type + function is16b(t) { return t == 26 || t == 29 || t == 34; } // is digital 16 bit type function isVir(t) { return t >= 80 && t < 96; } // is virtual type - function hasW(t) { return (t >= 18 && t <= 21) || (t >= 28 && t <= 32) || (t >= 44 && t <= 45) || (t >= 88 && t <= 89); } - function hasCCT(t) { return t == 20 || t == 21 || t == 42 || t == 45 || t == 28 || t == 32; } + function hasW(t) { return (t >= 18 && t <= 21) || (t >= 28 && t <= 32) || t == 34 || (t >= 44 && t <= 45) || (t >= 88 && t <= 89); } + function hasCCT(t) { return t == 20 || t == 21 || t == 42 || t == 45 || t == 28 || t == 32 || t == 34; } // https://www.educative.io/edpresso/how-to-dynamically-load-a-js-file-in-javascript function loadJS(FILE_URL, async = true) { let scE = d.createElement("script"); @@ -264,7 +264,8 @@ gId("rf"+n).onclick = (t == 31) ? (()=>{return false}) : (()=>{}); // prevent change for TM1814 gRGBW |= hasW(t); // RGBW checkbox, TYPE_xxxx values from const.h gId("co"+n).style.display = (isVir(t) || isAna(t)) ? "none":"inline"; // hide color order for PWM - gId("dig"+n+"w").style.display = (isDig(t) && hasW(t)) ? "inline":"none"; // show swap channels dropdown + gId("dig"+n+"w").style.display = (isDig(t) && hasW(t)) ? "inline":"none"; // show swap channels dropdown + gId("dig"+n+"w").querySelector("[data-opt=CCT]").disabled = !hasCCT(t); // disable WW/CW swapping if (!(isDig(t) && hasW(t))) d.Sf["WO"+n].value = 0; // reset swapping gId("dig"+n+"c").style.display = (isAna(t)) ? "none":"inline"; // hide count for analog gId("dig"+n+"r").style.display = (isVir(t)) ? "none":"inline"; // hide reversed for virtual @@ -419,6 +420,7 @@ ${i+1}: \ \ \ +\ \ \ \ @@ -459,7 +461,7 @@ mA/LED: -

+
Start:   From ea80c1ed830251ae2154e69495dc6fb5d2cda3d1 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Wed, 7 Aug 2024 22:39:31 +0200 Subject: [PATCH 047/142] Swap WW & CW --- wled00/bus_wrapper.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/wled00/bus_wrapper.h b/wled00/bus_wrapper.h index d1d4f8941..ae39adc14 100644 --- a/wled00/bus_wrapper.h +++ b/wled00/bus_wrapper.h @@ -286,10 +286,10 @@ #define B_32_I1_TM1914_3 NeoPixelBusLg #define B_32_I1_TM1914_3P NeoPixelBusLg // parallel I2S //Sm16825 (RGBWC) -#define B_32_RN_SM16825_5 NeoPixelBusLg -#define B_32_I0_SM16825_5 NeoPixelBusLg -#define B_32_I1_SM16825_5 NeoPixelBusLg -#define B_32_I1_SM16825_5P NeoPixelBusLg // parallel I2S +#define B_32_RN_SM16825_5 NeoPixelBusLg +#define B_32_I0_SM16825_5 NeoPixelBusLg +#define B_32_I1_SM16825_5 NeoPixelBusLg +#define B_32_I1_SM16825_5P NeoPixelBusLg // parallel I2S #endif //APA102 From 96c7716d3efb86254dfbd0e1b3febcdc1b8a928a Mon Sep 17 00:00:00 2001 From: jdiamond Date: Thu, 8 Aug 2024 03:13:33 +0000 Subject: [PATCH 048/142] Added a usermod for interacting with BLE Pixels Dice. --- usermods/pixels_dice_tray/README.md | 252 +++++++++ .../WLED_ESP32_4MB_64KB_FS.csv | 6 + usermods/pixels_dice_tray/dice_state.h | 76 +++ .../pixels_dice_tray/generate_roll_info.py | 230 ++++++++ usermods/pixels_dice_tray/images/effect.webp | Bin 0 -> 14460 bytes usermods/pixels_dice_tray/images/info.webp | Bin 0 -> 24412 bytes .../pixels_dice_tray/images/roll_plot.png | Bin 0 -> 10025 bytes usermods/pixels_dice_tray/images/status.webp | Bin 0 -> 24080 bytes usermods/pixels_dice_tray/led_effects.h | 124 ++++ .../mqtt_client/mqtt_logger.py | 104 ++++ .../mqtt_client/mqtt_plotter.py | 69 +++ .../mqtt_client/requirements.txt | 2 + usermods/pixels_dice_tray/pixels_dice_tray.h | 535 ++++++++++++++++++ .../platformio_override.ini.sample | 114 ++++ usermods/pixels_dice_tray/roll_info.h | 107 ++++ usermods/pixels_dice_tray/tft_menu.h | 479 ++++++++++++++++ wled00/FX.cpp | 7 +- wled00/FX.h | 2 +- wled00/const.h | 1 + wled00/pin_manager.h | 3 +- wled00/usermods_list.cpp | 8 + 21 files changed, 2115 insertions(+), 4 deletions(-) create mode 100644 usermods/pixels_dice_tray/README.md create mode 100644 usermods/pixels_dice_tray/WLED_ESP32_4MB_64KB_FS.csv create mode 100644 usermods/pixels_dice_tray/dice_state.h create mode 100644 usermods/pixels_dice_tray/generate_roll_info.py create mode 100644 usermods/pixels_dice_tray/images/effect.webp create mode 100644 usermods/pixels_dice_tray/images/info.webp create mode 100644 usermods/pixels_dice_tray/images/roll_plot.png create mode 100644 usermods/pixels_dice_tray/images/status.webp create mode 100644 usermods/pixels_dice_tray/led_effects.h create mode 100644 usermods/pixels_dice_tray/mqtt_client/mqtt_logger.py create mode 100644 usermods/pixels_dice_tray/mqtt_client/mqtt_plotter.py create mode 100644 usermods/pixels_dice_tray/mqtt_client/requirements.txt create mode 100644 usermods/pixels_dice_tray/pixels_dice_tray.h create mode 100644 usermods/pixels_dice_tray/platformio_override.ini.sample create mode 100644 usermods/pixels_dice_tray/roll_info.h create mode 100644 usermods/pixels_dice_tray/tft_menu.h diff --git a/usermods/pixels_dice_tray/README.md b/usermods/pixels_dice_tray/README.md new file mode 100644 index 000000000..5440b225c --- /dev/null +++ b/usermods/pixels_dice_tray/README.md @@ -0,0 +1,252 @@ +# A mod for using Pixel Dice with ESP32S3 boards + +A usermod to connect to and handle rolls from [Pixels Dice](https://gamewithpixels.com/). WLED acts as both an display controller, and a gateway to connect the die to the Wifi network. + +High level features: + +* Several LED effects that respond to die rolls + * Effect color and parameters can be modified like any other effect + * Different die can be set to control different segments +* An optional GUI on a TFT screen with custom button controls + * Gives die connection and roll status + * Can do basic LED effect controls + * Can display custom info for different roll types (ie. RPG stats/spell info) +* Publish MQTT events from die rolls + * Also report the selected roll type +* Control settings through the WLED web + +See for a write up of the design process of the hardware and software I used this with. + +I also set up a custom web installer for the usermod at for 8MB ESP32-S3 boards. + +## Table of Contents + + +* [Demos](#demos) + + [TFT GUI](#tft-gui) + + [Multiple Die Controlling Different Segments](#multiple-die-controlling-different-segments) +* [Hardware](#hardware) +* [Library used](#library-used) +* [Compiling](#compiling) + + [platformio_override.ini](#platformio_overrideini) + + [Manual platformio.ini changes](#manual-platformioini-changes) +* [Configuration](#configuration) + + [Controlling Dice Connections](#controlling-dice-connections) + + [Controlling Effects](#controlling-effects) + - [DieSimple](#diesimple) + - [DiePulse](#diepulse) + - [DieCheck](#diecheck) +* [TFT GUI](#tft-gui-1) + + [Status](#status) + + [Effect Menu](#effect-menu) + + [Roll Info](#roll-info) +* [MQTT](#mqtt) +* [Potential Modifications and Additional Features](#potential-modifications-and-additional-features) +* [ESP32 Issues](#esp32-issues) + + + +## Demos + + +### TFT GUI +[![Watch the video](https://img.youtube.com/vi/VNsHq1TbiW8/0.jpg)](https://youtu.be/VNsHq1TbiW8) + + +### Multiple Die Controlling Different Segments +[![Watch the video](https://img.youtube.com/vi/oCDr44C-qwM/0.jpg)](https://youtu.be/oCDr44C-qwM) + + +## Hardware + +The main purpose of this mod is to support [Pixels Dice](https://gamewithpixels.com/). The board acts as a BLE central for the dice acting as peripherals. While any ESP32 variant with BLE capabilities should be able to support this usermod, in practice I found that the original ESP32 did not work. See [ESP32 Issues](#esp32-issues) for a deeper dive. + +The only other ESP32 variant I tested was the ESP32-S3, which worked without issue. While there's still concern over the contention between BLE and WiFi for the radio, I haven't noticed any performance impact in practice. The only special behavior that was needed was setting `noWifiSleep = false;` to allow the OS to sleep the WiFi when the BLE is active. + +The basic build of this usermod doesn't require any special hardware. However, the LCD status GUI was specifically designed for the [LILYGO T-QT Pro](https://www.lilygo.cc/products/t-qt-pro). + +It should be relatively easy to support other displays, though the positioning of the text may need to be adjusted. + + +## Library used + +[axlan/pixels-dice-interface](https://github.com/axlan/arduino-pixels-dice) + +Optional: [Bodmer/TFT_eSPI](https://github.com/Bodmer/TFT_eSPI) + + +## Compiling + + +### platformio_override.ini + +Copy and update the example `platformio_override.ini.sample` to the root directory of your particular build (renaming it `platformio_override.ini`). +This file should be placed in the same directory as `platformio.ini`. This file is set up for the [LILYGO T-QT Pro](https://www.lilygo.cc/products/t-qt-pro). Specifically, the 8MB flash version. See the next section for notes on setting the build flags. For other boards, you may want to use a different environment as the basis. + + +### Manual platformio.ini changes + +Using the `platformio_override.ini.sample` as a reference, you'll need to update the `build_flags` and `lib_deps` of the target you're building for. + +If you don't need the TFT GUI, you just need to add + + +```ini +... +build_flags = + ... + -D USERMOD_PIXELS_DICE_TRAY ;; Enables this UserMod +lib_deps = + ... + ESP32 BLE Arduino + axlan/pixels-dice-interface @ 1.2.0 +... +``` + +For the TFT support you'll need to add `Bodmer/TFT_eSPI` to `lib_deps`, and all of the required TFT parameters to `build_flags` (see `platformio_override.ini.sample`). + +Save the `platformio.ini` file, and perform the desired build. + + +## Configuration + +In addition to configuring which dice to connect to, this mod uses a lot of the built in WLED features: +* The LED segments, effects, and customization parameters +* The buttons for the UI +* The MQTT settings for reporting the dice rolls + + +### Controlling Dice Connections + +**NOTE:** To configure the die itself (set its name, the die LEDs, etc.), you still need to use the Pixels Dice phone App. + +The usermods settings page has the configuration for controlling the dice and the display: + * Ble Scan Duration - The time to look for BLE broadcasts before taking a break + * Rotation - If display used, set this parameter to rotate the display. + +The main setting here though are the Die 0 and 1 settings. A slot is disabled if it's left blank. Putting the name of a die will make that slot only connect to die with that name. Alteratively, if the name is set to `*` the slot will use the first unassociated die it sees. Saving the configuration while a wildcard slot is connected to a die will replace the `*` with that die's name. + +**NOTE:** The slot a die is in is important since that's how they're identified for controlling LED effects. Effects can be set to respond to die 0, 1, or any. + +The configuration also includes the pins configured in the TFT build flags. These are just so the UI recognizes that these pins are being used. The [Bodmer/TFT_eSPI](https://github.com/Bodmer/TFT_eSPI) requires that these are set at build time and changing these values is ignored. + + +### Controlling Effects + +The die effects for rolls take advantage of most of the normal WLED effect features: . + +If you have different segments, they can have different effects driven by the same die, or different dice. + + +#### DieSimple +Turn off LEDs while rolling, than light up solid LEDs in proportion to die roll. + +* Color 1 - Selects the "good" color that increases based on the die roll +* Color 2 - Selects the "background" color for the rest of the segment +* Custom 1 - Sets which die should control this effect. If the value is greater then 1, it will respond to both dice. + + +#### DiePulse +Play `breath` effect while rolling, than apply `blend` effect in proportion to die roll. + +* Color 1 - See `breath` and `blend` +* Color 2 - Selects the "background" color for the rest of the segment +* Palette - See `breath` and `blend` +* Custom 1 - Sets which die should control this effect. If the value is greater then 1, it will respond to both dice. + + +#### DieCheck +Play `running` effect while rolling, than apply `glitter` effect if roll passes threshold, or `gravcenter` if roll is below. + +* Color 1 - See `glitter` and `gravcenter`, used as first color for `running` +* Color 2 - See `glitter` and `gravcenter` +* Color 3 - Used as second color for `running` +* Palette - See `glitter` and `gravcenter` +* Custom 1 - Sets which die should control this effect. If the value is greater then 1, it will respond to both dice. +* Custom 2 - Sets the threshold for success animation. For example if 10, success plays on rolls of 10 or above. + + +## TFT GUI + +The optional TFT GUI currently supports 3 "screens": +1. Status +2. Effect Control +3. Roll Info + +Double pressing the right button goes forward through the screens, and double pressing left goes back (with rollover). + + +### Status +Status Menu + +Shows the status of each die slot (0 on top and 1 on the bottom). + +If a die is connected, its roll stats and battery status are shown. The rolls will continue to be tracked even when viewing other screens. + +Long press either button to clear the roll stats. + + +### Effect Menu +Effect Menu + +Allows limited customization of the die effect for the currently selected LED segment. + +The left button moves the cursor (blue box) up and down the options for the current field. + +The right button updates the value for the field. + +The first field is the effect. Updating it will switch between the die effects. + +The DieCheck effect has an additional field "PASS". Pressing the right button on this field will copy the current face up value from the most recently rolled die. + +Long pressing either value will set the effect parameters (color, palette, controlling dice, etc.) to a default set of values. + + +### Roll Info +Roll Info Menu + +Sets the "roll type" reported by MQTT events and can show additional info. + +Pressing the right button goes forward through the rolls, and double pressing left goes back (with rollover). + +The names and info for the rolls are generated from the `usermods/pixels_dice_tray/generate_roll_info.py` script. It updates `usermods/pixels_dice_tray/roll_info.h` with code generated from a simple markdown language. + + +## MQTT + +See for general MQTT configuration for WLED. + +The usermod produces two types of events + +* `$mqttDeviceTopic/dice/roll` - JSON that reports each die roll event with the following keys. + - name - The name of the die that triggered the event + - state - Integer indicating the die state `[UNKNOWN = 0, ON_FACE = 1, HANDLING = 2, ROLLING = 3, CROOKED = 4]` + - val - The value on the die's face. For d20 1-20 + - time - The uptime timestamp the roll was received in milliseconds. +* `$mqttDeviceTopic/dice/roll_label` - A string that indicates the roll type selected in the [Roll Info](#roll-info) TFT menu. + +Where `$mqttDeviceTopic` is the topic set in the WLED MQTT configuration. + +Events can be logged to a CSV file using the script `usermods/pixels_dice_tray/mqtt_client/mqtt_logger.py`. These can then be used to generate interactive HTML plots with `usermods/pixels_dice_tray/mqtt_client/mqtt_plotter.py`. + +Roll Plot + + +## Potential Modifications and Additional Features + +This usermod is in support of a particular dice box project, but it would be fairly straightforward to extend for other applications. +* Add more dice - There's no reason that several more dice slots couldn't be allowed. In addition LED effects that use multiple dice could be added (e.g. a contested roll). +* Better support for die other then d20's. There's a few places where I assume the die is a d20. It wouldn't be that hard to support arbitrary die sizes. +* TFT Menu - The menu system is pretty extensible. I put together some basic things I found useful, and was mainly limited by the screen size. +* Die controlled UI - I originally planned to make an alternative UI that used the die directly. You'd press a button, and the current face up on the die would trigger an action. This was an interesting idea, but didn't seem to practical since I could more flexibly reproduce this by responding to the dice MQTT events. + + +## ESP32 Issues + +I really wanted to have this work on the original ESP32 boards to lower the barrier to entry, but there were several issues. + +First, the BLE stack requires a lot of flash. I had to make a special partitioning plan `WLED_ESP32_4MB_64KB_FS.csv` to even fit the build on 4MB devices. This only has 64KB of file system space, which is limited, but still functional. + +The bigger issue is that the build consistently crashes if the BLE scan task starts up. It's a bit unclear to me exactly what is failing since the backtrace is showing an exception in `new[]` memory allocation in the UDP stack. There appears to be a ton of heap available, so my guess is that this is a synchronization issue of some sort from the tasks running in parallel. I tried messing with the task core affinity a bit but didn't make much progress. It's not really clear what difference between the ESP32S3 and ESP32 would cause this difference. + +At the end of the day, its generally not advised to run the BLE and Wifi at the same time anyway (though it appears to work without issue on the ESP32S3). Probably the best path forward would be to switch between them. This would actually not be too much of an issue, since discovering and getting data from the die should be possible to do in bursts (at least in theory). diff --git a/usermods/pixels_dice_tray/WLED_ESP32_4MB_64KB_FS.csv b/usermods/pixels_dice_tray/WLED_ESP32_4MB_64KB_FS.csv new file mode 100644 index 000000000..ffa509e6d --- /dev/null +++ b/usermods/pixels_dice_tray/WLED_ESP32_4MB_64KB_FS.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, 0x9000, 0x5000, +otadata, data, ota, 0xe000, 0x2000, +app0, app, ota_0, 0x10000, 0x1F0000, +app1, app, ota_1, 0x200000,0x1F0000, +spiffs, data, spiffs, 0x3F0000,0x10000, \ No newline at end of file diff --git a/usermods/pixels_dice_tray/dice_state.h b/usermods/pixels_dice_tray/dice_state.h new file mode 100644 index 000000000..eee4759fd --- /dev/null +++ b/usermods/pixels_dice_tray/dice_state.h @@ -0,0 +1,76 @@ +/** + * Structs for passing around usermod state + */ +#pragma once + +#include // https://github.com/axlan/arduino-pixels-dice + +/** + * Here's how the rolls are tracked in this usermod. + * 1. The arduino-pixels-dice library reports rolls and state mapped to + * PixelsDieID. + * 2. The "configured_die_names" sets which die to connect to and their order. + * 3. The rest of the usermod references the die by this order (ie. the LED + * effect is triggered for rolls for die 0). + */ + +static constexpr size_t MAX_NUM_DICE = 2; +static constexpr uint8_t INVALID_ROLL_VALUE = 0xFF; + +/** + * The state of the connected die, and new events since the last update. + */ +struct DiceUpdate { + // The vectors to hold results queried from the library + // Since vectors allocate data, it's more efficient to keep reusing an instance + // instead of declaring them on the stack. + std::vector dice_list; + pixels::RollUpdates roll_updates; + pixels::BatteryUpdates battery_updates; + // The PixelsDieID for each dice index. 0 if the die isn't connected. + // The ordering here matches configured_die_names. + std::array connected_die_ids{0, 0}; +}; + +struct DiceSettings { + // The mapping of dice names, to the index of die used for effects (ie. The + // die named "Cat" is die 0). BLE discovery will stop when all the dice are + // found. The die slot is disabled if the name is empty. If the name is "*", + // the slot will use the first unassociated die it sees. + std::array configured_die_names{"*", "*"}; + // A label set to describe the next die roll. Index into GetRollName(). + uint8_t roll_label = INVALID_ROLL_VALUE; +}; + +// These are updated in the main loop, but accessed by the effect functions as +// well. My understand is that both of these accesses should be running on the +// same "thread/task" since WLED doesn't directly create additional threads. The +// exception would be network callbacks and interrupts, but I don't believe +// these accesses are triggered by those. If synchronization was needed, I could +// look at the example in `requestJSONBufferLock()`. +std::array last_die_events; + +static pixels::RollEvent GetLastRoll() { + pixels::RollEvent last_roll; + for (const auto& event : last_die_events) { + if (event.timestamp > last_roll.timestamp) { + last_roll = event; + } + } + return last_roll; +} + +/** + * Returns true if the container has an item that matches the value. + */ +template +static bool Contains(const C& container, T value) { + return std::find(container.begin(), container.end(), value) != + container.end(); +} + +// These aren't known until runtime since they're being added dynamically. +static uint8_t FX_MODE_SIMPLE_D20 = 0xFF; +static uint8_t FX_MODE_PULSE_D20 = 0xFF; +static uint8_t FX_MODE_CHECK_D20 = 0xFF; +std::array DIE_LED_MODES = {0xFF, 0xFF, 0xFF}; diff --git a/usermods/pixels_dice_tray/generate_roll_info.py b/usermods/pixels_dice_tray/generate_roll_info.py new file mode 100644 index 000000000..589597086 --- /dev/null +++ b/usermods/pixels_dice_tray/generate_roll_info.py @@ -0,0 +1,230 @@ +''' +File for generating roll labels and info text for the InfoMenu. + +Uses a very limited markdown language for styling text. +''' +import math +from pathlib import Path +import re +from textwrap import indent + +# Variables for calculating values in info text +CASTER_LEVEL = 9 +SPELL_ABILITY_MOD = 6 +BASE_ATK_BONUS = 6 +SIZE_BONUS = 1 +STR_BONUS = 2 +DEX_BONUS = -1 + +# TFT library color values +TFT_BLACK =0x0000 +TFT_NAVY =0x000F +TFT_DARKGREEN =0x03E0 +TFT_DARKCYAN =0x03EF +TFT_MAROON =0x7800 +TFT_PURPLE =0x780F +TFT_OLIVE =0x7BE0 +TFT_LIGHTGREY =0xD69A +TFT_DARKGREY =0x7BEF +TFT_BLUE =0x001F +TFT_GREEN =0x07E0 +TFT_CYAN =0x07FF +TFT_RED =0xF800 +TFT_MAGENTA =0xF81F +TFT_YELLOW =0xFFE0 +TFT_WHITE =0xFFFF +TFT_ORANGE =0xFDA0 +TFT_GREENYELLOW =0xB7E0 +TFT_PINK =0xFE19 +TFT_BROWN =0x9A60 +TFT_GOLD =0xFEA0 +TFT_SILVER =0xC618 +TFT_SKYBLUE =0x867D +TFT_VIOLET =0x915C + + +class Size: + def __init__(self, w, h): + self.w = w + self.h = h + + +# Font 1 6x8 +# Font 2 12x16 +CHAR_SIZE = { + 1: Size(6, 8), + 2: Size(12, 16), +} + +SCREEN_SIZE = Size(128, 128) + +# Calculates distance for short range spell. +def short_range() -> int: + return 25 + 5 * CASTER_LEVEL + +# Entries in markdown language. +# Parameter 0 of the tuple is the roll name +# Parameter 1 of the tuple is the roll info. +# The text will be shown when the roll type is selected. An error will be raised +# if the text would unexpectedly goes past the end of the screen. There are a +# few styling parameters that need to be on their own lines: +# $COLOR - The color for the text +# $SIZE - Sets the text size (see CHAR_SIZE) +# $WRAP - By default text won't wrap and generate an error. This enables text wrapping. Lines will wrap mid-word. +ENTRIES = [ + tuple(["Barb Chain", f'''\ +$COLOR({TFT_RED}) +Barb Chain +$COLOR({TFT_WHITE}) +Atk/CMD {BASE_ATK_BONUS + SPELL_ABILITY_MOD} +Range: {short_range()} +$WRAP(1) +$SIZE(1) +Summon {1 + math.floor((CASTER_LEVEL-1)/3)} chains. Make a melee atk 1d6 or a trip CMD=AT. On a hit make Will save or shaken 1d4 rnds. +''']), + tuple(["Saves", f'''\ +$COLOR({TFT_GREEN}) +Saves +$COLOR({TFT_WHITE}) +FORT 8 +REFLEX 8 +WILL 9 +''']), + tuple(["Skill", f'''\ +Skill +''']), + tuple(["Attack", f'''\ +Attack +Melee +{BASE_ATK_BONUS + SIZE_BONUS + STR_BONUS} +Range +{BASE_ATK_BONUS + SIZE_BONUS + DEX_BONUS} +''']), + tuple(["Cure", f'''\ +Cure +Lit 1d8+{min(5, CASTER_LEVEL)} +Mod 2d8+{min(10, CASTER_LEVEL)} +Ser 3d8+{min(15, CASTER_LEVEL)} +''']), + tuple(["Concentrate", f'''\ +Concentrat ++{CASTER_LEVEL + SPELL_ABILITY_MOD} +$SIZE(1) +Defensive 15+2*SP_LV +Dmg 10+DMG+SP_LV +Grapple 10+CMB+SP_LV +''']), +] + +RE_SIZE = re.compile(r'\$SIZE\(([0-9])\)') +RE_COLOR = re.compile(r'\$COLOR\(([0-9]+)\)') +RE_WRAP = re.compile(r'\$WRAP\(([0-9])\)') + +END_HEADER_TXT = '// GENERATED\n' + +def main(): + roll_info_file = Path(__file__).parent / 'roll_info.h' + old_contents = open(roll_info_file, 'r').read() + + end_header = old_contents.index(END_HEADER_TXT) + + with open(roll_info_file, 'w') as fd: + fd.write(old_contents[:end_header+len(END_HEADER_TXT)]) + + for key, entry in enumerate(ENTRIES): + size = 2 + wrap = False + y_loc = 0 + results = [] + for line in entry[1].splitlines(): + if line.startswith('$'): + m_size = RE_SIZE.match(line) + m_color = RE_COLOR.match(line) + m_wrap = RE_WRAP.match(line) + if m_size: + size = int(m_size.group(1)) + results.append(f'tft.setTextSize({size});') + elif m_color: + results.append( + f'tft.setTextColor({int(m_color.group(1))});') + elif m_wrap: + wrap = bool(int(m_wrap.group(1))) + else: + print(f'Entry {key} unknown modifier "{line}".') + exit(1) + else: + max_chars_per_line = math.floor( + SCREEN_SIZE.w / CHAR_SIZE[size].w) + if len(line) > max_chars_per_line: + if wrap: + while len(line) > max_chars_per_line: + results.append( + f'tft.println("{line[:max_chars_per_line]}");') + line = line[max_chars_per_line:].lstrip() + y_loc += CHAR_SIZE[size].h + else: + print(f'Entry {key} line "{line}" too long.') + exit(1) + + if len(line) > 0: + y_loc += CHAR_SIZE[size].h + results.append(f'tft.println("{line}");') + + if y_loc > SCREEN_SIZE.h: + print( + f'Entry {key} line "{line}" went past bottom of screen.') + exit(1) + + result = indent('\n'.join(results), ' ') + + fd.write(f'''\ +static void PrintRoll{key}() {{ +{result} +}} + +''') + + results = [] + for key, entry in enumerate(ENTRIES): + results.append(f'''\ +case {key}: + return "{entry[0]}";''') + + cases = indent('\n'.join(results), ' ') + + fd.write(f'''\ +static const char* GetRollName(uint8_t key) {{ + switch (key) {{ +{cases} + }} + return ""; +}} + +''') + + results = [] + for key, entry in enumerate(ENTRIES): + results.append(f'''\ +case {key}: + PrintRoll{key}(); + return;''') + + cases = indent('\n'.join(results), ' ') + + fd.write(f'''\ +static void PrintRollInfo(uint8_t key) {{ + tft.setTextColor(TFT_WHITE); + tft.setCursor(0, 0); + tft.setTextSize(2); + switch (key) {{ +{cases} + }} + tft.setTextColor(TFT_RED); + tft.setCursor(0, 60); + tft.println("Unknown"); +}} + +''') + + fd.write(f'static constexpr size_t NUM_ROLL_INFOS = {len(ENTRIES)};\n') + + +main() diff --git a/usermods/pixels_dice_tray/images/effect.webp b/usermods/pixels_dice_tray/images/effect.webp new file mode 100644 index 0000000000000000000000000000000000000000..989ed1eb9e7b46b10bc80b9478bab17bc0c52cab GIT binary patch literal 14460 zcmV-?ID^MhNk&F=H~;`wMM6+kP&goHH~;{Ud;y&SDzO3d0zQ#MoJ%F6uP383DyIIGHpY`5K|y+%hqaJN(CSE?{4w?u-`<7bEM&0Z?2@$MSA=aAHp3}jGnI!J?0B@Ytsv&|FEA91 zvJ`)63f^6a>qbJ4iC`<(i}?Qx3cg*Qk>@h~Fi?9Aj!4T@|Xc88?`G@H4Viv-#yAuRpgw@Fa!IFvIspKZU zas4TNR2DSWva_5WakV{2;7wgQ96g|sA{b~ezVj4LednR|xvH_>x;s!fV0R}9o5l81 zo&@{b`kQ|vwlm3z8=gcsSkw8hOiK3wnf3xJKF^Sj%Hq5p4btooNxn?t7PyT~0p`(f z+nRgp*3d_q^xyKUFZH!i&WK_)OCw4hBs4kWcgWft>C$~^AAS#E2v^$kQ-!=AxY*jC zb(lf0*M1i1Q4vruN2dHC*{}49ZyW;$5Jm>=TT4DO@4HJ-a29g!CP4|E1_SvgW$W>A zu|7GnO-7}n)~#tSo;4|Cw-eyx<98MD1b2Nabq^F4oUSxa=n#5*|7#R{DGeFM zyhg|c&3IF`z{7xaBc6v`Y_Eo6m63zHG0O>GyXeT-_t9(AV$zlJ0VAO;oH@N@T+D z`8{%LRY0)$MT)nq&YKx@M2v-O{HC-=5p5^UPl?(lX2`z|aPQF>xpfR$2yU z+gDR{mX_Q=R!z)GSXCWE0?MX3BKsZOU)NltH`SdN>mhQ=3dXWyXHl3N4MgiC2 z*T~=RRgaNBMNkA-biG8F&_ap{eXpFY59Ni=Z#Hx&Rm2#rXSO3xW8SeXq`4Yo2`O^5 zJmIAbcc�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 literal 0 HcmV?d00001 diff --git a/usermods/pixels_dice_tray/images/roll_plot.png b/usermods/pixels_dice_tray/images/roll_plot.png new file mode 100644 index 0000000000000000000000000000000000000000..47f5688635c067128e70cad6ad1b105e82d2296d GIT binary patch literal 10025 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 this value. + */ + +/** + * Return the last die roll based on the custom1 effect setting. + */ +static pixels::RollEvent GetLastRollForSegment() { + // If an invalid die is selected, fallback to using the most recent roll from + // any die. + if (SEGMENT.custom1 >= MAX_NUM_DICE) { + return GetLastRoll(); + } else { + return last_die_events[SEGMENT.custom1]; + } +} + + +/* + * Alternating pixels running function (copied static function). + */ +// paletteBlend: 0 - wrap when moving, 1 - always wrap, 2 - never wrap, 3 - none (undefined) +#define PALETTE_SOLID_WRAP (strip.paletteBlend == 1 || strip.paletteBlend == 3) +static uint16_t running_copy(uint32_t color1, uint32_t color2, bool theatre = false) { + int width = (theatre ? 3 : 1) + (SEGMENT.intensity >> 4); // window + uint32_t cycleTime = 50 + (255 - SEGMENT.speed); + uint32_t it = strip.now / cycleTime; + bool usePalette = color1 == SEGCOLOR(0); + + for (int i = 0; i < SEGLEN; i++) { + uint32_t col = color2; + if (usePalette) color1 = SEGMENT.color_from_palette(i, true, PALETTE_SOLID_WRAP, 0); + if (theatre) { + if ((i % width) == SEGENV.aux0) col = color1; + } else { + int pos = (i % (width<<1)); + if ((pos < SEGENV.aux0-width) || ((pos >= SEGENV.aux0) && (pos < SEGENV.aux0+width))) col = color1; + } + SEGMENT.setPixelColor(i,col); + } + + if (it != SEGENV.step) { + SEGENV.aux0 = (SEGENV.aux0 +1) % (theatre ? width : (width<<1)); + SEGENV.step = it; + } + return FRAMETIME; +} + +static uint16_t simple_roll() { + auto roll = GetLastRollForSegment(); + if (roll.state != pixels::RollState::ON_FACE) { + SEGMENT.fill(0); + } else { + uint16_t num_segments = float(roll.current_face + 1) / 20.0 * SEGLEN; + for (int i = 0; i <= num_segments; i++) { + SEGMENT.setPixelColor(i, SEGCOLOR(0)); + } + for (int i = num_segments; i < SEGLEN; i++) { + SEGMENT.setPixelColor(i, SEGCOLOR(1)); + } + } + return FRAMETIME; +} +// See https://kno.wled.ge/interfaces/json-api/#effect-metadata +// Name - DieSimple +// Parameters - +// * Selected Die (custom1) +// Colors - Uses color1 and color2 +// Palette - Not used +// Flags - Effect is optimized for use on 1D LED strips. +// Defaults - Selected Die set to 0xFF (USER_ANY_DIE) +static const char _data_FX_MODE_SIMPLE_DIE[] PROGMEM = + "DieSimple@,,Selected Die;!,!;;1;c1=255"; + +static uint16_t pulse_roll() { + auto roll = GetLastRollForSegment(); + if (roll.state != pixels::RollState::ON_FACE) { + return mode_breath(); + } else { + uint16_t ret = mode_blends(); + uint16_t num_segments = float(roll.current_face + 1) / 20.0 * SEGLEN; + for (int i = num_segments; i < SEGLEN; i++) { + SEGMENT.setPixelColor(i, SEGCOLOR(1)); + } + return ret; + } +} +static const char _data_FX_MODE_PULSE_DIE[] PROGMEM = + "DiePulse@!,!,Selected Die;!,!;!;1;sx=24,pal=50,c1=255"; + +static uint16_t check_roll() { + auto roll = GetLastRollForSegment(); + if (roll.state != pixels::RollState::ON_FACE) { + return running_copy(SEGCOLOR(0), SEGCOLOR(2)); + } else { + if (roll.current_face + 1 >= SEGMENT.custom2) { + return mode_glitter(); + } else { + return mode_gravcenter(); + } + } +} +static const char _data_FX_MODE_CHECK_DIE[] PROGMEM = + "DieCheck@!,!,Selected Die,Target Roll;1,2,3;!;1;pal=0,ix=128,m12=2,si=0,c1=255,c2=10"; diff --git a/usermods/pixels_dice_tray/mqtt_client/mqtt_logger.py b/usermods/pixels_dice_tray/mqtt_client/mqtt_logger.py new file mode 100644 index 000000000..a3e4aa014 --- /dev/null +++ b/usermods/pixels_dice_tray/mqtt_client/mqtt_logger.py @@ -0,0 +1,104 @@ +#!/usr/bin/env python +import argparse +import json +import os +from pathlib import Path +import time + +# Dependency installed with `pip install paho-mqtt`. +# https://pypi.org/project/paho-mqtt/ +import paho.mqtt.client as mqtt + +state = {"label": "None"} + + +# Define MQTT callbacks +def on_connect(client, userdata, connect_flags, reason_code, properties): + print("Connected with result code " + str(reason_code)) + state["start_time"] = None + client.subscribe(f"{state['root_topic']}#") + + +def on_message(client, userdata, msg): + if msg.topic.endswith("roll_label"): + state["label"] = msg.payload.decode("ascii") + print(f"Label set to {state['label']}") + elif msg.topic.endswith("roll"): + json_str = msg.payload.decode("ascii") + msg_data = json.loads(json_str) + # Convert the relative timestamps reported to the dice to an approximate absolute time. + # The "last_time" check is to detect if the ESP32 was restarted or the counter rolled over. + if state["start_time"] is None or msg_data["time"] < state["last_time"]: + state["start_time"] = time.time() - (msg_data["time"] / 1000.0) + state["last_time"] = msg_data["time"] + timestamp = state["start_time"] + (msg_data["time"] / 1000.0) + state["csv_fd"].write( + f"{timestamp:.3f}, {msg_data['name']}, {state['label']}, {msg_data['state']}, {msg_data['val']}\n" + ) + state["csv_fd"].flush() + if msg_data["state"] == 1: + print( + f"{timestamp:.3f}: {msg_data['name']} rolled {msg_data['val']}") + + +def main(): + parser = argparse.ArgumentParser( + description="Log die rolls from WLED MQTT events to CSV.") + + # IP address (with a default value) + parser.add_argument( + "--host", + type=str, + default="127.0.0.1", + help="Host address of broker (default: 127.0.0.1)", + ) + parser.add_argument( + "--port", type=int, default=1883, help="Broker TCP port (default: 1883)" + ) + parser.add_argument("--user", type=str, help="Optional MQTT username") + parser.add_argument("--password", type=str, help="Optional MQTT password") + parser.add_argument( + "--topic", + type=str, + help="Optional MQTT topic to listen to. For example if topic is 'wled/e5a658/dice/', subscript to to 'wled/e5a658/dice/#'. By default, listen to all topics looking for ones that end in 'roll_label' and 'roll'.", + ) + parser.add_argument( + "-o", + "--output-dir", + type=Path, + default=Path(__file__).absolute().parent / "logs", + help="Directory to log to", + ) + args = parser.parse_args() + + timestr = time.strftime("%Y-%m-%d") + os.makedirs(args.output_dir, exist_ok=True) + state["csv_fd"] = open(args.output_dir / f"roll_log_{timestr}.csv", "a") + + # Create `an MQTT client + client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) + + # Set MQTT callbacks + client.on_connect = on_connect + client.on_message = on_message + + if args.user and args.password: + client.username_pw_set(args.user, args.password) + + state["root_topic"] = "" + + # Connect to the MQTT broker + client.connect(args.host, args.port, 60) + + try: + while client.loop(timeout=1.0) == mqtt.MQTT_ERR_SUCCESS: + time.sleep(0.1) + except KeyboardInterrupt: + exit(0) + + print("Connection Failure") + exit(1) + + +if __name__ == "__main__": + main() diff --git a/usermods/pixels_dice_tray/mqtt_client/mqtt_plotter.py b/usermods/pixels_dice_tray/mqtt_client/mqtt_plotter.py new file mode 100644 index 000000000..3ce0b7bf1 --- /dev/null +++ b/usermods/pixels_dice_tray/mqtt_client/mqtt_plotter.py @@ -0,0 +1,69 @@ +import argparse +from http import server +import os +from pathlib import Path +import socketserver + +import pandas as pd +import plotly.express as px + +# python -m http.server 8000 --directory /tmp/ + + +def main(): + parser = argparse.ArgumentParser( + description="Generate an html plot of rolls captured by mqtt_logger.py") + parser.add_argument("input_file", type=Path, help="Log file to plot") + parser.add_argument( + "-s", + "--start-server", + action="store_true", + help="After generating the plot, run a webserver pointing to it", + ) + parser.add_argument( + "-o", + "--output-dir", + type=Path, + default=Path(__file__).absolute().parent / "logs", + help="Directory to log to", + ) + args = parser.parse_args() + + df = pd.read_csv( + args.input_file, names=["timestamp", "die", "label", "state", "roll"] + ) + + df_filt = df[df["state"] == 1] + + time = (df_filt["timestamp"] - df_filt["timestamp"].min()) / 60 / 60 + + fig = px.bar( + df_filt, + x=time, + y="roll", + color="label", + labels={ + "x": "Game Time (min)", + }, + title=f"Roll Report: {args.input_file.name}", + ) + + output_path = args.output_dir / (args.input_file.stem + ".html") + + fig.write_html(output_path) + if args.start_server: + PORT = 8000 + os.chdir(args.output_dir) + try: + with socketserver.TCPServer( + ("", PORT), server.SimpleHTTPRequestHandler + ) as httpd: + print( + f"Serving HTTP on http://0.0.0.0:{PORT}/{output_path.name}") + httpd.serve_forever() + except KeyboardInterrupt: + pass + + +if __name__ == "__main__": + main() diff --git a/usermods/pixels_dice_tray/mqtt_client/requirements.txt b/usermods/pixels_dice_tray/mqtt_client/requirements.txt new file mode 100644 index 000000000..8fb305c7e --- /dev/null +++ b/usermods/pixels_dice_tray/mqtt_client/requirements.txt @@ -0,0 +1,2 @@ +plotly-express +paho-mqtt \ No newline at end of file diff --git a/usermods/pixels_dice_tray/pixels_dice_tray.h b/usermods/pixels_dice_tray/pixels_dice_tray.h new file mode 100644 index 000000000..238af314e --- /dev/null +++ b/usermods/pixels_dice_tray/pixels_dice_tray.h @@ -0,0 +1,535 @@ +#pragma once + +#include // https://github.com/axlan/arduino-pixels-dice +#include "wled.h" + +#include "dice_state.h" +#include "led_effects.h" +#include "tft_menu.h" + +// Set this parameter to rotate the display. 1-3 rotate by 90,180,270 degrees. +#ifndef USERMOD_PIXELS_DICE_TRAY_ROTATION + #define USERMOD_PIXELS_DICE_TRAY_ROTATION 0 +#endif + +// How often we are redrawing screen +#ifndef USERMOD_PIXELS_DICE_TRAY_REFRESH_RATE_MS + #define USERMOD_PIXELS_DICE_TRAY_REFRESH_RATE_MS 200 +#endif + +// Time with no updates before screen turns off (-1 to disable) +#ifndef USERMOD_PIXELS_DICE_TRAY_TIMEOUT_MS + #define USERMOD_PIXELS_DICE_TRAY_TIMEOUT_MS 5 * 60 * 1000 +#endif + +// Duration of each search for BLE devices. +#ifndef BLE_SCAN_DURATION_SEC + #define BLE_SCAN_DURATION_SEC 4 +#endif + +// Time between searches for BLE devices. +#ifndef BLE_TIME_BETWEEN_SCANS_SEC + #define BLE_TIME_BETWEEN_SCANS_SEC 5 +#endif + +#define WLED_DEBOUNCE_THRESHOLD \ + 50 // only consider button input of at least 50ms as valid (debouncing) +#define WLED_LONG_PRESS \ + 600 // long press if button is released after held for at least 600ms +#define WLED_DOUBLE_PRESS \ + 350 // double press if another press within 350ms after a short press + +class PixelsDiceTrayUsermod : public Usermod { + private: + bool enabled = true; + + DiceUpdate dice_update; + + // Settings + uint32_t ble_scan_duration_sec = BLE_SCAN_DURATION_SEC; + unsigned rotation = USERMOD_PIXELS_DICE_TRAY_ROTATION; + DiceSettings dice_settings; + +#if USING_TFT_DISPLAY + MenuController menu_ctrl; +#endif + + static void center(String& line, uint8_t width) { + int len = line.length(); + if (len < width) + for (byte i = (width - len) / 2; i > 0; i--) + line = ' ' + line; + for (byte i = line.length(); i < width; i++) + line += ' '; + } + + // NOTE: THIS MOD DOES NOT SUPPORT CHANGING THE SPI PINS FROM THE UI! The + // TFT_eSPI library requires that they are compiled in. + static void SetSPIPinsFromMacros() { +#if USING_TFT_DISPLAY + spi_mosi = TFT_MOSI; + // Done in TFT library. + if (TFT_MISO == TFT_MOSI) { + spi_miso = -1; + } + spi_sclk = TFT_SCLK; +#endif + } + + void UpdateDieNames( + const std::array& new_die_names) { + for (size_t i = 0; i < MAX_NUM_DICE; i++) { + // If the saved setting was a wildcard, and that connected to a die, use + // the new name instead of the wildcard. Saving this "locks" the name in. + bool overriden_wildcard = + new_die_names[i] == "*" && dice_update.connected_die_ids[i] != 0; + if (!overriden_wildcard && + new_die_names[i] != dice_settings.configured_die_names[i]) { + dice_settings.configured_die_names[i] = new_die_names[i]; + dice_update.connected_die_ids[i] = 0; + last_die_events[i] = pixels::RollEvent(); + } + } + } + + public: + PixelsDiceTrayUsermod() +#if USING_TFT_DISPLAY + : menu_ctrl(&dice_settings) +#endif + { + } + + // 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() override { + DEBUG_PRINTLN(F("DiceTray: init")); +#if USING_TFT_DISPLAY + SetSPIPinsFromMacros(); + PinManagerPinType spiPins[] = { + {spi_mosi, true}, {spi_miso, false}, {spi_sclk, true}}; + if (!pinManager.allocateMultiplePins(spiPins, 3, PinOwner::HW_SPI)) { + enabled = false; + } else { + PinManagerPinType displayPins[] = { + {TFT_CS, true}, {TFT_DC, true}, {TFT_RST, true}, {TFT_BL, true}}; + if (!pinManager.allocateMultiplePins( + displayPins, sizeof(displayPins) / sizeof(PinManagerPinType), + PinOwner::UM_FourLineDisplay)) { + pinManager.deallocateMultiplePins(spiPins, 3, PinOwner::HW_SPI); + enabled = false; + } + } + + if (!enabled) { + DEBUG_PRINTLN(F("DiceTray: TFT Display pin allocations failed.")); + return; + } +#endif + + // Need to enable WiFi sleep: + // "E (1513) wifi:Error! Should enable WiFi modem sleep when both WiFi and Bluetooth are enabled!!!!!!" + noWifiSleep = false; + + // Get the mode indexes that the effects are registered to. + FX_MODE_SIMPLE_D20 = strip.addEffect(255, &simple_roll, _data_FX_MODE_SIMPLE_DIE); + FX_MODE_PULSE_D20 = strip.addEffect(255, &pulse_roll, _data_FX_MODE_PULSE_DIE); + FX_MODE_CHECK_D20 = strip.addEffect(255, &check_roll, _data_FX_MODE_CHECK_DIE); + DIE_LED_MODES = {FX_MODE_SIMPLE_D20, FX_MODE_PULSE_D20, FX_MODE_CHECK_D20}; + + // Start a background task scanning for dice. + // On completion the discovered dice are connected to. + pixels::ScanForDice(ble_scan_duration_sec, BLE_TIME_BETWEEN_SCANS_SEC); + +#if USING_TFT_DISPLAY + menu_ctrl.Init(rotation); +#endif + } + + /* + * connected() is called every time the WiFi is (re)connected + * Use it to initialize network interfaces + */ + void connected() override { + // Serial.println("Connected to WiFi!"); + } + + /* + * 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() override { + static long last_loop_time = 0; + static long last_die_connected_time = millis(); + + char mqtt_topic_buffer[MQTT_MAX_TOPIC_LEN + 16]; + char mqtt_data_buffer[128]; + + // Check if we time interval for redrawing passes. + if (millis() - last_loop_time < USERMOD_PIXELS_DICE_TRAY_REFRESH_RATE_MS) { + return; + } + last_loop_time = millis(); + + // Update dice_list with the connected dice + pixels::ListDice(dice_update.dice_list); + // Get all the roll/battery updates since the last loop + pixels::GetDieRollUpdates(dice_update.roll_updates); + pixels::GetDieBatteryUpdates(dice_update.battery_updates); + + // Go through list of connected die. + // TODO: Blacklist die that are connected to, but don't match the configured + // names. + std::array die_connected = {false, false}; + for (auto die_id : dice_update.dice_list) { + bool matched = false; + // First check if we've already matched this ID to a connected die. + for (size_t i = 0; i < MAX_NUM_DICE; i++) { + if (die_id == dice_update.connected_die_ids[i]) { + die_connected[i] = true; + matched = true; + break; + } + } + + // If this isn't already matched, check if its name matches an expected name. + if (!matched) { + auto die_name = pixels::GetDieDescription(die_id).name; + for (size_t i = 0; i < MAX_NUM_DICE; i++) { + if (0 == dice_update.connected_die_ids[i] && + die_name == dice_settings.configured_die_names[i]) { + dice_update.connected_die_ids[i] = die_id; + die_connected[i] = true; + matched = true; + DEBUG_PRINTF_P(PSTR("DiceTray: %u (%s) connected.\n"), i, + die_name.c_str()); + break; + } + } + + // If it doesn't match any expected names, check if there's any wildcards to match. + if (!matched) { + auto description = pixels::GetDieDescription(die_id); + for (size_t i = 0; i < MAX_NUM_DICE; i++) { + if (dice_settings.configured_die_names[i] == "*") { + dice_update.connected_die_ids[i] = die_id; + die_connected[i] = true; + dice_settings.configured_die_names[i] = die_name; + DEBUG_PRINTF_P(PSTR("DiceTray: %u (%s) connected as wildcard.\n"), + i, die_name.c_str()); + break; + } + } + } + } + } + + // Clear connected die that aren't still present. + bool all_found = true; + bool none_found = true; + for (size_t i = 0; i < MAX_NUM_DICE; i++) { + if (!die_connected[i]) { + if (dice_update.connected_die_ids[i] != 0) { + dice_update.connected_die_ids[i] = 0; + last_die_events[i] = pixels::RollEvent(); + DEBUG_PRINTF_P(PSTR("DiceTray: %u disconnected.\n"), i); + } + + if (!dice_settings.configured_die_names[i].empty()) { + all_found = false; + } + } else { + none_found = false; + } + } + + // Update last_die_events + for (const auto& roll : dice_update.roll_updates) { + for (size_t i = 0; i < MAX_NUM_DICE; i++) { + if (dice_update.connected_die_ids[i] == roll.first) { + last_die_events[i] = roll.second; + } + } + if (WLED_MQTT_CONNECTED) { + snprintf(mqtt_topic_buffer, sizeof(mqtt_topic_buffer), PSTR("%s/%s"), + mqttDeviceTopic, "dice/roll"); + const char* name = pixels::GetDieDescription(roll.first).name.c_str(); + snprintf(mqtt_data_buffer, sizeof(mqtt_data_buffer), + "{\"name\":\"%s\",\"state\":%d,\"val\":%d,\"time\":%d}", name, + int(roll.second.state), roll.second.current_face + 1, + roll.second.timestamp); + mqtt->publish(mqtt_topic_buffer, 0, false, mqtt_data_buffer); + } + } + +#if USERMOD_PIXELS_DICE_TRAY_TIMEOUT_MS > 0 && USING_TFT_DISPLAY + // If at least one die is configured, but none are found + if (none_found) { + if (millis() - last_die_connected_time > + USERMOD_PIXELS_DICE_TRAY_TIMEOUT_MS) { + // Turn off LEDs and backlight and go to sleep. + // Since none of the wake up pins are wired up, expect to sleep + // until power cycle or reset, so don't need to handle normal + // wakeup. + bri = 0; + applyFinalBri(); + menu_ctrl.EnableBacklight(false); + gpio_hold_en((gpio_num_t)TFT_BL); + gpio_deep_sleep_hold_en(); + esp_deep_sleep_start(); + } + } else { + last_die_connected_time = millis(); + } +#endif + + if (pixels::IsScanning() && all_found) { + DEBUG_PRINTF_P(PSTR("DiceTray: All dice found. Stopping search.\n")); + pixels::StopScanning(); + } else if (!pixels::IsScanning() && !all_found) { + DEBUG_PRINTF_P(PSTR("DiceTray: Resuming dice search.\n")); + pixels::ScanForDice(ble_scan_duration_sec, BLE_TIME_BETWEEN_SCANS_SEC); + } +#if USING_TFT_DISPLAY + menu_ctrl.Update(dice_update); +#endif + } + + /* + * 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) override { + JsonObject user = root["u"]; + if (user.isNull()) + user = root.createNestedObject("u"); + + JsonArray lightArr = user.createNestedArray("DiceTray"); // name + lightArr.add(enabled ? F("installed") : F("disabled")); // unit + } + + /* + * 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) override { + // root["user0"] = userVar0; + } + + /* + * 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) override { + // userVar0 = root["user0"] | userVar0; //if "user0" key exists in JSON, + // update, else keep old value if (root["bri"] == 255) + // Serial.println(F("Don't burn down your garage!")); + } + + /* + * 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) override { + JsonObject top = root.createNestedObject("DiceTray"); + top["ble_scan_duration"] = ble_scan_duration_sec; + top["die_0"] = dice_settings.configured_die_names[0]; + top["die_1"] = dice_settings.configured_die_names[1]; +#if USING_TFT_DISPLAY + top["rotation"] = rotation; + JsonArray pins = top.createNestedArray("pin"); + pins.add(TFT_CS); + pins.add(TFT_DC); + pins.add(TFT_RST); + pins.add(TFT_BL); +#endif + } + + void appendConfigData() override { + // Slightly annoying that you can't put text before an element. + // The an item on the usermod config page has the following HTML: + // ```html + // Die 0 + // + // + // ``` + // addInfo let's you add data before or after the two input fields. + // + // To work around this, add info text to the end of the preceding item. + // + // See addInfo in wled00/data/settings_um.htm for details on what this function does. + oappend(SET_F( + "addInfo('DiceTray:ble_scan_duration',1,'

Set to \"*\" to " + "connect to any die.
Leave Blank to disable.

Saving will replace \"*\" with die names.','');")); +#if USING_TFT_DISPLAY + oappend(SET_F("ddr=addDropdown('DiceTray','rotation');")); + oappend(SET_F("addOption(ddr,'0 deg',0);")); + oappend(SET_F("addOption(ddr,'90 deg',1);")); + oappend(SET_F("addOption(ddr,'180 deg',2);")); + oappend(SET_F("addOption(ddr,'270 deg',3);")); + oappend(SET_F( + "addInfo('DiceTray:rotation',1,'
DO NOT CHANGE " + "SPI PINS.
CHANGES ARE IGNORED.','');")); + oappend(SET_F("addInfo('TFT:pin[]',0,'','SPI CS');")); + oappend(SET_F("addInfo('TFT:pin[]',1,'','SPI DC');")); + oappend(SET_F("addInfo('TFT:pin[]',2,'','SPI RST');")); + oappend(SET_F("addInfo('TFT:pin[]',3,'','SPI BL');")); +#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 :) + */ + bool readFromConfig(JsonObject& root) override { + // we look for JSON object: + // {"DiceTray":{"rotation":0,"font_size":1}} + JsonObject top = root["DiceTray"]; + if (top.isNull()) { + DEBUG_PRINTLN(F("DiceTray: No config found. (Using defaults.)")); + return false; + } + + if (top.containsKey("die_0") && top.containsKey("die_1")) { + const std::array new_die_names{ + top["die_0"], top["die_1"]}; + UpdateDieNames(new_die_names); + } else { + DEBUG_PRINTLN(F("DiceTray: No die names found.")); + } + +#if USING_TFT_DISPLAY + unsigned new_rotation = min(top["rotation"] | rotation, 3u); + + // Restore the SPI pins to their compiled in defaults. + SetSPIPinsFromMacros(); + + if (new_rotation != rotation) { + rotation = new_rotation; + menu_ctrl.Init(rotation); + } + + // Update with any modified settings. + menu_ctrl.Redraw(); +#endif + + // use "return !top["newestParameter"].isNull();" when updating Usermod with + // new features + return !top["DiceTray"].isNull(); + } + + /** + * handleButton() can be used to override default button behaviour. Returning true + * will prevent button working in a default way. + * Replicating button.cpp + */ +#if USING_TFT_DISPLAY + bool handleButton(uint8_t b) override { + if (!enabled || b > 1 // buttons 0,1 only + || buttonType[b] == BTN_TYPE_SWITCH || buttonType[b] == BTN_TYPE_NONE || + buttonType[b] == BTN_TYPE_RESERVED || + buttonType[b] == BTN_TYPE_PIR_SENSOR || + buttonType[b] == BTN_TYPE_ANALOG || + buttonType[b] == BTN_TYPE_ANALOG_INVERTED) { + return false; + } + + unsigned long now = millis(); + static bool buttonPressedBefore[2] = {false}; + static bool buttonLongPressed[2] = {false}; + static unsigned long buttonPressedTime[2] = {0}; + static unsigned long buttonWaitTime[2] = {0}; + + //momentary button logic + if (!buttonLongPressed[b] && isButtonPressed(b)) { //pressed + if (!buttonPressedBefore[b]) { + buttonPressedTime[b] = now; + } + buttonPressedBefore[b] = true; + + if (now - buttonPressedTime[b] > WLED_LONG_PRESS) { //long press + menu_ctrl.HandleButton(ButtonType::LONG, b); + buttonLongPressed[b] = true; + return true; + } + } else if (!isButtonPressed(b) && buttonPressedBefore[b]) { //released + + long dur = now - buttonPressedTime[b]; + if (dur < WLED_DEBOUNCE_THRESHOLD) { + buttonPressedBefore[b] = false; + return true; + } //too short "press", debounce + + bool doublePress = buttonWaitTime[b]; //did we have short press before? + buttonWaitTime[b] = 0; + + if (!buttonLongPressed[b]) { //short press + // if this is second release within 350ms it is a double press (buttonWaitTime!=0) + if (doublePress) { + menu_ctrl.HandleButton(ButtonType::DOUBLE, b); + } else { + buttonWaitTime[b] = now; + } + } + buttonPressedBefore[b] = false; + buttonLongPressed[b] = false; + } + // if 350ms elapsed since last press/release it is a short press + if (buttonWaitTime[b] && now - buttonWaitTime[b] > WLED_DOUBLE_PRESS && + !buttonPressedBefore[b]) { + buttonWaitTime[b] = 0; + menu_ctrl.HandleButton(ButtonType::SINGLE, b); + } + + return true; + } +#endif + + /* + * 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_PIXELS_DICE_TRAY; } + + // 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! +}; diff --git a/usermods/pixels_dice_tray/platformio_override.ini.sample b/usermods/pixels_dice_tray/platformio_override.ini.sample new file mode 100644 index 000000000..844ff1bb2 --- /dev/null +++ b/usermods/pixels_dice_tray/platformio_override.ini.sample @@ -0,0 +1,114 @@ +[platformio] +default_envs = t_qt_pro_8MB, esp32s3dev_8MB_qspi + +# ------------------------------------------------------------------------------ +# T-QT Pro 8MB with integrated 128x128 TFT screen +# ------------------------------------------------------------------------------ +[env:t_qt_pro_8MB] +board = esp32-s3-devkitc-1 ;; generic dev board; +platform = ${esp32s3.platform} +upload_speed = 921600 +build_unflags = ${common.build_unflags} +board_build.partitions = ${esp32.large_partitions} +board_build.f_flash = 80000000L +board_build.flash_mode = qio +monitor_filters = esp32_exception_decoder +build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=T-QT-PRO-8MB + -D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0 + -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") + + -D USERMOD_PIXELS_DICE_TRAY ;; Enables this UserMod + -D USERMOD_PIXELS_DICE_TRAY_BL_ACTIVE_LOW=1 + -D USERMOD_PIXELS_DICE_TRAY_ROTATION=2 + + ;-D WLED_DEBUG + ;;;;;;;;;;;;;;;;;; TFT_eSPI Settings ;;;;;;;;;;;;;;;;;;;;;;;; + ;-DCORE_DEBUG_LEVEL=ARDUHAL_LOG_LEVEL_DEBUG + -D USER_SETUP_LOADED=1 + + ; Define the TFT driver, pins etc. from: https://github.com/Bodmer/TFT_eSPI/blob/master/User_Setups/Setup211_LilyGo_T_QT_Pro_S3.h + ; GC9A01 128 x 128 display with no chip select line + -D USER_SETUP_ID=211 + -D GC9A01_DRIVER=1 + -D TFT_WIDTH=128 + -D TFT_HEIGHT=128 + + -D TFT_BACKLIGHT_ON=0 + -D TFT_ROTATION=3 + -D CGRAM_OFFSET=1 + + -D TFT_MISO=-1 + -D TFT_MOSI=2 + -D TFT_SCLK=3 + -D TFT_CS=5 + -D TFT_DC=6 + -D TFT_RST=1 + -D TFT_BL=10 + -D LOAD_GLCD=1 + -D LOAD_FONT2=1 + -D LOAD_FONT4=1 + -D LOAD_FONT6=1 + -D LOAD_FONT7=1 + -D LOAD_FONT8=1 + -D LOAD_GFXFF=1 + ; Avoid SPIFFS dependancy that was causing compile issues. + ;-D SMOOTH_FONT=1 + -D SPI_FREQUENCY=40000000 + -D SPI_READ_FREQUENCY=20000000 + -D SPI_TOUCH_FREQUENCY=2500000 + +lib_deps = ${esp32s3.lib_deps} + ${esp32.AR_lib_deps} + ESP32 BLE Arduino + bodmer/TFT_eSPI @ 2.5.43 + axlan/pixels-dice-interface @ 1.2.0 + +# ------------------------------------------------------------------------------ +# ESP32S3 dev board with 8MB flash and no extended RAM. +# ------------------------------------------------------------------------------ +[env:esp32s3dev_8MB_qspi] +board = esp32-s3-devkitc-1 ;; generic dev board; +platform = ${esp32s3.platform} +upload_speed = 921600 +build_unflags = ${common.build_unflags} +board_build.partitions = ${esp32.large_partitions} +board_build.f_flash = 80000000L +board_build.flash_mode = qio +monitor_filters = esp32_exception_decoder +build_flags = ${common.build_flags} ${esp32s3.build_flags} -D WLED_RELEASE_NAME=ESP32-S3_8MB_qspi + -D CONFIG_LITTLEFS_FOR_IDF_3_2 -D WLED_WATCHDOG_TIMEOUT=0 + -D ARDUINO_USB_CDC_ON_BOOT=1 -D ARDUINO_USB_MODE=1 ;; for boards with USB-OTG connector only (USBCDC or "TinyUSB") + + -D USERMOD_PIXELS_DICE_TRAY ;; Enables this UserMod + + ;-D WLED_DEBUG +lib_deps = ${esp32s3.lib_deps} + ${esp32.AR_lib_deps} + ESP32 BLE Arduino + axlan/pixels-dice-interface @ 1.2.0 + +# ------------------------------------------------------------------------------ +# ESP32 dev board without screen +# ------------------------------------------------------------------------------ +# THIS DOES NOT WORK!!!!!! +# While it builds and programs onto the device, I ran into a series of issues +# trying to actually run. +# Right after the AP init there's an allocation exception which claims to be in +# the UDP server. There seems to be a ton of heap remaining, so the exact error +# might be a red herring. +# It appears that the BLE scanning task is conflicting with the networking tasks. +# I was successfully running simple applications with the pixels-dice-interface +# on ESP32 dev boards, so it may be an issue with too much being scheduled in +# parallel. Also not clear exactly what difference between the ESP32 and the +# ESP32S3 would be causing this, though they do run different BLE versions. +# May be related to some of the issues discussed in: +# https://github.com/Aircoookie/WLED/issues/1382 +; [env:esp32dev] +; build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=ESP32 +; ; Enable Pixels dice mod +; -D USERMOD_PIXELS_DICE_TRAY +; lib_deps = ${esp32.lib_deps} +; ESP32 BLE Arduino +; axlan/pixels-dice-interface @ 1.2.0 +; ; Tiny file system partition, no core dump to fit BLE library. +; board_build.partitions = usermods/pixels_dice_tray/WLED_ESP32_4MB_64KB_FS.csv diff --git a/usermods/pixels_dice_tray/roll_info.h b/usermods/pixels_dice_tray/roll_info.h new file mode 100644 index 000000000..fd8203134 --- /dev/null +++ b/usermods/pixels_dice_tray/roll_info.h @@ -0,0 +1,107 @@ +#pragma once + +#include + +extern TFT_eSPI tft; + +// The following functions are generated by: +// usermods/pixels_dice_tray/generate_roll_info.py + +// GENERATED +static void PrintRoll0() { + tft.setTextColor(63488); + tft.println("Barb Chain"); + tft.setTextColor(65535); + tft.println("Atk/CMD 12"); + tft.println("Range: 70"); + tft.setTextSize(1); + tft.println("Summon 3 chains. Make"); + tft.println("a melee atk 1d6 or a "); + tft.println("trip CMD=AT. On a hit"); + tft.println("make Will save or sha"); + tft.println("ken 1d4 rnds."); +} + +static void PrintRoll1() { + tft.setTextColor(2016); + tft.println("Saves"); + tft.setTextColor(65535); + tft.println("FORT 8"); + tft.println("REFLEX 8"); + tft.println("WILL 9"); +} + +static void PrintRoll2() { + tft.println("Skill"); +} + +static void PrintRoll3() { + tft.println("Attack"); + tft.println("Melee +9"); + tft.println("Range +6"); +} + +static void PrintRoll4() { + tft.println("Cure"); + tft.println("Lit 1d8+5"); + tft.println("Mod 2d8+9"); + tft.println("Ser 3d8+9"); +} + +static void PrintRoll5() { + tft.println("Concentrat"); + tft.println("+15"); + tft.setTextSize(1); + tft.println("Defensive 15+2*SP_LV"); + tft.println("Dmg 10+DMG+SP_LV"); + tft.println("Grapple 10+CMB+SP_LV"); +} + +static const char* GetRollName(uint8_t key) { + switch (key) { + case 0: + return "Barb Chain"; + case 1: + return "Saves"; + case 2: + return "Skill"; + case 3: + return "Attack"; + case 4: + return "Cure"; + case 5: + return "Concentrate"; + } + return ""; +} + +static void PrintRollInfo(uint8_t key) { + tft.setTextColor(TFT_WHITE); + tft.setCursor(0, 0); + tft.setTextSize(2); + switch (key) { + case 0: + PrintRoll0(); + return; + case 1: + PrintRoll1(); + return; + case 2: + PrintRoll2(); + return; + case 3: + PrintRoll3(); + return; + case 4: + PrintRoll4(); + return; + case 5: + PrintRoll5(); + return; + } + tft.setTextColor(TFT_RED); + tft.setCursor(0, 60); + tft.println("Unknown"); +} + +static constexpr size_t NUM_ROLL_INFOS = 6; diff --git a/usermods/pixels_dice_tray/tft_menu.h b/usermods/pixels_dice_tray/tft_menu.h new file mode 100644 index 000000000..0b8fd8394 --- /dev/null +++ b/usermods/pixels_dice_tray/tft_menu.h @@ -0,0 +1,479 @@ +/** + * Code for using the 128x128 LCD and two buttons on the T-QT Pro as a GUI. + */ +#pragma once + +#ifndef TFT_WIDTH + #warning TFT parameters not specified, not using screen. +#else + #include + #include // https://github.com/axlan/arduino-pixels-dice + #include "wled.h" + + #include "dice_state.h" + #include "roll_info.h" + + #define USING_TFT_DISPLAY 1 + + #ifndef TFT_BL + #define TFT_BL -1 + #endif + +// Bitmask for icon +const uint8_t LIGHTNING_ICON_8X8[] PROGMEM = { + 0b00001111, 0b00010010, 0b00100100, 0b01001111, + 0b10000001, 0b11110010, 0b00010100, 0b00011000, +}; + +TFT_eSPI tft = TFT_eSPI(TFT_WIDTH, TFT_HEIGHT); + +/** + * Print text with box surrounding it. + * + * @param txt Text to draw + * @param color Color for box lines + */ +static void PrintLnInBox(const char* txt, uint32_t color) { + int16_t sx = tft.getCursorX(); + int16_t sy = tft.getCursorY(); + tft.setCursor(sx + 2, sy); + tft.print(txt); + int16_t w = tft.getCursorX() - sx + 1; + tft.println(); + int16_t h = tft.getCursorY() - sy - 1; + tft.drawRect(sx, sy, w, h, color); +} + +/** + * Override the current colors for the selected segment to the defaults for the + * selected die effect. + */ +void SetDefaultColors(uint8_t mode) { + Segment& seg = strip.getFirstSelectedSeg(); + if (mode == FX_MODE_SIMPLE_D20) { + seg.setColor(0, GREEN); + seg.setColor(1, 0); + } else if (mode == FX_MODE_PULSE_D20) { + seg.setColor(0, GREEN); + seg.setColor(1, RED); + } else if (mode == FX_MODE_CHECK_D20) { + seg.setColor(0, RED); + seg.setColor(1, 0); + seg.setColor(2, GREEN); + } +} + +/** + * Get the pointer to the custom2 value for the current LED segment. This is + * used to set the target roll for relevant effects. + */ +static uint8_t* GetCurrentRollTarget() { + return &strip.getFirstSelectedSeg().custom2; +} + +/** + * Class for drawing a histogram of roll results. + */ +class RollCountWidget { + private: + int16_t xs = 0; + int16_t ys = 0; + uint16_t border_color = TFT_RED; + uint16_t bar_color = TFT_GREEN; + uint16_t bar_width = 6; + uint16_t max_bar_height = 60; + unsigned roll_counts[20] = {0}; + unsigned total = 0; + unsigned max_count = 0; + + public: + RollCountWidget(int16_t xs = 0, int16_t ys = 0, + uint16_t border_color = TFT_RED, + uint16_t bar_color = TFT_GREEN, uint16_t bar_width = 6, + uint16_t max_bar_height = 60) + : xs(xs), + ys(ys), + border_color(border_color), + bar_color(bar_color), + bar_width(bar_width), + max_bar_height(max_bar_height) {} + + void Clear() { + memset(roll_counts, 0, sizeof(roll_counts)); + total = 0; + max_count = 0; + } + + unsigned GetNumRolls() const { return total; } + + void AddRoll(unsigned val) { + if (val > 19) { + return; + } + roll_counts[val]++; + total++; + max_count = max(roll_counts[val], max_count); + } + + void Draw() { + // Add 2 pixels to lengths for boarder width. + tft.drawRect(xs, ys, bar_width * 20 + 2, max_bar_height + 2, border_color); + for (size_t i = 0; i < 20; i++) { + if (roll_counts[i] > 0) { + // Scale bar by highest count. + uint16_t bar_height = round(float(roll_counts[i]) / float(max_count) * + float(max_bar_height)); + // Add space between bars + uint16_t padding = (bar_width > 1) ? 1 : 0; + // Need to start from top of bar and draw down + tft.fillRect(xs + 1 + bar_width * i, + ys + 1 + max_bar_height - bar_height, bar_width - padding, + bar_height, bar_color); + } + } + } +}; + +enum class ButtonType { SINGLE, DOUBLE, LONG }; + +// Base class for different menu pages. +class MenuBase { + public: + /** + * Handle new die events and connections. Called even when menu isn't visible. + */ + virtual void Update(const DiceUpdate& dice_update) = 0; + + /** + * Draw menu to the screen. + */ + virtual void Draw(const DiceUpdate& dice_update, bool force_redraw) = 0; + + /** + * Handle button presses if the menu is currently active. + */ + virtual void HandleButton(ButtonType type, uint8_t b) = 0; + + protected: + static DiceSettings* settings; + friend class MenuController; +}; +DiceSettings* MenuBase::settings = nullptr; + +/** + * Menu to show connection status and roll histograms. + */ +class DiceStatusMenu : public MenuBase { + public: + DiceStatusMenu() + : die_roll_counts{RollCountWidget{0, 20, TFT_BLUE, TFT_GREEN, 6, 40}, + RollCountWidget{0, SECTION_HEIGHT + 20, TFT_BLUE, + TFT_GREEN, 6, 40}} {} + + void Update(const DiceUpdate& dice_update) override { + for (size_t i = 0; i < MAX_NUM_DICE; i++) { + const auto die_id = dice_update.connected_die_ids[i]; + const auto connected = die_id != 0; + // Redraw if connection status changed. + die_updated[i] |= die_id != last_die_ids[i]; + last_die_ids[i] = die_id; + + if (connected) { + bool charging = false; + for (const auto& battery : dice_update.battery_updates) { + if (battery.first == die_id) { + if (die_battery[i].battery_level == INVALID_BATTERY || + battery.second.is_charging != die_battery[i].is_charging) { + die_updated[i] = true; + } + die_battery[i] = battery.second; + } + } + + for (const auto& roll : dice_update.roll_updates) { + if (roll.first == die_id && + roll.second.state == pixels::RollState::ON_FACE) { + die_roll_counts[i].AddRoll(roll.second.current_face); + die_updated[i] = true; + } + } + } + } + } + + void Draw(const DiceUpdate& dice_update, bool force_redraw) override { + // This could probably be optimized for partial redraws. + for (size_t i = 0; i < MAX_NUM_DICE; i++) { + const int16_t ys = SECTION_HEIGHT * i; + const auto die_id = dice_update.connected_die_ids[i]; + const auto connected = die_id != 0; + // Screen updates might be slow, yield in case network task needs to do + // work. + yield(); + bool battery_update = + connected && (millis() - last_update[i] > BATTERY_REFRESH_RATE_MS); + if (force_redraw || die_updated[i] || battery_update) { + last_update[i] = millis(); + tft.fillRect(0, ys, TFT_WIDTH, SECTION_HEIGHT, TFT_BLACK); + tft.drawRect(0, ys, TFT_WIDTH, SECTION_HEIGHT, TFT_BLUE); + if (settings->configured_die_names[i].empty()) { + tft.setTextColor(TFT_RED); + tft.setCursor(2, ys + 4); + tft.setTextSize(2); + tft.println("Connection"); + tft.setCursor(2, tft.getCursorY()); + tft.println("Disabled"); + } else if (!connected) { + tft.setTextColor(TFT_RED); + tft.setCursor(2, ys + 4); + tft.setTextSize(2); + tft.println(settings->configured_die_names[i].c_str()); + tft.setCursor(2, tft.getCursorY()); + tft.print("Waiting..."); + } else { + tft.setTextColor(TFT_WHITE); + tft.setCursor(0, ys + 2); + tft.setTextSize(1); + tft.println(settings->configured_die_names[i].c_str()); + tft.print("Cnt "); + tft.print(die_roll_counts[i].GetNumRolls()); + if (die_battery[i].battery_level != INVALID_BATTERY) { + tft.print(" Bat "); + tft.print(die_battery[i].battery_level); + tft.print("%"); + if (die_battery[i].is_charging) { + tft.drawBitmap(tft.getCursorX(), tft.getCursorY(), + LIGHTNING_ICON_8X8, 8, 8, TFT_YELLOW); + } + } + die_roll_counts[i].Draw(); + } + die_updated[i] = false; + } + } + } + + void HandleButton(ButtonType type, uint8_t b) override { + if (type == ButtonType::LONG) { + for (size_t i = 0; i < MAX_NUM_DICE; i++) { + die_roll_counts[i].Clear(); + die_updated[i] = true; + } + } + }; + + private: + static constexpr long BATTERY_REFRESH_RATE_MS = 60 * 1000; + static constexpr int16_t SECTION_HEIGHT = TFT_HEIGHT / MAX_NUM_DICE; + static constexpr uint8_t INVALID_BATTERY = 0xFF; + std::array last_update{0, 0}; + std::array last_die_ids{0, 0}; + std::array die_updated{false, false}; + std::array die_battery = { + pixels::BatteryEvent{INVALID_BATTERY, false}, + pixels::BatteryEvent{INVALID_BATTERY, false}}; + std::array die_roll_counts; +}; + +/** + * Some limited controls for setting the die effects on the current LED + * segment. + */ +class EffectMenu : public MenuBase { + public: + EffectMenu() = default; + + void Update(const DiceUpdate& dice_update) override {} + + void Draw(const DiceUpdate& dice_update, bool force_redraw) override { + // NOTE: This doesn't update automatically if the effect is updated on the + // web UI and vice-versa. + if (force_redraw) { + tft.fillScreen(TFT_BLACK); + uint8_t mode = strip.getFirstSelectedSeg().mode; + if (Contains(DIE_LED_MODES, mode)) { + char lineBuffer[CHAR_WIDTH_BIG + 1]; + extractModeName(mode, JSON_mode_names, lineBuffer, CHAR_WIDTH_BIG); + tft.setTextColor(TFT_WHITE); + tft.setCursor(0, 0); + tft.setTextSize(2); + PrintLnInBox(lineBuffer, (field_idx == 0) ? TFT_BLUE : TFT_BLACK); + if (mode == FX_MODE_CHECK_D20) { + snprintf(lineBuffer, sizeof(lineBuffer), "PASS: %u", + *GetCurrentRollTarget()); + PrintLnInBox(lineBuffer, (field_idx == 1) ? TFT_BLUE : TFT_BLACK); + } + } else { + char lineBuffer[CHAR_WIDTH_SMALL + 1]; + extractModeName(mode, JSON_mode_names, lineBuffer, CHAR_WIDTH_SMALL); + tft.setTextColor(TFT_WHITE); + tft.setCursor(0, 0); + tft.setTextSize(1); + tft.println(lineBuffer); + } + } + } + + /** + * Button 0 navigates up and down the settings for the effect. + * Button 1 changes the value for the selected settings. + * Long pressing a button resets the effect parameters to their defaults for + * the current die effect. + */ + void HandleButton(ButtonType type, uint8_t b) override { + Segment& seg = strip.getFirstSelectedSeg(); + auto mode_itr = + std::find(DIE_LED_MODES.begin(), DIE_LED_MODES.end(), seg.mode); + if (mode_itr != DIE_LED_MODES.end()) { + mode_idx = mode_itr - DIE_LED_MODES.begin(); + } + + if (mode_itr == DIE_LED_MODES.end()) { + seg.setMode(DIE_LED_MODES[mode_idx]); + } else { + if (type == ButtonType::LONG) { + // Need to set mode to different value so defaults are actually loaded. + seg.setMode(0); + seg.setMode(DIE_LED_MODES[mode_idx], true); + SetDefaultColors(DIE_LED_MODES[mode_idx]); + } else if (b == 0) { + field_idx = (field_idx + 1) % DIE_LED_MODE_NUM_FIELDS[mode_idx]; + } else { + if (field_idx == 0) { + mode_idx = (mode_idx + 1) % DIE_LED_MODES.size(); + seg.setMode(DIE_LED_MODES[mode_idx]); + } else if (DIE_LED_MODES[mode_idx] == FX_MODE_CHECK_D20 && + field_idx == 1) { + *GetCurrentRollTarget() = GetLastRoll().current_face + 1; + } + } + } + }; + + private: + static constexpr std::array DIE_LED_MODE_NUM_FIELDS = {1, 1, 2}; + static constexpr size_t CHAR_WIDTH_BIG = 10; + static constexpr size_t CHAR_WIDTH_SMALL = 21; + size_t mode_idx = 0; + size_t field_idx = 0; +}; + +constexpr std::array EffectMenu::DIE_LED_MODE_NUM_FIELDS; + +/** + * Menu for setting the roll label and some info for that roll type. + */ +class InfoMenu : public MenuBase { + public: + InfoMenu() = default; + + void Update(const DiceUpdate& dice_update) override {} + + void Draw(const DiceUpdate& dice_update, bool force_redraw) override { + if (force_redraw) { + tft.fillScreen(TFT_BLACK); + if (settings->roll_label != INVALID_ROLL_VALUE) { + PrintRollInfo(settings->roll_label); + } else { + tft.setTextColor(TFT_RED); + tft.setCursor(0, 60); + tft.setTextSize(2); + tft.println("Set Roll"); + } + } + } + + /** + * Single clicking navigates through the roll types. Button 0 goes down, and + * button 1 goes up with wrapping. + */ + void HandleButton(ButtonType type, uint8_t b) override { + if (settings->roll_label >= NUM_ROLL_INFOS) { + settings->roll_label = 0; + } else if (b == 0) { + settings->roll_label = (settings->roll_label == 0) + ? NUM_ROLL_INFOS - 1 + : settings->roll_label - 1; + } else if (b == 1) { + settings->roll_label = (settings->roll_label + 1) % NUM_ROLL_INFOS; + } + if (WLED_MQTT_CONNECTED) { + char mqtt_topic_buffer[MQTT_MAX_TOPIC_LEN + 16]; + snprintf(mqtt_topic_buffer, sizeof(mqtt_topic_buffer), PSTR("%s/%s"), + mqttDeviceTopic, "dice/settings->roll_label"); + mqtt->publish(mqtt_topic_buffer, 0, false, + GetRollName(settings->roll_label)); + } + }; +}; + +/** + * Interface for the rest of the app to update the menus. + */ +class MenuController { + public: + MenuController(DiceSettings* settings) { MenuBase::settings = settings; } + + void Init(unsigned rotation) { + tft.init(); + tft.setRotation(rotation); + tft.fillScreen(TFT_BLACK); + tft.setTextColor(TFT_RED); + tft.setCursor(0, 60); + tft.setTextDatum(MC_DATUM); + tft.setTextSize(2); + EnableBacklight(true); + + force_redraw = true; + } + + // Set the pin to turn the backlight on or off if available. + static void EnableBacklight(bool enable) { + #if TFT_BL > 0 + #if USERMOD_PIXELS_DICE_TRAY_BL_ACTIVE_LOW + enable = !enable; + #endif + digitalWrite(TFT_BL, enable); + #endif + } + + /** + * Double clicking navigates between menus. Button 0 goes down, and button 1 + * goes up with wrapping. + */ + void HandleButton(ButtonType type, uint8_t b) { + force_redraw = true; + // Switch menus with double click + if (ButtonType::DOUBLE == type) { + if (b == 0) { + current_index = + (current_index == 0) ? menu_ptrs.size() - 1 : current_index - 1; + } else { + current_index = (current_index + 1) % menu_ptrs.size(); + } + } else { + menu_ptrs[current_index]->HandleButton(type, b); + } + } + + void Update(const DiceUpdate& dice_update) { + for (auto menu_ptr : menu_ptrs) { + menu_ptr->Update(dice_update); + } + menu_ptrs[current_index]->Draw(dice_update, force_redraw); + force_redraw = false; + } + + void Redraw() { force_redraw = true; } + + private: + size_t current_index = 0; + bool force_redraw = true; + + DiceStatusMenu status_menu; + EffectMenu effect_menu; + InfoMenu info_menu; + const std::array menu_ptrs = {&status_menu, &effect_menu, + &info_menu}; +}; +#endif diff --git a/wled00/FX.cpp b/wled00/FX.cpp index bded21060..8c24ab166 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7899,18 +7899,21 @@ static const char _data_RESERVED[] PROGMEM = "RSVD"; // add (or replace reserved) effect mode and data into vector // use id==255 to find unallocated gaps (with "Reserved" data string) // if vector size() is smaller than id (single) data is appended at the end (regardless of id) -void WS2812FX::addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name) { +// return the actual id used for the effect or 255 if the add failed. +uint8_t WS2812FX::addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name) { if (id == 255) { // find empty slot for (size_t i=1; i<_mode.size(); i++) if (_modeData[i] == _data_RESERVED) { id = i; break; } } if (id < _mode.size()) { - if (_modeData[id] != _data_RESERVED) return; // do not overwrite alerady added effect + if (_modeData[id] != _data_RESERVED) return 255; // do not overwrite an already added effect _mode[id] = mode_fn; _modeData[id] = mode_name; + return id; } else { _mode.push_back(mode_fn); _modeData.push_back(mode_name); if (_modeCount < _mode.size()) _modeCount++; + return (_mode.size() <= 255) ? _mode.size() - 1 : 255; } } diff --git a/wled00/FX.h b/wled00/FX.h index 57de5df44..b914fec2f 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -771,8 +771,8 @@ class WS2812FX { // 96 bytes setPixelColor(unsigned n, uint32_t c), // paints absolute strip pixel with index n and color c show(void), // initiates LED output setTargetFps(uint8_t fps), - addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name), // add effect to the list; defined in FX.cpp setupEffectData(void); // add default effects to the list; defined in FX.cpp + uint8_t addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name); // add effect to the list; defined in FX.cpp inline void restartRuntime() { for (Segment &seg : _segments) seg.markForReset(); } inline void setTransitionMode(bool t) { for (Segment &seg : _segments) seg.startTransition(t ? _transitionDur : 0); } diff --git a/wled00/const.h b/wled00/const.h index 0ff70e47d..7a08635f3 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -200,6 +200,7 @@ #define USERMOD_ID_INA226 50 //Usermod "usermod_ina226.h" #define USERMOD_ID_AHT10 51 //Usermod "usermod_aht10.h" #define USERMOD_ID_LD2410 52 //Usermod "usermod_ld2410.h" +#define USERMOD_ID_PIXELS_DICE_TRAY 53 //Usermod "pixels_dice_tray.h" //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot diff --git a/wled00/pin_manager.h b/wled00/pin_manager.h index 1cec0550d..2fc003880 100644 --- a/wled00/pin_manager.h +++ b/wled00/pin_manager.h @@ -63,7 +63,8 @@ enum struct PinOwner : uint8_t { UM_PWM_OUTPUTS = USERMOD_ID_PWM_OUTPUTS, // 0x26 // Usermod "usermod_pwm_outputs.h" UM_LDR_DUSK_DAWN = USERMOD_ID_LDR_DUSK_DAWN, // 0x2B // Usermod "usermod_LDR_Dusk_Dawn_v2.h" UM_MAX17048 = USERMOD_ID_MAX17048, // 0x2F // Usermod "usermod_max17048.h" - UM_BME68X = USERMOD_ID_BME68X // 0x31 // Usermod "usermod_bme68x.h -- Uses "standard" HW_I2C pins + UM_BME68X = USERMOD_ID_BME68X, // 0x31 // Usermod "usermod_bme68x.h -- Uses "standard" HW_I2C pins + UM_PIXELS_DICE_TRAY = USERMOD_ID_PIXELS_DICE_TRAY, // 0x35 // Usermod "pixels_dice_tray.h" -- Needs compile time specified 6 pins for display including SPI. }; static_assert(0u == static_cast(PinOwner::None), "PinOwner::None must be zero, so default array initialization works as expected"); diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp index a39aa5f41..f3b05e582 100644 --- a/wled00/usermods_list.cpp +++ b/wled00/usermods_list.cpp @@ -106,6 +106,10 @@ #include "../usermods/ST7789_display/ST7789_Display.h" #endif +#ifdef USERMOD_PIXELS_DICE_TRAY + #include "../usermods/pixels_dice_tray/pixels_dice_tray.h" +#endif + #ifdef USERMOD_SEVEN_SEGMENT #include "../usermods/seven_segment_display/usermod_v2_seven_segment_display.h" #endif @@ -331,6 +335,10 @@ void registerUsermods() usermods.add(new St7789DisplayUsermod()); #endif + #ifdef USERMOD_PIXELS_DICE_TRAY + usermods.add(new PixelsDiceTrayUsermod()); + #endif + #ifdef USERMOD_SEVEN_SEGMENT usermods.add(new SevenSegmentDisplay()); #endif From db5e66a9b092954c457900b4a39247875a3e7383 Mon Sep 17 00:00:00 2001 From: Frank <91616163+softhack007@users.noreply.github.com> Date: Fri, 9 Aug 2024 12:53:41 +0200 Subject: [PATCH 049/142] playing with Fire2012 * speedup: add functions to only blur rows or columns (50% faster) * fire2012: tinkering with bur options. Vertical blur only when slider < 64 (faster); extra blur for slider values >192 (bush burn) --- wled00/FX.cpp | 6 ++++-- wled00/FX.h | 10 ++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index bded21060..b0fac6f81 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -2098,7 +2098,7 @@ uint16_t mode_fire_2012() { // Step 4. Map from heat cells to LED colors for (int j = 0; j < SEGLEN; j++) { - SEGMENT.setPixelColor(indexToVStrip(j, stripNr), ColorFromPalette(SEGPALETTE, MIN(heat[j],240), 255, NOBLEND)); + SEGMENT.setPixelColor(indexToVStrip(j, stripNr), ColorFromPalette(SEGPALETTE, min(heat[j], byte(240)), 255, NOBLEND)); } } }; @@ -2108,7 +2108,9 @@ uint16_t mode_fire_2012() { if (SEGMENT.is2D()) { uint8_t blurAmount = SEGMENT.custom2 >> 2; - SEGMENT.blur(blurAmount); + if (blurAmount > 48) blurAmount += blurAmount-48; // extra blur when slider > 192 (bush burn) + if (blurAmount < 16) SEGMENT.blurCols(SEGMENT.custom2 >> 1); // no side-burn when slider < 64 (faster) + else SEGMENT.blur(blurAmount); } if (it != SEGENV.step) diff --git a/wled00/FX.h b/wled00/FX.h index 57de5df44..98fdbf1fc 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -602,6 +602,16 @@ typedef struct Segment { uint32_t color_from_palette(uint16_t, bool mapping, bool wrap, uint8_t mcol, uint8_t pbri = 255) const; uint32_t color_wheel(uint8_t pos) const; + // 2D Blur: shortcuts for bluring columns or rows only (50% faster than full 2D blur) + inline void blurCols(fract8 blur_amount, bool smear = false) { // blur all columns + const unsigned cols = virtualWidth(); + for (unsigned k = 0; k < cols; k++) blurCol(k, blur_amount, smear); + } + inline void blurRows(fract8 blur_amount, bool smear = false) { // blur all rows + const unsigned rows = virtualHeight(); + for ( unsigned i = 0; i < rows; i++) blurRow(i, blur_amount, smear); + } + // 2D matrix uint16_t virtualWidth(void) const; // segment width in virtual pixels (accounts for groupping and spacing) uint16_t virtualHeight(void) const; // segment height in virtual pixels (accounts for groupping and spacing) From ed5eb2822827399d9dfeb55db9a5068bb2bec8f9 Mon Sep 17 00:00:00 2001 From: Arthur Suzuki Date: Wed, 22 Nov 2023 02:36:06 +0100 Subject: [PATCH 050/142] Added POV image effect Setup is really easy, after first boot and WiFi/LEDs setup: go to wled.local/edit and upload a couple image to WLed's filesystem. Only PNG is supported right now, further support for GIF is planned. The image should be as wide as the 1D segment you want to apply to. When done, go to the Effect page on the UI, select "POV Image" effect. You could also update the image with a post to the JSON-API like this: curl -X POST http://[wled]/json/state -d '{"seg":{"id":0,"fx":114,"f":"/axel.png"}}' The segment should move at around 120RPM (that's 2revolutions per seconds) for an image to showup. More informations and pictures here : https://lumina.toys --- platformio_override.sample.ini | 4 + usermods/pov_display/usermod_pov_display.h | 85 ++++++++++++++++++++++ wled00/const.h | 1 + wled00/usermods_list.cpp | 8 ++ 4 files changed, 98 insertions(+) create mode 100644 usermods/pov_display/usermod_pov_display.h diff --git a/platformio_override.sample.ini b/platformio_override.sample.ini index be959e46a..7f6524922 100644 --- a/platformio_override.sample.ini +++ b/platformio_override.sample.ini @@ -29,6 +29,8 @@ lib_deps = ${esp8266.lib_deps} ; ;gmag11/QuickESPNow @ ~0.7.0 # will also load QuickDebug ; https://github.com/blazoncek/QuickESPNow.git#optional-debug ;; exludes debug library ; ${esp32.AR_lib_deps} ;; used for USERMOD_AUDIOREACTIVE +; bitbank2/PNGdec@^1.0.1 ;; used for POV display uncomment following + build_unflags = ${common.build_unflags} build_flags = ${common.build_flags} ${esp8266.build_flags} ; @@ -152,6 +154,8 @@ build_flags = ${common.build_flags} ${esp8266.build_flags} ; -D TACHO_PIN=33 ; -D PWM_PIN=32 ; +; Use POV Display usermod +; -D USERMOD_POV_DISPLAY ; Use built-in or custom LED as a status indicator (assumes LED is connected to GPIO16) ; -D STATUSLED=16 ; diff --git a/usermods/pov_display/usermod_pov_display.h b/usermods/pov_display/usermod_pov_display.h new file mode 100644 index 000000000..b1fc0dba6 --- /dev/null +++ b/usermods/pov_display/usermod_pov_display.h @@ -0,0 +1,85 @@ +#pragma once +#include "wled.h" +#include + +void * openFile(const char *filename, int32_t *size) { + f = WLED_FS.open(filename); + *size = f.size(); + return &f; +} + +void closeFile(void *handle) { + if (f) f.close(); +} + +int32_t readFile(PNGFILE *pFile, uint8_t *pBuf, int32_t iLen) +{ + int32_t iBytesRead; + iBytesRead = iLen; + File *f = static_cast(pFile->fHandle); + // Note: If you read a file all the way to the last byte, seek() stops working + if ((pFile->iSize - pFile->iPos) < iLen) + iBytesRead = pFile->iSize - pFile->iPos - 1; // <-- ugly work-around + if (iBytesRead <= 0) + return 0; + iBytesRead = (int32_t)f->read(pBuf, iBytesRead); + pFile->iPos = f->position(); + return iBytesRead; +} + +int32_t seekFile(PNGFILE *pFile, int32_t iPosition) +{ + int i = micros(); + File *f = static_cast(pFile->fHandle); + f->seek(iPosition); + pFile->iPos = (int32_t)f->position(); + i = micros() - i; + return pFile->iPos; +} + +void draw(PNGDRAW *pDraw) { + uint16_t usPixels[SEGLEN]; + png.getLineAsRGB565(pDraw, usPixels, PNG_RGB565_LITTLE_ENDIAN, 0xffffffff); + for(int x=0; x < SEGLEN; x++) { + uint16_t color = usPixels[x]; + byte r = ((color >> 11) & 0x1F); + byte g = ((color >> 5) & 0x3F); + byte b = (color & 0x1F); + SEGMENT.setPixelColor(x, RGBW32(r,g,b,0)); + } + strip.show(); +} + +uint16_t mode_pov_image(void) { + const char * filepath = SEGMENT.name; + int rc = png.open(filepath, openFile, closeFile, readFile, seekFile, draw); + if (rc == PNG_SUCCESS) { + rc = png.decode(NULL, 0); + png.close(); + return FRAMETIME; + } + return FRAMETIME; +} + +class PovDisplayUsermod : public Usermod +{ + public: + static const char _data_FX_MODE_POV_IMAGE[] PROGMEM = "POV Image@!;;;1"; + + PNG png; + File f; + + void setup() { + strip.addEffect(255, &mode_pov_image, _data_FX_MODE_POV_IMAGE); + } + + void loop() { + } + + uint16_t getId() + { + return USERMOD_ID_POV_DISPLAY; + } + + void connected() {} +}; diff --git a/wled00/const.h b/wled00/const.h index 0ff70e47d..72e1a3bae 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -200,6 +200,7 @@ #define USERMOD_ID_INA226 50 //Usermod "usermod_ina226.h" #define USERMOD_ID_AHT10 51 //Usermod "usermod_aht10.h" #define USERMOD_ID_LD2410 52 //Usermod "usermod_ld2410.h" +#define USERMOD_ID_POV_DISPLAY 53 //Usermod "usermod_pov_display.h" //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp index a39aa5f41..be2239d65 100644 --- a/wled00/usermods_list.cpp +++ b/wled00/usermods_list.cpp @@ -210,6 +210,10 @@ #include "../usermods/LDR_Dusk_Dawn_v2/usermod_LDR_Dusk_Dawn_v2.h" #endif +#ifdef USERMOD_POV_DISPLAY + #include "../usermods/pov_display/usermod_pov_display.h" +#endif + #ifdef USERMOD_STAIRCASE_WIPE #include "../usermods/stairway_wipe_basic/stairway-wipe-usermod-v2.h" #endif @@ -454,4 +458,8 @@ void registerUsermods() #ifdef USERMOD_LD2410 usermods.add(new LD2410Usermod()); #endif + + #ifdef USERMOD_POV_DISPLAY + usermods.add(new PovDisplayUsermod()); + #endif } From cde5314d416777e255fec5f9b6437705669e6f27 Mon Sep 17 00:00:00 2001 From: jdiamond Date: Sun, 11 Aug 2024 01:23:41 +0000 Subject: [PATCH 051/142] Add "dice" to environment names, and add more information about fitting in a 4MB partition. --- usermods/pixels_dice_tray/README.md | 4 +++- usermods/pixels_dice_tray/platformio_override.ini.sample | 9 +++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/usermods/pixels_dice_tray/README.md b/usermods/pixels_dice_tray/README.md index 5440b225c..6daa4fa72 100644 --- a/usermods/pixels_dice_tray/README.md +++ b/usermods/pixels_dice_tray/README.md @@ -63,6 +63,8 @@ The main purpose of this mod is to support [Pixels Dice](https://gamewithpixels. The only other ESP32 variant I tested was the ESP32-S3, which worked without issue. While there's still concern over the contention between BLE and WiFi for the radio, I haven't noticed any performance impact in practice. The only special behavior that was needed was setting `noWifiSleep = false;` to allow the OS to sleep the WiFi when the BLE is active. +In addition, the BLE stack requires a lot of flash. This build takes 1.9MB with the TFT code, or 1.85MB without it. This makes it too big to fit in the `tools/WLED_ESP32_4MB_256KB_FS.csv` partition layout, and I needed to make a `WLED_ESP32_4MB_64KB_FS.csv` to even fit on 4MB devices. This only has 64KB of file system space, which is functional, but users with more than a handful of presets would run into problems with 64KB only. This means that while 4MB can be supported, larger flash sizes are needed for full functionality. + The basic build of this usermod doesn't require any special hardware. However, the LCD status GUI was specifically designed for the [LILYGO T-QT Pro](https://www.lilygo.cc/products/t-qt-pro). It should be relatively easy to support other displays, though the positioning of the text may need to be adjusted. @@ -245,7 +247,7 @@ This usermod is in support of a particular dice box project, but it would be fai I really wanted to have this work on the original ESP32 boards to lower the barrier to entry, but there were several issues. -First, the BLE stack requires a lot of flash. I had to make a special partitioning plan `WLED_ESP32_4MB_64KB_FS.csv` to even fit the build on 4MB devices. This only has 64KB of file system space, which is limited, but still functional. +First there are the issues with the partition sizes for 4MB mentioned in the [Hardware](#hardware) section. The bigger issue is that the build consistently crashes if the BLE scan task starts up. It's a bit unclear to me exactly what is failing since the backtrace is showing an exception in `new[]` memory allocation in the UDP stack. There appears to be a ton of heap available, so my guess is that this is a synchronization issue of some sort from the tasks running in parallel. I tried messing with the task core affinity a bit but didn't make much progress. It's not really clear what difference between the ESP32S3 and ESP32 would cause this difference. diff --git a/usermods/pixels_dice_tray/platformio_override.ini.sample b/usermods/pixels_dice_tray/platformio_override.ini.sample index 844ff1bb2..b712f8b2e 100644 --- a/usermods/pixels_dice_tray/platformio_override.ini.sample +++ b/usermods/pixels_dice_tray/platformio_override.ini.sample @@ -1,10 +1,10 @@ [platformio] -default_envs = t_qt_pro_8MB, esp32s3dev_8MB_qspi +default_envs = t_qt_pro_8MB_dice, esp32s3dev_8MB_qspi_dice # ------------------------------------------------------------------------------ # T-QT Pro 8MB with integrated 128x128 TFT screen # ------------------------------------------------------------------------------ -[env:t_qt_pro_8MB] +[env:t_qt_pro_8MB_dice] board = esp32-s3-devkitc-1 ;; generic dev board; platform = ${esp32s3.platform} upload_speed = 921600 @@ -66,7 +66,7 @@ lib_deps = ${esp32s3.lib_deps} # ------------------------------------------------------------------------------ # ESP32S3 dev board with 8MB flash and no extended RAM. # ------------------------------------------------------------------------------ -[env:esp32s3dev_8MB_qspi] +[env:esp32s3dev_8MB_qspi_dice] board = esp32-s3-devkitc-1 ;; generic dev board; platform = ${esp32s3.platform} upload_speed = 921600 @@ -103,7 +103,8 @@ lib_deps = ${esp32s3.lib_deps} # ESP32S3 would be causing this, though they do run different BLE versions. # May be related to some of the issues discussed in: # https://github.com/Aircoookie/WLED/issues/1382 -; [env:esp32dev] +; [env:esp32dev_dice] +; extends = env:esp32dev ; build_flags = ${common.build_flags} ${esp32.build_flags} -D WLED_RELEASE_NAME=ESP32 ; ; Enable Pixels dice mod ; -D USERMOD_PIXELS_DICE_TRAY From b73f049484f961da6fb13388506a36410748cb4c Mon Sep 17 00:00:00 2001 From: jdiamond Date: Tue, 13 Aug 2024 04:40:59 +0000 Subject: [PATCH 052/142] Clean up addEffect() changes. --- wled00/FX.cpp | 6 ++++-- wled00/FX.h | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 8c24ab166..fc1d96ce2 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -7909,11 +7909,13 @@ uint8_t WS2812FX::addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name) _mode[id] = mode_fn; _modeData[id] = mode_name; return id; - } else { + } else if(_mode.size() < 255) { // 255 is reserved for indicating the effect wasn't added _mode.push_back(mode_fn); _modeData.push_back(mode_name); if (_modeCount < _mode.size()) _modeCount++; - return (_mode.size() <= 255) ? _mode.size() - 1 : 255; + return _mode.size() - 1; + } else { + return 255; // The vector is full so return 255 } } diff --git a/wled00/FX.h b/wled00/FX.h index b914fec2f..14c19edb7 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -772,7 +772,6 @@ class WS2812FX { // 96 bytes show(void), // initiates LED output setTargetFps(uint8_t fps), setupEffectData(void); // add default effects to the list; defined in FX.cpp - uint8_t addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name); // add effect to the list; defined in FX.cpp inline void restartRuntime() { for (Segment &seg : _segments) seg.markForReset(); } inline void setTransitionMode(bool t) { for (Segment &seg : _segments) seg.startTransition(t ? _transitionDur : 0); } @@ -808,7 +807,8 @@ class WS2812FX { // 96 bytes getActiveSegmentsNum(void), getFirstSelectedSegId(void), getLastActiveSegmentId(void), - getActiveSegsLightCapabilities(bool selectedOnly = false); + getActiveSegsLightCapabilities(bool selectedOnly = false), + addEffect(uint8_t id, mode_ptr mode_fn, const char *mode_name); // add effect to the list; defined in FX.cpp inline uint8_t getBrightness(void) { return _brightness; } // returns current strip brightness inline uint8_t getMaxSegments(void) { return MAX_NUM_SEGMENTS; } // returns maximum number of supported segments (fixed value) From bcf862044a22113207fc72a3dfe82eecf4e46dac Mon Sep 17 00:00:00 2001 From: Frank Date: Tue, 13 Aug 2024 22:21:21 +0200 Subject: [PATCH 053/142] Update wled00.cpp * added #include - this is basically what the preprocessing tool (wled.ino -> wled00.ino.cpp) does * added a comment that Arduino IDE is not supported, use platformIO. --- wled00/wled00.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/wled00/wled00.cpp b/wled00/wled00.cpp index 866543ab9..f3f090715 100644 --- a/wled00/wled00.cpp +++ b/wled00/wled00.cpp @@ -1,9 +1,12 @@ +#include /* * WLED Arduino IDE compatibility file. + * (this is the former wled00.ino) * * Where has everything gone? * - * In April 2020, the project's structure underwent a major change. + * In April 2020, the project's structure underwent a major change. + * We now use the platformIO build system, and building WLED in Arduino IDE is not supported any more. * Global variables are now found in file "wled.h" * Global function declarations are found in "fcn_declare.h" * From 2443e2ec7c6800d2053fb05e6820be4a4440741c Mon Sep 17 00:00:00 2001 From: Frank Date: Wed, 14 Aug 2024 11:16:46 +0200 Subject: [PATCH 054/142] wled00 -> wled_main --- wled00/{wled00.cpp => wled_main.cpp} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename wled00/{wled00.cpp => wled_main.cpp} (100%) diff --git a/wled00/wled00.cpp b/wled00/wled_main.cpp similarity index 100% rename from wled00/wled00.cpp rename to wled00/wled_main.cpp From cec67d8eff1844fe9917685dd0aa39321a5e07db Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Wed, 14 Aug 2024 22:15:48 +0200 Subject: [PATCH 055/142] Const and 2D box blur - added 2D blur --- wled00/FX.cpp | 37 ++++++----- wled00/FX.h | 75 ++++++++++----------- wled00/FX_2Dfcn.cpp | 156 ++++++++++++++++++++++++++++++++++---------- wled00/FX_fcn.cpp | 50 +++++++------- 4 files changed, 204 insertions(+), 114 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index b0fac6f81..944afe641 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -1226,15 +1226,18 @@ uint16_t mode_fireworks() { } SEGMENT.fade_out(128); - bool valid1 = (SEGENV.aux0 < width*height); - bool valid2 = (SEGENV.aux1 < width*height); uint8_t x = SEGENV.aux0%width, y = SEGENV.aux0/width; // 2D coordinates stored in upper and lower byte - uint32_t sv1 = 0, sv2 = 0; - if (valid1) sv1 = SEGMENT.is2D() ? SEGMENT.getPixelColorXY(x, y) : SEGMENT.getPixelColor(SEGENV.aux0); // get spark color - if (valid2) sv2 = SEGMENT.is2D() ? SEGMENT.getPixelColorXY(x, y) : SEGMENT.getPixelColor(SEGENV.aux1); - if (!SEGENV.step) SEGMENT.blur(16); - if (valid1) { if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(x, y, sv1); else SEGMENT.setPixelColor(SEGENV.aux0, sv1); } // restore spark color after blur - if (valid2) { if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(x, y, sv2); else SEGMENT.setPixelColor(SEGENV.aux1, sv2); } // restore old spark color after blur + if (!SEGENV.step) { + // fireworks mode (blur flares) + bool valid1 = (SEGENV.aux0 < width*height); + bool valid2 = (SEGENV.aux1 < width*height); + uint32_t sv1 = 0, sv2 = 0; + if (valid1) sv1 = SEGMENT.is2D() ? SEGMENT.getPixelColorXY(x, y) : SEGMENT.getPixelColor(SEGENV.aux0); // get spark color + if (valid2) sv2 = SEGMENT.is2D() ? SEGMENT.getPixelColorXY(x, y) : SEGMENT.getPixelColor(SEGENV.aux1); + SEGMENT.blur(16); // used in mode_rain() + if (valid1) { if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(x, y, sv1); else SEGMENT.setPixelColor(SEGENV.aux0, sv1); } // restore spark color after blur + if (valid2) { if (SEGMENT.is2D()) SEGMENT.setPixelColorXY(x, y, sv2); else SEGMENT.setPixelColor(SEGENV.aux1, sv2); } // restore old spark color after blur + } for (int i=0; i> 1)) == 0) { @@ -1261,7 +1264,7 @@ uint16_t mode_rain() { SEGENV.step += FRAMETIME; if (SEGENV.call && SEGENV.step > SPEED_FORMULA_L) { SEGENV.step = 1; - if (strip.isMatrix) { + if (SEGMENT.is2D()) { //uint32_t ctemp[width]; //for (int i = 0; i 100 ? 16 : 0); + if (SEGMENT.check3) SEGMENT.blur(16, cols*rows < 100); return FRAMETIME; } // mode_2DBlackHole() -static const char _data_FX_MODE_2DBLACKHOLE[] PROGMEM = "Black Hole@Fade rate,Outer Y freq.,Outer X freq.,Inner X freq.,Inner Y freq.,Solid;!;!;2;pal=11"; +static const char _data_FX_MODE_2DBLACKHOLE[] PROGMEM = "Black Hole@Fade rate,Outer Y freq.,Outer X freq.,Inner X freq.,Inner Y freq.,Solid,,Blur;!;!;2;pal=11"; //////////////////////////// @@ -5636,7 +5639,7 @@ uint16_t mode_2DPulser(void) { // By: ldirko https://edi int y = map((sin8(a * 5) + sin8(a * 4) + sin8(a * 2)), 0, 765, rows-1, 0); SEGMENT.setPixelColorXY(x, y, ColorFromPalette(SEGPALETTE, map(y, 0, rows-1, 0, 255), 255, LINEARBLEND)); - SEGMENT.blur(1 + (SEGMENT.intensity>>4)); + SEGMENT.blur(SEGMENT.intensity>>4); return FRAMETIME; } // mode_2DPulser() @@ -6219,7 +6222,7 @@ uint16_t mode_2Ddriftrose(void) { uint32_t y = (CY + (cos_t(angle) * (beatsin8(i, 0, L*2)-L))) * 255.f; SEGMENT.wu_pixel(x, y, CHSV(i * 10, 255, 255)); } - SEGMENT.blur((SEGMENT.intensity>>4)+1); + SEGMENT.blur(SEGMENT.intensity>>4); return FRAMETIME; } @@ -6475,11 +6478,11 @@ uint16_t mode_2DWaverly(void) { SEGMENT.addPixelColorXY((cols - 1) - i, (rows - 1) - j, ColorFromPalette(SEGPALETTE, map(j, 0, thisMax, 250, 0), 255, LINEARBLEND)); } } - SEGMENT.blur(cols*rows > 100 ? 16 : 0); + if (SEGMENT.check3) SEGMENT.blur(16, cols*rows < 100); return FRAMETIME; } // mode_2DWaverly() -static const char _data_FX_MODE_2DWAVERLY[] PROGMEM = "Waverly@Amplification,Sensitivity;;!;2v;ix=64,si=0"; // Beatsin +static const char _data_FX_MODE_2DWAVERLY[] PROGMEM = "Waverly@Amplification,Sensitivity,,,,,Blur;;!;2v;ix=64,si=0"; // Beatsin #endif // WLED_DISABLE_2D diff --git a/wled00/FX.h b/wled00/FX.h index 98fdbf1fc..276a16703 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -636,7 +636,8 @@ typedef struct Segment { inline void addPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0, bool fast = false) { addPixelColorXY(x, y, RGBW32(r,g,b,w), fast); } inline void addPixelColorXY(int x, int y, CRGB c, bool fast = false) { addPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), fast); } inline void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) { setPixelColorXY(x, y, color_fade(getPixelColorXY(x,y), fade, true)); } - void box_blur(uint16_t i, bool vertical, fract8 blur_amount); // 1D box blur (with weight) + void box_blur(unsigned r = 1U, bool smear = false); // 2D box blur + void blur2D(uint8_t blur_amount, bool smear = false); void blurRow(uint32_t row, fract8 blur_amount, bool smear = false); void blurCol(uint32_t col, fract8 blur_amount, bool smear = false); void moveX(int8_t delta, bool wrap = false); @@ -666,14 +667,15 @@ typedef struct Segment { inline void setPixelColorXY(float x, float y, byte r, byte g, byte b, byte w = 0, bool aa = true) { setPixelColor(x, RGBW32(r,g,b,w), aa); } inline void setPixelColorXY(float x, float y, CRGB c, bool aa = true) { setPixelColor(x, RGBW32(c.r,c.g,c.b,0), aa); } #endif - inline uint32_t getPixelColorXY(uint16_t x, uint16_t y) { return getPixelColor(x); } + inline uint32_t getPixelColorXY(int x, int y) { return getPixelColor(x); } inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t c, uint8_t blend) { blendPixelColor(x, c, blend); } inline void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) { blendPixelColor(x, RGBW32(c.r,c.g,c.b,0), blend); } inline void addPixelColorXY(int x, int y, uint32_t color, bool fast = false) { addPixelColor(x, color, fast); } inline void addPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0, bool fast = false) { addPixelColor(x, RGBW32(r,g,b,w), fast); } inline void addPixelColorXY(int x, int y, CRGB c, bool fast = false) { addPixelColor(x, RGBW32(c.r,c.g,c.b,0), fast); } inline void fadePixelColorXY(uint16_t x, uint16_t y, uint8_t fade) { fadePixelColor(x, fade); } - inline void box_blur(uint16_t i, bool vertical, fract8 blur_amount) {} + inline void box_blur(unsigned i, bool vertical, fract8 blur_amount) {} + inline void blur2D(uint8_t blur_amount, bool smear = false) {} inline void blurRow(uint32_t row, fract8 blur_amount, bool smear = false) {} inline void blurCol(uint32_t col, fract8 blur_amount, bool smear = false) {} inline void moveX(int8_t delta, bool wrap = false) {} @@ -800,56 +802,55 @@ class WS2812FX { // 96 bytes bool paletteFade, checkSegmentAlignment(void), - hasRGBWBus(void), - hasCCTBus(void), - // return true if the strip is being sent pixel updates - isUpdating(void), + hasRGBWBus(void) const, + hasCCTBus(void) const, + isUpdating(void) const, // return true if the strip is being sent pixel updates deserializeMap(uint8_t n=0); - inline bool isServicing(void) { return _isServicing; } // returns true if strip.service() is executing - inline bool hasWhiteChannel(void) { return _hasWhiteChannel; } // returns true if strip contains separate white chanel - inline bool isOffRefreshRequired(void) { return _isOffRefreshRequired; } // returns true if strip requires regular updates (i.e. TM1814 chipset) - inline bool isSuspended(void) { return _suspend; } // returns true if strip.service() execution is suspended - inline bool needsUpdate(void) { return _triggered; } // returns true if strip received a trigger() request + inline bool isServicing(void) const { return _isServicing; } // returns true if strip.service() is executing + inline bool hasWhiteChannel(void) const { return _hasWhiteChannel; } // returns true if strip contains separate white chanel + inline bool isOffRefreshRequired(void) const { return _isOffRefreshRequired; } // returns true if strip requires regular updates (i.e. TM1814 chipset) + inline bool isSuspended(void) const { return _suspend; } // returns true if strip.service() execution is suspended + inline bool needsUpdate(void) const { return _triggered; } // returns true if strip received a trigger() request uint8_t paletteBlend, cctBlending, - getActiveSegmentsNum(void), - getFirstSelectedSegId(void), - getLastActiveSegmentId(void), - getActiveSegsLightCapabilities(bool selectedOnly = false); + getActiveSegmentsNum(void) const, + getFirstSelectedSegId(void) const, + getLastActiveSegmentId(void) const, + getActiveSegsLightCapabilities(bool selectedOnly = false) const; - inline uint8_t getBrightness(void) { return _brightness; } // returns current strip brightness - inline uint8_t getMaxSegments(void) { return MAX_NUM_SEGMENTS; } // returns maximum number of supported segments (fixed value) - inline uint8_t getSegmentsNum(void) { return _segments.size(); } // returns currently present segments - inline uint8_t getCurrSegmentId(void) { return _segment_index; } // returns current segment index (only valid while strip.isServicing()) - inline uint8_t getMainSegmentId(void) { return _mainSegment; } // returns main segment index - inline uint8_t getPaletteCount() { return 13 + GRADIENT_PALETTE_COUNT + customPalettes.size(); } - inline uint8_t getTargetFps() { return _targetFps; } // returns rough FPS value for las 2s interval - inline uint8_t getModeCount() { return _modeCount; } // returns number of registered modes/effects + inline uint8_t getBrightness(void) const { return _brightness; } // returns current strip brightness + inline uint8_t getMaxSegments(void) const { return MAX_NUM_SEGMENTS; } // returns maximum number of supported segments (fixed value) + inline uint8_t getSegmentsNum(void) const { return _segments.size(); } // returns currently present segments + inline uint8_t getCurrSegmentId(void) const { return _segment_index; } // returns current segment index (only valid while strip.isServicing()) + inline uint8_t getMainSegmentId(void) const { return _mainSegment; } // returns main segment index + inline uint8_t getPaletteCount() const { return 13 + GRADIENT_PALETTE_COUNT + customPalettes.size(); } + inline uint8_t getTargetFps() const { return _targetFps; } // returns rough FPS value for las 2s interval + inline uint8_t getModeCount() const { return _modeCount; } // returns number of registered modes/effects uint16_t - getLengthPhysical(void), - getLengthTotal(void), // will include virtual/nonexistent pixels in matrix - getFps(), - getMappedPixelIndex(uint16_t index); + getLengthPhysical(void) const, + getLengthTotal(void) const, // will include virtual/nonexistent pixels in matrix + getFps() const, + getMappedPixelIndex(uint16_t index) const; - inline uint16_t getFrameTime(void) { return _frametime; } // returns amount of time a frame should take (in ms) - inline uint16_t getMinShowDelay(void) { return MIN_SHOW_DELAY; } // returns minimum amount of time strip.service() can be delayed (constant) - inline uint16_t getLength(void) { return _length; } // returns actual amount of LEDs on a strip (2D matrix may have less LEDs than W*H) - inline uint16_t getTransition(void) { return _transitionDur; } // returns currently set transition time (in ms) + inline uint16_t getFrameTime(void) const { return _frametime; } // returns amount of time a frame should take (in ms) + inline uint16_t getMinShowDelay(void) const { return MIN_SHOW_DELAY; } // returns minimum amount of time strip.service() can be delayed (constant) + inline uint16_t getLength(void) const { return _length; } // returns actual amount of LEDs on a strip (2D matrix may have less LEDs than W*H) + inline uint16_t getTransition(void) const { return _transitionDur; } // returns currently set transition time (in ms) uint32_t now, timebase, - getPixelColor(uint16_t); + getPixelColor(uint16_t) const; - inline uint32_t getLastShow(void) { return _lastShow; } // returns millis() timestamp of last strip.show() call - inline uint32_t segColor(uint8_t i) { return _colors_t[i]; } // returns currently valid color (for slot i) AKA SEGCOLOR(); may be blended between two colors while in transition + inline uint32_t getLastShow(void) const { return _lastShow; } // returns millis() timestamp of last strip.show() call + inline uint32_t segColor(uint8_t i) const { return _colors_t[i]; } // returns currently valid color (for slot i) AKA SEGCOLOR(); may be blended between two colors while in transition const char * - getModeData(uint8_t id = 0) { return (id && id<_modeCount) ? _modeData[id] : PSTR("Solid"); } + getModeData(uint8_t id = 0) const { return (id && id<_modeCount) ? _modeData[id] : PSTR("Solid"); } const char ** getModeDataSrc(void) { return &(_modeData[0]); } // vectors use arrays for underlying data @@ -900,7 +901,7 @@ class WS2812FX { // 96 bytes inline void setPixelColorXY(int x, int y, byte r, byte g, byte b, byte w = 0) { setPixelColorXY(x, y, RGBW32(r,g,b,w)); } inline void setPixelColorXY(int x, int y, CRGB c) { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0)); } - inline uint32_t getPixelColorXY(uint16_t x, uint16_t y) { return getPixelColor(isMatrix ? y * Segment::maxWidth + x : x);} + inline uint32_t getPixelColorXY(int x, int y) const { return getPixelColor(isMatrix ? y * Segment::maxWidth + x : x); } // end 2D support diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index c135c3dbc..0fd5bb09f 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -184,13 +184,16 @@ void IRAM_ATTR Segment::setPixelColorXY(int x, int y, uint32_t col) x *= groupLength(); // expand to physical pixels y *= groupLength(); // expand to physical pixels - if (x >= width() || y >= height()) return; // if pixel would fall out of segment just exit + + unsigned W = width(); + unsigned H = height(); + if (x >= W || y >= H) return; // if pixel would fall out of segment just exit uint32_t tmpCol = col; - for (int j = 0; j < grouping; j++) { // groupping vertically - for (int g = 0; g < grouping; g++) { // groupping horizontally + for (unsigned j = 0; j < grouping; j++) { // groupping vertically + for (unsigned g = 0; g < grouping; g++) { // groupping horizontally unsigned xX = (x+g), yY = (y+j); - if (xX >= width() || yY >= height()) continue; // we have reached one dimension's end + if (xX >= W || yY >= H) continue; // we have reached one dimension's end #ifndef WLED_DISABLE_MODE_BLEND // if blending modes, blend with underlying pixel @@ -339,39 +342,126 @@ void Segment::blurCol(uint32_t col, fract8 blur_amount, bool smear) { setPixelColorXY(col, rows - 1, curnew); } -// 1D Box blur (with added weight - blur_amount: [0=no blur, 255=max blur]) -void Segment::box_blur(uint16_t i, bool vertical, fract8 blur_amount) { +void Segment::blur2D(uint8_t blur_amount, bool smear) { if (!isActive() || blur_amount == 0) return; // not active - const int cols = virtualWidth(); - const int rows = virtualHeight(); - const int dim1 = vertical ? rows : cols; - const int dim2 = vertical ? cols : rows; - if (i >= dim2) return; - const float seep = blur_amount/255.f; - const float keep = 3.f - 2.f*seep; - // 1D box blur - uint32_t out[dim1], in[dim1]; - for (int j = 0; j < dim1; j++) { - int x = vertical ? i : j; - int y = vertical ? j : i; - in[j] = getPixelColorXY(x, y); + const unsigned cols = virtualWidth(); + const unsigned rows = virtualHeight(); + + const uint8_t keep = smear ? 255 : 255 - blur_amount; + const uint8_t seep = blur_amount >> (1 + smear); + uint32_t lastnew; + uint32_t last; + for (unsigned row = 0; row < rows; row++) { + uint32_t carryover = BLACK; + uint32_t curnew = BLACK; + for (unsigned x = 0; x < cols; x++) { + uint32_t cur = getPixelColorXY(x, row); + uint32_t part = color_fade(cur, seep); + curnew = color_fade(cur, keep); + if (x > 0) { + if (carryover) curnew = color_add(curnew, carryover, true); + uint32_t prev = color_add(lastnew, part, true); + // optimization: only set pixel if color has changed + if (last != prev) setPixelColorXY(x - 1, row, prev); + } else setPixelColorXY(x, row, curnew); // first pixel + lastnew = curnew; + last = cur; // save original value for comparison on next iteration + carryover = part; + } + setPixelColorXY(cols-1, row, curnew); // set last pixel } - for (int j = 0; j < dim1; j++) { - uint32_t curr = in[j]; - uint32_t prev = j > 0 ? in[j-1] : BLACK; - uint32_t next = j < dim1-1 ? in[j+1] : BLACK; - uint8_t r, g, b, w; - r = (R(curr)*keep + (R(prev) + R(next))*seep) / 3; - g = (G(curr)*keep + (G(prev) + G(next))*seep) / 3; - b = (B(curr)*keep + (B(prev) + B(next))*seep) / 3; - w = (W(curr)*keep + (W(prev) + W(next))*seep) / 3; - out[j] = RGBW32(r,g,b,w); + for (unsigned col = 0; col < cols; col++) { + uint32_t carryover = BLACK; + uint32_t curnew = BLACK; + for (unsigned y = 0; y < rows; y++) { + uint32_t cur = getPixelColorXY(col, y); + uint32_t part = color_fade(cur, seep); + curnew = color_fade(cur, keep); + if (y > 0) { + if (carryover) curnew = color_add(curnew, carryover, true); + uint32_t prev = color_add(lastnew, part, true); + // optimization: only set pixel if color has changed + if (last != prev) setPixelColorXY(col, y - 1, prev); + } else setPixelColorXY(col, y, curnew); // first pixel + lastnew = curnew; + last = cur; //save original value for comparison on next iteration + carryover = part; + } + setPixelColorXY(col, rows - 1, curnew); } - for (int j = 0; j < dim1; j++) { - int x = vertical ? i : j; - int y = vertical ? j : i; - setPixelColorXY(x, y, out[j]); +} + +// 2D Box blur +void Segment::box_blur(unsigned radius, bool smear) { + if (!isActive() || radius == 0) return; // not active + if (radius > 3) radius = 3; + const unsigned d = (1 + 2*radius) * (1 + 2*radius); // averaging divisor + const unsigned cols = virtualWidth(); + const unsigned rows = virtualHeight(); + uint16_t *tmpRSum = new uint16_t[cols*rows]; + uint16_t *tmpGSum = new uint16_t[cols*rows]; + uint16_t *tmpBSum = new uint16_t[cols*rows]; + uint16_t *tmpWSum = new uint16_t[cols*rows]; + // fill summed-area table (https://en.wikipedia.org/wiki/Summed-area_table) + for (unsigned x = 0; x < cols; x++) { + unsigned rS, gS, bS, wS; + unsigned index; + rS = gS = bS = wS = 0; + for (unsigned y = 0; y < rows; y++) { + index = x * cols + y; + if (x > 0) { + unsigned index2 = (x - 1) * cols + y; + tmpRSum[index] = tmpRSum[index2]; + tmpGSum[index] = tmpGSum[index2]; + tmpBSum[index] = tmpBSum[index2]; + tmpWSum[index] = tmpWSum[index2]; + } else { + tmpRSum[index] = 0; + tmpGSum[index] = 0; + tmpBSum[index] = 0; + tmpWSum[index] = 0; + } + uint32_t c = getPixelColorXY(x, y); + rS += R(c); + gS += G(c); + bS += B(c); + wS += W(c); + tmpRSum[index] += rS; + tmpGSum[index] += gS; + tmpBSum[index] += bS; + tmpWSum[index] += wS; + } } + // do a box blur using pre-calculated sums + for (unsigned x = 0; x < cols; x++) { + for (unsigned y = 0; y < rows; y++) { + // sum = D + A - B - C where k = (x,y) + // +----+-+---- (x) + // | | | + // +----A-B + // | |k| + // +----C-D + // | + //(y) + unsigned x0 = x < radius ? 0 : x - radius; + unsigned y0 = y < radius ? 0 : y - radius; + unsigned x1 = x >= cols - radius ? cols - 1 : x + radius; + unsigned y1 = y >= rows - radius ? rows - 1 : y + radius; + unsigned A = x0 * cols + y0; + unsigned B = x1 * cols + y0; + unsigned C = x0 * cols + y1; + unsigned D = x1 * cols + y1; + unsigned r = tmpRSum[D] + tmpRSum[A] - tmpRSum[C] - tmpRSum[B]; + unsigned g = tmpGSum[D] + tmpGSum[A] - tmpGSum[C] - tmpGSum[B]; + unsigned b = tmpBSum[D] + tmpBSum[A] - tmpBSum[C] - tmpBSum[B]; + unsigned w = tmpWSum[D] + tmpWSum[A] - tmpWSum[C] - tmpWSum[B]; + setPixelColorXY(x, y, RGBW32(r/d, g/d, b/d, w/d)); + } + } + delete[] tmpRSum; + delete[] tmpGSum; + delete[] tmpBSum; + delete[] tmpWSum; } void Segment::moveX(int8_t delta, bool wrap) { diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 758df62fa..80eedc085 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -954,9 +954,9 @@ uint32_t IRAM_ATTR Segment::getPixelColor(int i) const if (reverse) i = virtualLength() - i - 1; i *= groupLength(); i += start; - /* offset/phase */ + // offset/phase i += offset; - if ((i >= stop) && (stop>0)) i -= length(); // avoids negative pixel index (stop = 0 is a possible value) + if (i >= stop) i -= length(); return strip.getPixelColor(i); } @@ -1110,15 +1110,13 @@ void Segment::blur(uint8_t blur_amount, bool smear) { #ifndef WLED_DISABLE_2D if (is2D()) { // compatibility with 2D - const unsigned cols = virtualWidth(); - const unsigned rows = virtualHeight(); - for (unsigned i = 0; i < rows; i++) blurRow(i, blur_amount, smear); // blur all rows - for (unsigned k = 0; k < cols; k++) blurCol(k, blur_amount, smear); // blur all columns + blur2D(blur_amount, smear); + //box_blur(map(blur_amount,1,255,1,3), smear); return; } #endif uint8_t keep = smear ? 255 : 255 - blur_amount; - uint8_t seep = blur_amount >> 1; + uint8_t seep = blur_amount >> (1 + smear); unsigned vlength = virtualLength(); uint32_t carryover = BLACK; uint32_t lastnew; @@ -1129,13 +1127,11 @@ void Segment::blur(uint8_t blur_amount, bool smear) { uint32_t part = color_fade(cur, seep); curnew = color_fade(cur, keep); if (i > 0) { - if (carryover) - curnew = color_add(curnew, carryover, true); + if (carryover) curnew = color_add(curnew, carryover, true); uint32_t prev = color_add(lastnew, part, true); - if (last != prev) // optimization: only set pixel if color has changed - setPixelColor(i - 1, prev); - } - else // first pixel + // optimization: only set pixel if color has changed + if (last != prev) setPixelColor(i - 1, prev); + } else // first pixel setPixelColor(i, curnew); lastnew = curnew; last = cur; // save original value for comparison on next iteration @@ -1357,7 +1353,7 @@ void IRAM_ATTR WS2812FX::setPixelColor(unsigned i, uint32_t col) { BusManager::setPixelColor(i, col); } -uint32_t IRAM_ATTR WS2812FX::getPixelColor(uint16_t i) { +uint32_t IRAM_ATTR WS2812FX::getPixelColor(uint16_t i) const { i = getMappedPixelIndex(i); if (i >= _length) return 0; return BusManager::getPixelColor(i); @@ -1385,7 +1381,7 @@ void WS2812FX::show(void) { * Returns a true value if any of the strips are still being updated. * On some hardware (ESP32), strip updates are done asynchronously. */ -bool WS2812FX::isUpdating() { +bool WS2812FX::isUpdating() const { return !BusManager::canAllShow(); } @@ -1393,7 +1389,7 @@ bool WS2812FX::isUpdating() { * Returns the refresh rate of the LED strip. Useful for finding out whether a given setup is fast enough. * Only updates on show() or is set to 0 fps if last show is more than 2 secs ago, so accuracy varies */ -uint16_t WS2812FX::getFps() { +uint16_t WS2812FX::getFps() const { if (millis() - _lastShow > 2000) return 0; return _cumulativeFps +1; } @@ -1452,17 +1448,17 @@ void WS2812FX::setBrightness(uint8_t b, bool direct) { } } -uint8_t WS2812FX::getActiveSegsLightCapabilities(bool selectedOnly) { +uint8_t WS2812FX::getActiveSegsLightCapabilities(bool selectedOnly) const { uint8_t totalLC = 0; - for (segment &seg : _segments) { + for (const segment &seg : _segments) { if (seg.isActive() && (!selectedOnly || seg.isSelected())) totalLC |= seg.getLightCapabilities(); } return totalLC; } -uint8_t WS2812FX::getFirstSelectedSegId(void) { +uint8_t WS2812FX::getFirstSelectedSegId(void) const { size_t i = 0; - for (segment &seg : _segments) { + for (const segment &seg : _segments) { if (seg.isActive() && seg.isSelected()) return i; i++; } @@ -1478,14 +1474,14 @@ void WS2812FX::setMainSegmentId(uint8_t n) { return; } -uint8_t WS2812FX::getLastActiveSegmentId(void) { +uint8_t WS2812FX::getLastActiveSegmentId(void) const { for (size_t i = _segments.size() -1; i > 0; i--) { if (_segments[i].isActive()) return i; } return 0; } -uint8_t WS2812FX::getActiveSegmentsNum(void) { +uint8_t WS2812FX::getActiveSegmentsNum(void) const { uint8_t c = 0; for (size_t i = 0; i < _segments.size(); i++) { if (_segments[i].isActive()) c++; @@ -1493,13 +1489,13 @@ uint8_t WS2812FX::getActiveSegmentsNum(void) { return c; } -uint16_t WS2812FX::getLengthTotal(void) { +uint16_t WS2812FX::getLengthTotal(void) const { unsigned len = Segment::maxWidth * Segment::maxHeight; // will be _length for 1D (see finalizeInit()) but should cover whole matrix for 2D if (isMatrix && _length > len) len = _length; // for 2D with trailing strip return len; } -uint16_t WS2812FX::getLengthPhysical(void) { +uint16_t WS2812FX::getLengthPhysical(void) const { unsigned len = 0; for (size_t b = 0; b < BusManager::getNumBusses(); b++) { Bus *bus = BusManager::getBus(b); @@ -1512,7 +1508,7 @@ uint16_t WS2812FX::getLengthPhysical(void) { //used for JSON API info.leds.rgbw. Little practical use, deprecate with info.leds.rgbw. //returns if there is an RGBW bus (supports RGB and White, not only white) //not influenced by auto-white mode, also true if white slider does not affect output white channel -bool WS2812FX::hasRGBWBus(void) { +bool WS2812FX::hasRGBWBus(void) const { for (size_t b = 0; b < BusManager::getNumBusses(); b++) { Bus *bus = BusManager::getBus(b); if (bus == nullptr || bus->getLength()==0) break; @@ -1521,7 +1517,7 @@ bool WS2812FX::hasRGBWBus(void) { return false; } -bool WS2812FX::hasCCTBus(void) { +bool WS2812FX::hasCCTBus(void) const { if (cctFromRgb && !correctWB) return false; for (size_t b = 0; b < BusManager::getNumBusses(); b++) { Bus *bus = BusManager::getBus(b); @@ -1813,7 +1809,7 @@ bool WS2812FX::deserializeMap(uint8_t n) { return (customMappingSize > 0); } -uint16_t IRAM_ATTR WS2812FX::getMappedPixelIndex(uint16_t index) { +uint16_t IRAM_ATTR WS2812FX::getMappedPixelIndex(uint16_t index) const { // convert logical address to physical if (index < customMappingSize && (realtimeMode == REALTIME_MODE_INACTIVE || realtimeRespectLedMaps)) index = customMappingTable[index]; From e68375a71eca592df1420b191eeb39222c4aa565 Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Thu, 15 Aug 2024 09:08:57 +0100 Subject: [PATCH 056/142] Remove repeating code to fetch audio data --- wled00/FX.cpp | 162 +++++++----------------------------------- wled00/fcn_declare.h | 1 + wled00/um_manager.cpp | 8 +++ 3 files changed, 36 insertions(+), 135 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index cce3098e1..e51f66bf8 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -6325,11 +6325,7 @@ uint16_t mode_ripplepeak(void) { // * Ripple peak. By Andrew Tuli if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed Ripple* ripples = reinterpret_cast(SEGENV.data); - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = usermods.getAudioData(); uint8_t samplePeak = *(uint8_t*)um_data->u_data[3]; #ifdef ESP32 float FFT_MajorPeak = *(float*) um_data->u_data[4]; @@ -6416,11 +6412,7 @@ uint16_t mode_2DSwirl(void) { int ni = (cols - 1) - i; int nj = (cols - 1) - j; - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = usermods.getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; //ewowi: use instead of sampleAvg??? int volumeRaw = *(int16_t*) um_data->u_data[1]; @@ -6446,11 +6438,7 @@ uint16_t mode_2DWaverly(void) { const int cols = SEGMENT.virtualWidth(); const int rows = SEGMENT.virtualHeight(); - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = usermods.getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; SEGMENT.fadeToBlackBy(SEGMENT.speed); @@ -6499,11 +6487,7 @@ uint16_t mode_gravcenter(void) { // Gravcenter. By Andrew Tuline. if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed Gravity* gravcen = reinterpret_cast(SEGENV.data); - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = usermods.getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; //SEGMENT.fade_out(240); @@ -6548,11 +6532,7 @@ uint16_t mode_gravcentric(void) { // Gravcentric. By Andrew if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed Gravity* gravcen = reinterpret_cast(SEGENV.data); - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = usermods.getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; // printUmData(); @@ -6600,11 +6580,7 @@ uint16_t mode_gravimeter(void) { // Gravmeter. By Andrew Tuline. if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed Gravity* gravcen = reinterpret_cast(SEGENV.data); - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = usermods.getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; //SEGMENT.fade_out(240); @@ -6641,11 +6617,7 @@ static const char _data_FX_MODE_GRAVIMETER[] PROGMEM = "Gravimeter@Rate of fall, // * JUGGLES // ////////////////////// uint16_t mode_juggles(void) { // Juggles. By Andrew Tuline. - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = usermods.getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; SEGMENT.fade_out(224); // 6.25% @@ -6668,11 +6640,7 @@ uint16_t mode_matripix(void) { // Matripix. By Andrew Tuline. if (SEGLEN == 1) return mode_static(); // even with 1D effect we have to take logic for 2D segments for allocation as fill_solid() fills whole segment - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = usermods.getAudioData(); int volumeRaw = *(int16_t*)um_data->u_data[1]; if (SEGENV.call == 0) { @@ -6700,11 +6668,7 @@ uint16_t mode_midnoise(void) { // Midnoise. By Andrew Tuline. if (SEGLEN == 1) return mode_static(); // Changing xdist to SEGENV.aux0 and ydist to SEGENV.aux1. - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = usermods.getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; SEGMENT.fade_out(SEGMENT.speed); @@ -6739,11 +6703,7 @@ uint16_t mode_noisefire(void) { // Noisefire. By Andrew Tuline. CRGB::DarkOrange, CRGB::DarkOrange, CRGB::Orange, CRGB::Orange, CRGB::Yellow, CRGB::Orange, CRGB::Yellow, CRGB::Yellow); - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = usermods.getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; if (SEGENV.call == 0) SEGMENT.fill(BLACK); @@ -6767,11 +6727,7 @@ static const char _data_FX_MODE_NOISEFIRE[] PROGMEM = "Noisefire@!,!;;;01v;m12=2 /////////////////////// uint16_t mode_noisemeter(void) { // Noisemeter. By Andrew Tuline. - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = usermods.getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; int volumeRaw = *(int16_t*)um_data->u_data[1]; @@ -6808,11 +6764,7 @@ uint16_t mode_pixelwave(void) { // Pixelwave. By Andrew Tuline. SEGMENT.fill(BLACK); } - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = usermods.getAudioData(); int volumeRaw = *(int16_t*)um_data->u_data[1]; uint8_t secondHand = micros()/(256-SEGMENT.speed)/500+1 % 16; @@ -6844,11 +6796,7 @@ uint16_t mode_plasmoid(void) { // Plasmoid. By Andrew Tuline. if (!SEGENV.allocateData(sizeof(plasphase))) return mode_static(); //allocation failed Plasphase* plasmoip = reinterpret_cast(SEGENV.data); - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = usermods.getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; SEGMENT.fadeToBlackBy(32); @@ -6883,11 +6831,7 @@ uint16_t mode_puddlepeak(void) { // Puddlepeak. By Andrew Tuline. uint8_t fadeVal = map(SEGMENT.speed,0,255, 224, 254); unsigned pos = random16(SEGLEN); // Set a random starting position. - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = usermods.getAudioData(); uint8_t samplePeak = *(uint8_t*)um_data->u_data[3]; uint8_t *maxVol = (uint8_t*)um_data->u_data[6]; uint8_t *binNum = (uint8_t*)um_data->u_data[7]; @@ -6928,11 +6872,7 @@ uint16_t mode_puddles(void) { // Puddles. By Andrew Tuline. SEGMENT.fade_out(fadeVal); - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = usermods.getAudioData(); int volumeRaw = *(int16_t*)um_data->u_data[1]; if (volumeRaw > 1) { @@ -6990,11 +6930,7 @@ uint16_t mode_blurz(void) { // Blurz. By Andrew Tuline. if (SEGLEN == 1) return mode_static(); // even with 1D effect we have to take logic for 2D segments for allocation as fill_solid() fills whole segment - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = usermods.getAudioData(); uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; if (SEGENV.call == 0) { @@ -7027,11 +6963,7 @@ uint16_t mode_DJLight(void) { // Written by ??? Adapted by Wil // No need to prevent from executing on single led strips, only mid will be set (mid = 0) const int mid = SEGLEN / 2; - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = usermods.getAudioData(); uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; if (SEGENV.call == 0) { @@ -7063,11 +6995,7 @@ uint16_t mode_freqmap(void) { // Map FFT_MajorPeak to SEGLEN. // Start frequency = 60 Hz and log10(60) = 1.78 // End frequency = MAX_FREQUENCY in Hz and lo10(MAX_FREQUENCY) = MAX_FREQ_LOG10 - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = usermods.getAudioData(); float FFT_MajorPeak = *(float*)um_data->u_data[4]; float my_magnitude = *(float*)um_data->u_data[5] / 4.0f; if (FFT_MajorPeak < 1) FFT_MajorPeak = 1; // log10(0) is "forbidden" (throws exception) @@ -7097,11 +7025,7 @@ static const char _data_FX_MODE_FREQMAP[] PROGMEM = "Freqmap@Fade rate,Starting /////////////////////// uint16_t mode_freqmatrix(void) { // Freqmatrix. By Andreas Pleschung. // No need to prevent from executing on single led strips, we simply change pixel 0 each time and avoid the shift - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = usermods.getAudioData(); float FFT_MajorPeak = *(float*)um_data->u_data[4]; float volumeSmth = *(float*)um_data->u_data[0]; @@ -7156,11 +7080,7 @@ static const char _data_FX_MODE_FREQMATRIX[] PROGMEM = "Freqmatrix@Speed,Sound e // SEGMENT.speed select faderate // SEGMENT.intensity select colour index uint16_t mode_freqpixels(void) { // Freqpixel. By Andrew Tuline. - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = usermods.getAudioData(); float FFT_MajorPeak = *(float*)um_data->u_data[4]; float my_magnitude = *(float*)um_data->u_data[5] / 16.0f; if (FFT_MajorPeak < 1) FFT_MajorPeak = 1.0f; // log10(0) is "forbidden" (throws exception) @@ -7203,11 +7123,7 @@ static const char _data_FX_MODE_FREQPIXELS[] PROGMEM = "Freqpixels@Fade rate,Sta // Depending on the music stream you have you might find it useful to change the frequency mapping. uint16_t mode_freqwave(void) { // Freqwave. By Andreas Pleschung. // As before, this effect can also work on single pixels, we just lose the shifting effect - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = usermods.getAudioData(); float FFT_MajorPeak = *(float*)um_data->u_data[4]; float volumeSmth = *(float*)um_data->u_data[0]; @@ -7262,11 +7178,7 @@ uint16_t mode_gravfreq(void) { // Gravfreq. By Andrew Tuline. if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed Gravity* gravcen = reinterpret_cast(SEGENV.data); - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = usermods.getAudioData(); float FFT_MajorPeak = *(float*)um_data->u_data[4]; float volumeSmth = *(float*)um_data->u_data[0]; if (FFT_MajorPeak < 1) FFT_MajorPeak = 1; // log10(0) is "forbidden" (throws exception) @@ -7309,11 +7221,7 @@ static const char _data_FX_MODE_GRAVFREQ[] PROGMEM = "Gravfreq@Rate of fall,Sens // ** Noisemove // ////////////////////// uint16_t mode_noisemove(void) { // Noisemove. By: Andrew Tuline - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = usermods.getAudioData(); uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; int fadeoutDelay = (256 - SEGMENT.speed) / 96; @@ -7336,11 +7244,7 @@ static const char _data_FX_MODE_NOISEMOVE[] PROGMEM = "Noisemove@Speed of perlin // ** Rocktaves // ////////////////////// uint16_t mode_rocktaves(void) { // Rocktaves. Same note from each octave is same colour. By: Andrew Tuline - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = usermods.getAudioData(); float FFT_MajorPeak = *(float*) um_data->u_data[4]; float my_magnitude = *(float*) um_data->u_data[5] / 16.0f; @@ -7378,11 +7282,7 @@ static const char _data_FX_MODE_ROCKTAVES[] PROGMEM = "Rocktaves@;!,!;!;01f;m12= uint16_t mode_waterfall(void) { // Waterfall. By: Andrew Tuline // effect can work on single pixels, we just lose the shifting effect - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = usermods.getAudioData(); uint8_t samplePeak = *(uint8_t*)um_data->u_data[3]; float FFT_MajorPeak = *(float*) um_data->u_data[4]; uint8_t *maxVol = (uint8_t*)um_data->u_data[6]; @@ -7437,11 +7337,7 @@ uint16_t mode_2DGEQ(void) { // By Will Tatam. Code reduction by Ewoud Wijma. if (!SEGENV.allocateData(cols*sizeof(uint16_t))) return mode_static(); //allocation failed uint16_t *previousBarHeight = reinterpret_cast(SEGENV.data); //array of previous bar heights per frequency band - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } + um_data_t *um_data = usermods.getAudioData(); uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; if (SEGENV.call == 0) for (int i=0; iu_data[2]; if (SEGENV.call == 0) { diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index d95b8ef8e..3f0e63338 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -326,6 +326,7 @@ class UsermodManager { void handleOverlayDraw(); bool handleButton(uint8_t b); bool getUMData(um_data_t **um_data, uint8_t mod_id = USERMOD_ID_RESERVED); // USERMOD_ID_RESERVED will poll all usermods + um_data_t *getAudioData(); void setup(); void connected(); void appendConfigData(); diff --git a/wled00/um_manager.cpp b/wled00/um_manager.cpp index 2db29c3cd..3add18f39 100644 --- a/wled00/um_manager.cpp +++ b/wled00/um_manager.cpp @@ -23,6 +23,14 @@ bool UsermodManager::getUMData(um_data_t **data, uint8_t mod_id) { } return false; } +um_data_t* UsermodManager::getAudioData() { + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + return um_data; +} void UsermodManager::addToJsonState(JsonObject& obj) { for (unsigned i = 0; i < numMods; i++) ums[i]->addToJsonState(obj); } void UsermodManager::addToJsonInfo(JsonObject& obj) { for (unsigned i = 0; i < numMods; i++) ums[i]->addToJsonInfo(obj); } void UsermodManager::readFromJsonState(JsonObject& obj) { for (unsigned i = 0; i < numMods; i++) ums[i]->readFromJsonState(obj); } From 577fce69e2bf1aea598b398e7bec0c2090a67cf4 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Thu, 15 Aug 2024 16:18:34 +0200 Subject: [PATCH 057/142] MQTT unification and cleanup --- wled00/cfg.cpp | 8 ++++---- wled00/mqtt.cpp | 9 +++++---- wled00/set.cpp | 2 +- wled00/wled.h | 2 +- wled00/xml.cpp | 2 +- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index f9a94e228..a6e919dee 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -537,7 +537,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(alexaNumPresets, interfaces["va"]["p"]); -#ifdef WLED_ENABLE_MQTT +#ifndef WLED_DISABLE_MQTT JsonObject if_mqtt = interfaces["mqtt"]; CJSON(mqttEnabled, if_mqtt["en"]); getStringFromJson(mqttServer, if_mqtt[F("broker")], MQTT_MAX_SERVER_LEN+1); @@ -1019,7 +1019,7 @@ void serializeConfig() { if_va["p"] = alexaNumPresets; #endif -#ifdef WLED_ENABLE_MQTT +#ifndef WLED_DISABLE_MQTT JsonObject if_mqtt = interfaces.createNestedObject("mqtt"); if_mqtt["en"] = mqttEnabled; if_mqtt[F("broker")] = mqttServer; @@ -1165,7 +1165,7 @@ bool deserializeConfigSec() { [[maybe_unused]] JsonObject interfaces = root["if"]; -#ifdef WLED_ENABLE_MQTT +#ifndef WLED_DISABLE_MQTT JsonObject if_mqtt = interfaces["mqtt"]; getStringFromJson(mqttPass, if_mqtt["psk"], 65); #endif @@ -1206,7 +1206,7 @@ void serializeConfigSec() { ap["psk"] = apPass; [[maybe_unused]] JsonObject interfaces = root.createNestedObject("if"); -#ifdef WLED_ENABLE_MQTT +#ifndef WLED_DISABLE_MQTT JsonObject if_mqtt = interfaces.createNestedObject("mqtt"); if_mqtt["psk"] = mqttPass; #endif diff --git a/wled00/mqtt.cpp b/wled00/mqtt.cpp index 5599824ef..775a4fe58 100644 --- a/wled00/mqtt.cpp +++ b/wled00/mqtt.cpp @@ -4,10 +4,10 @@ * MQTT communication protocol for home automation */ -#ifdef WLED_ENABLE_MQTT +#ifndef WLED_DISABLE_MQTT #define MQTT_KEEP_ALIVE_TIME 60 // contact the MQTT broker every 60 seconds -void parseMQTTBriPayload(char* payload) +static void parseMQTTBriPayload(char* payload) { if (strstr(payload, "ON") || strstr(payload, "on") || strstr(payload, "true")) {bri = briLast; stateUpdated(CALL_MODE_DIRECT_CHANGE);} else if (strstr(payload, "T" ) || strstr(payload, "t" )) {toggleOnOff(); stateUpdated(CALL_MODE_DIRECT_CHANGE);} @@ -20,7 +20,7 @@ void parseMQTTBriPayload(char* payload) } -void onMqttConnect(bool sessionPresent) +static void onMqttConnect(bool sessionPresent) { //(re)subscribe to required topics char subuf[38]; @@ -52,7 +52,7 @@ void onMqttConnect(bool sessionPresent) } -void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { +static void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProperties properties, size_t len, size_t index, size_t total) { static char *payloadStr; DEBUG_PRINT(F("MQTT msg: ")); @@ -166,6 +166,7 @@ bool initMqtt() if (mqtt == nullptr) { mqtt = new AsyncMqttClient(); + if (!mqtt) return false; mqtt->onMessage(onMqttMessage); mqtt->onConnect(onMqttConnect); } diff --git a/wled00/set.cpp b/wled00/set.cpp index 3dd226e00..5eecb5c9a 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -426,7 +426,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) t = request->arg(F("AP")).toInt(); if (t >= 0 && t <= 9) alexaNumPresets = t; - #ifdef WLED_ENABLE_MQTT + #ifndef WLED_DISABLE_MQTT mqttEnabled = request->hasArg(F("MQ")); strlcpy(mqttServer, request->arg(F("MS")).c_str(), MQTT_MAX_SERVER_LEN+1); t = request->arg(F("MQPORT")).toInt(); diff --git a/wled00/wled.h b/wled00/wled.h index d6915e7fb..b381a8a16 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -145,7 +145,7 @@ #endif #include "src/dependencies/e131/ESPAsyncE131.h" -#ifdef WLED_ENABLE_MQTT +#ifndef WLED_DISABLE_MQTT #include "src/dependencies/async-mqtt-client/AsyncMqttClient.h" #endif diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 7da301715..2ae599f5d 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -541,7 +541,7 @@ void getSettingsJS(byte subPage, char* dest) oappend(SET_F("toggle('Alexa');")); // hide Alexa settings #endif - #ifdef WLED_ENABLE_MQTT + #ifndef WLED_DISABLE_MQTT sappend('c',SET_F("MQ"),mqttEnabled); sappends('s',SET_F("MS"),mqttServer); sappend('v',SET_F("MQPORT"),mqttPort); From 9940d2590bae64c7461f45654d72be9400c644f2 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Thu, 15 Aug 2024 17:22:59 +0200 Subject: [PATCH 058/142] Arc expansion getPixelColor fix. --- wled00/FX_fcn.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 80eedc085..0608f6784 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -674,7 +674,7 @@ uint16_t IRAM_ATTR Segment::virtualLength() const { if (is2D()) { unsigned vW = virtualWidth(); unsigned vH = virtualHeight(); - unsigned vLen = vW * vH; // use all pixels from segment + unsigned vLen; switch (map1D2D) { case M12_pBar: vLen = vH; @@ -688,6 +688,9 @@ uint16_t IRAM_ATTR Segment::virtualLength() const { case M12_sPinwheel: vLen = getPinwheelLength(vW, vH); break; + default: + vLen = vW * vH; // use all pixels from segment + break; } return vLen; } @@ -913,6 +916,10 @@ uint32_t IRAM_ATTR Segment::getPixelColor(int i) const else return getPixelColorXY(0, vH - i -1); break; case M12_pArc: + if (i >= vW && i >= vH) { + unsigned vI = sqrt16(i*i/2); + return getPixelColorXY(vI,vI); // use diagonal + } case M12_pCorner: // use longest dimension return vW>vH ? getPixelColorXY(i, 0) : getPixelColorXY(0, i); From 24ecf1a16626870d53a8d6a229d47b7909ee7a2a Mon Sep 17 00:00:00 2001 From: Will Tatam Date: Thu, 15 Aug 2024 17:58:19 +0100 Subject: [PATCH 059/142] Move getAudioData to static --- wled00/FX.cpp | 63 ++++++++++++++++++++++++------------------- wled00/fcn_declare.h | 1 - wled00/um_manager.cpp | 8 ------ 3 files changed, 36 insertions(+), 36 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index e51f66bf8..bca9e58f2 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -73,6 +73,15 @@ int8_t tristate_square8(uint8_t x, uint8_t pulsewidth, uint8_t attdec) { return 0; } +static um_data_t* getAudioData() { + um_data_t *um_data; + if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + // add support for no audio + um_data = simulateSound(SEGMENT.soundSim); + } + return um_data; +} + // effect functions /* @@ -6325,7 +6334,7 @@ uint16_t mode_ripplepeak(void) { // * Ripple peak. By Andrew Tuli if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed Ripple* ripples = reinterpret_cast(SEGENV.data); - um_data_t *um_data = usermods.getAudioData(); + um_data_t *um_data = getAudioData(); uint8_t samplePeak = *(uint8_t*)um_data->u_data[3]; #ifdef ESP32 float FFT_MajorPeak = *(float*) um_data->u_data[4]; @@ -6412,7 +6421,7 @@ uint16_t mode_2DSwirl(void) { int ni = (cols - 1) - i; int nj = (cols - 1) - j; - um_data_t *um_data = usermods.getAudioData(); + um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; //ewowi: use instead of sampleAvg??? int volumeRaw = *(int16_t*) um_data->u_data[1]; @@ -6438,7 +6447,7 @@ uint16_t mode_2DWaverly(void) { const int cols = SEGMENT.virtualWidth(); const int rows = SEGMENT.virtualHeight(); - um_data_t *um_data = usermods.getAudioData(); + um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; SEGMENT.fadeToBlackBy(SEGMENT.speed); @@ -6487,7 +6496,7 @@ uint16_t mode_gravcenter(void) { // Gravcenter. By Andrew Tuline. if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed Gravity* gravcen = reinterpret_cast(SEGENV.data); - um_data_t *um_data = usermods.getAudioData(); + um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; //SEGMENT.fade_out(240); @@ -6532,7 +6541,7 @@ uint16_t mode_gravcentric(void) { // Gravcentric. By Andrew if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed Gravity* gravcen = reinterpret_cast(SEGENV.data); - um_data_t *um_data = usermods.getAudioData(); + um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; // printUmData(); @@ -6580,7 +6589,7 @@ uint16_t mode_gravimeter(void) { // Gravmeter. By Andrew Tuline. if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed Gravity* gravcen = reinterpret_cast(SEGENV.data); - um_data_t *um_data = usermods.getAudioData(); + um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; //SEGMENT.fade_out(240); @@ -6617,7 +6626,7 @@ static const char _data_FX_MODE_GRAVIMETER[] PROGMEM = "Gravimeter@Rate of fall, // * JUGGLES // ////////////////////// uint16_t mode_juggles(void) { // Juggles. By Andrew Tuline. - um_data_t *um_data = usermods.getAudioData(); + um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; SEGMENT.fade_out(224); // 6.25% @@ -6640,7 +6649,7 @@ uint16_t mode_matripix(void) { // Matripix. By Andrew Tuline. if (SEGLEN == 1) return mode_static(); // even with 1D effect we have to take logic for 2D segments for allocation as fill_solid() fills whole segment - um_data_t *um_data = usermods.getAudioData(); + um_data_t *um_data = getAudioData(); int volumeRaw = *(int16_t*)um_data->u_data[1]; if (SEGENV.call == 0) { @@ -6668,7 +6677,7 @@ uint16_t mode_midnoise(void) { // Midnoise. By Andrew Tuline. if (SEGLEN == 1) return mode_static(); // Changing xdist to SEGENV.aux0 and ydist to SEGENV.aux1. - um_data_t *um_data = usermods.getAudioData(); + um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; SEGMENT.fade_out(SEGMENT.speed); @@ -6703,7 +6712,7 @@ uint16_t mode_noisefire(void) { // Noisefire. By Andrew Tuline. CRGB::DarkOrange, CRGB::DarkOrange, CRGB::Orange, CRGB::Orange, CRGB::Yellow, CRGB::Orange, CRGB::Yellow, CRGB::Yellow); - um_data_t *um_data = usermods.getAudioData(); + um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; if (SEGENV.call == 0) SEGMENT.fill(BLACK); @@ -6727,7 +6736,7 @@ static const char _data_FX_MODE_NOISEFIRE[] PROGMEM = "Noisefire@!,!;;;01v;m12=2 /////////////////////// uint16_t mode_noisemeter(void) { // Noisemeter. By Andrew Tuline. - um_data_t *um_data = usermods.getAudioData(); + um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; int volumeRaw = *(int16_t*)um_data->u_data[1]; @@ -6764,7 +6773,7 @@ uint16_t mode_pixelwave(void) { // Pixelwave. By Andrew Tuline. SEGMENT.fill(BLACK); } - um_data_t *um_data = usermods.getAudioData(); + um_data_t *um_data = getAudioData(); int volumeRaw = *(int16_t*)um_data->u_data[1]; uint8_t secondHand = micros()/(256-SEGMENT.speed)/500+1 % 16; @@ -6796,7 +6805,7 @@ uint16_t mode_plasmoid(void) { // Plasmoid. By Andrew Tuline. if (!SEGENV.allocateData(sizeof(plasphase))) return mode_static(); //allocation failed Plasphase* plasmoip = reinterpret_cast(SEGENV.data); - um_data_t *um_data = usermods.getAudioData(); + um_data_t *um_data = getAudioData(); float volumeSmth = *(float*) um_data->u_data[0]; SEGMENT.fadeToBlackBy(32); @@ -6831,7 +6840,7 @@ uint16_t mode_puddlepeak(void) { // Puddlepeak. By Andrew Tuline. uint8_t fadeVal = map(SEGMENT.speed,0,255, 224, 254); unsigned pos = random16(SEGLEN); // Set a random starting position. - um_data_t *um_data = usermods.getAudioData(); + um_data_t *um_data = getAudioData(); uint8_t samplePeak = *(uint8_t*)um_data->u_data[3]; uint8_t *maxVol = (uint8_t*)um_data->u_data[6]; uint8_t *binNum = (uint8_t*)um_data->u_data[7]; @@ -6872,7 +6881,7 @@ uint16_t mode_puddles(void) { // Puddles. By Andrew Tuline. SEGMENT.fade_out(fadeVal); - um_data_t *um_data = usermods.getAudioData(); + um_data_t *um_data = getAudioData(); int volumeRaw = *(int16_t*)um_data->u_data[1]; if (volumeRaw > 1) { @@ -6930,7 +6939,7 @@ uint16_t mode_blurz(void) { // Blurz. By Andrew Tuline. if (SEGLEN == 1) return mode_static(); // even with 1D effect we have to take logic for 2D segments for allocation as fill_solid() fills whole segment - um_data_t *um_data = usermods.getAudioData(); + um_data_t *um_data = getAudioData(); uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; if (SEGENV.call == 0) { @@ -6963,7 +6972,7 @@ uint16_t mode_DJLight(void) { // Written by ??? Adapted by Wil // No need to prevent from executing on single led strips, only mid will be set (mid = 0) const int mid = SEGLEN / 2; - um_data_t *um_data = usermods.getAudioData(); + um_data_t *um_data = getAudioData(); uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; if (SEGENV.call == 0) { @@ -6995,7 +7004,7 @@ uint16_t mode_freqmap(void) { // Map FFT_MajorPeak to SEGLEN. // Start frequency = 60 Hz and log10(60) = 1.78 // End frequency = MAX_FREQUENCY in Hz and lo10(MAX_FREQUENCY) = MAX_FREQ_LOG10 - um_data_t *um_data = usermods.getAudioData(); + um_data_t *um_data = getAudioData(); float FFT_MajorPeak = *(float*)um_data->u_data[4]; float my_magnitude = *(float*)um_data->u_data[5] / 4.0f; if (FFT_MajorPeak < 1) FFT_MajorPeak = 1; // log10(0) is "forbidden" (throws exception) @@ -7025,7 +7034,7 @@ static const char _data_FX_MODE_FREQMAP[] PROGMEM = "Freqmap@Fade rate,Starting /////////////////////// uint16_t mode_freqmatrix(void) { // Freqmatrix. By Andreas Pleschung. // No need to prevent from executing on single led strips, we simply change pixel 0 each time and avoid the shift - um_data_t *um_data = usermods.getAudioData(); + um_data_t *um_data = getAudioData(); float FFT_MajorPeak = *(float*)um_data->u_data[4]; float volumeSmth = *(float*)um_data->u_data[0]; @@ -7080,7 +7089,7 @@ static const char _data_FX_MODE_FREQMATRIX[] PROGMEM = "Freqmatrix@Speed,Sound e // SEGMENT.speed select faderate // SEGMENT.intensity select colour index uint16_t mode_freqpixels(void) { // Freqpixel. By Andrew Tuline. - um_data_t *um_data = usermods.getAudioData(); + um_data_t *um_data = getAudioData(); float FFT_MajorPeak = *(float*)um_data->u_data[4]; float my_magnitude = *(float*)um_data->u_data[5] / 16.0f; if (FFT_MajorPeak < 1) FFT_MajorPeak = 1.0f; // log10(0) is "forbidden" (throws exception) @@ -7123,7 +7132,7 @@ static const char _data_FX_MODE_FREQPIXELS[] PROGMEM = "Freqpixels@Fade rate,Sta // Depending on the music stream you have you might find it useful to change the frequency mapping. uint16_t mode_freqwave(void) { // Freqwave. By Andreas Pleschung. // As before, this effect can also work on single pixels, we just lose the shifting effect - um_data_t *um_data = usermods.getAudioData(); + um_data_t *um_data = getAudioData(); float FFT_MajorPeak = *(float*)um_data->u_data[4]; float volumeSmth = *(float*)um_data->u_data[0]; @@ -7178,7 +7187,7 @@ uint16_t mode_gravfreq(void) { // Gravfreq. By Andrew Tuline. if (!SEGENV.allocateData(dataSize)) return mode_static(); //allocation failed Gravity* gravcen = reinterpret_cast(SEGENV.data); - um_data_t *um_data = usermods.getAudioData(); + um_data_t *um_data = getAudioData(); float FFT_MajorPeak = *(float*)um_data->u_data[4]; float volumeSmth = *(float*)um_data->u_data[0]; if (FFT_MajorPeak < 1) FFT_MajorPeak = 1; // log10(0) is "forbidden" (throws exception) @@ -7221,7 +7230,7 @@ static const char _data_FX_MODE_GRAVFREQ[] PROGMEM = "Gravfreq@Rate of fall,Sens // ** Noisemove // ////////////////////// uint16_t mode_noisemove(void) { // Noisemove. By: Andrew Tuline - um_data_t *um_data = usermods.getAudioData(); + um_data_t *um_data = getAudioData(); uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; int fadeoutDelay = (256 - SEGMENT.speed) / 96; @@ -7244,7 +7253,7 @@ static const char _data_FX_MODE_NOISEMOVE[] PROGMEM = "Noisemove@Speed of perlin // ** Rocktaves // ////////////////////// uint16_t mode_rocktaves(void) { // Rocktaves. Same note from each octave is same colour. By: Andrew Tuline - um_data_t *um_data = usermods.getAudioData(); + um_data_t *um_data = getAudioData(); float FFT_MajorPeak = *(float*) um_data->u_data[4]; float my_magnitude = *(float*) um_data->u_data[5] / 16.0f; @@ -7282,7 +7291,7 @@ static const char _data_FX_MODE_ROCKTAVES[] PROGMEM = "Rocktaves@;!,!;!;01f;m12= uint16_t mode_waterfall(void) { // Waterfall. By: Andrew Tuline // effect can work on single pixels, we just lose the shifting effect - um_data_t *um_data = usermods.getAudioData(); + um_data_t *um_data = getAudioData(); uint8_t samplePeak = *(uint8_t*)um_data->u_data[3]; float FFT_MajorPeak = *(float*) um_data->u_data[4]; uint8_t *maxVol = (uint8_t*)um_data->u_data[6]; @@ -7337,7 +7346,7 @@ uint16_t mode_2DGEQ(void) { // By Will Tatam. Code reduction by Ewoud Wijma. if (!SEGENV.allocateData(cols*sizeof(uint16_t))) return mode_static(); //allocation failed uint16_t *previousBarHeight = reinterpret_cast(SEGENV.data); //array of previous bar heights per frequency band - um_data_t *um_data = usermods.getAudioData(); + um_data_t *um_data = getAudioData(); uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; if (SEGENV.call == 0) for (int i=0; iu_data[2]; if (SEGENV.call == 0) { diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index 3f0e63338..d95b8ef8e 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -326,7 +326,6 @@ class UsermodManager { void handleOverlayDraw(); bool handleButton(uint8_t b); bool getUMData(um_data_t **um_data, uint8_t mod_id = USERMOD_ID_RESERVED); // USERMOD_ID_RESERVED will poll all usermods - um_data_t *getAudioData(); void setup(); void connected(); void appendConfigData(); diff --git a/wled00/um_manager.cpp b/wled00/um_manager.cpp index 3add18f39..2db29c3cd 100644 --- a/wled00/um_manager.cpp +++ b/wled00/um_manager.cpp @@ -23,14 +23,6 @@ bool UsermodManager::getUMData(um_data_t **data, uint8_t mod_id) { } return false; } -um_data_t* UsermodManager::getAudioData() { - um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { - // add support for no audio - um_data = simulateSound(SEGMENT.soundSim); - } - return um_data; -} void UsermodManager::addToJsonState(JsonObject& obj) { for (unsigned i = 0; i < numMods; i++) ums[i]->addToJsonState(obj); } void UsermodManager::addToJsonInfo(JsonObject& obj) { for (unsigned i = 0; i < numMods; i++) ums[i]->addToJsonInfo(obj); } void UsermodManager::readFromJsonState(JsonObject& obj) { for (unsigned i = 0; i < numMods; i++) ums[i]->readFromJsonState(obj); } From ee1bf1c221d222b225cc85c186e7e4647e2846d0 Mon Sep 17 00:00:00 2001 From: FreakyJ Date: Thu, 15 Aug 2024 20:18:06 +0200 Subject: [PATCH 060/142] #3809 Loxone JSON parser doesn't handle lx=0 correctly --- wled00/json.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wled00/json.cpp b/wled00/json.cpp index 895709680..670a38d13 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -197,11 +197,11 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) // lx parser #ifdef WLED_ENABLE_LOXONE int lx = elem[F("lx")] | -1; - if (lx > 0) { + if (lx >= 0) { parseLxJson(lx, id, false); } int ly = elem[F("ly")] | -1; - if (ly > 0) { + if (ly >= 0) { parseLxJson(ly, id, true); } #endif From 8d00e4d31d49f14b18d7cbe92584ada534c38ccb Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Sat, 17 Aug 2024 15:09:41 +0200 Subject: [PATCH 061/142] Save some tiny amounts of RAM - use `-D WLED_SAVE_RAM` --- wled00/FX.h | 9 +++ wled00/FX_fcn.cpp | 6 +- wled00/cfg.cpp | 24 ++++--- wled00/e131.cpp | 8 +-- wled00/json.cpp | 8 +-- wled00/set.cpp | 8 ++- wled00/wled.h | 180 ++++++++++++++++++++++++++++++++++++++-------- wled00/xml.cpp | 9 +-- 8 files changed, 196 insertions(+), 56 deletions(-) diff --git a/wled00/FX.h b/wled00/FX.h index 276a16703..6a1ae1b6b 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -720,6 +720,9 @@ class WS2812FX { // 96 bytes #ifndef WLED_DISABLE_2D panels(1), #endif + autoSegments(false), + correctWB(false), + cctFromRgb(false), // semi-private (just obscured) used in effect functions through macros _colors_t{0,0,0}, _virtualSegmentLength(0), @@ -908,6 +911,12 @@ class WS2812FX { // 96 bytes void loadCustomPalettes(void); // loads custom palettes from JSON std::vector customPalettes; // TODO: move custom palettes out of WS2812FX class + struct { + bool autoSegments : 1; + bool correctWB : 1; + bool cctFromRgb : 1; + }; + // using public variables to reduce code size increase due to inline function getSegment() (with bounds checking) // and color transitions uint32_t _colors_t[3]; // color used for effect (includes transition) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 0608f6784..c6c0dad4d 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1028,9 +1028,9 @@ void Segment::refreshLightCapabilities() { if (bus->getStart() + bus->getLength() <= segStartIdx) continue; //uint8_t type = bus->getType(); - if (bus->hasRGB() || (cctFromRgb && bus->hasCCT())) capabilities |= SEG_CAPABILITY_RGB; - if (!cctFromRgb && bus->hasCCT()) capabilities |= SEG_CAPABILITY_CCT; - if (correctWB && (bus->hasRGB() || bus->hasCCT())) capabilities |= SEG_CAPABILITY_CCT; //white balance correction (CCT slider) + if (bus->hasRGB() || (strip.cctFromRgb && bus->hasCCT())) capabilities |= SEG_CAPABILITY_RGB; + if (!strip.cctFromRgb && bus->hasCCT()) capabilities |= SEG_CAPABILITY_CCT; + if (strip.correctWB && (bus->hasRGB() || bus->hasCCT())) capabilities |= SEG_CAPABILITY_CCT; //white balance correction (CCT slider) if (bus->hasWhite()) { unsigned aWM = Bus::getGlobalAWMode() == AW_GLOBAL_DISABLED ? bus->getAutoWhiteMode() : Bus::getGlobalAWMode(); bool whiteSlider = (aWM == RGBW_MODE_DUAL || aWM == RGBW_MODE_MANUAL_ONLY); // white slider allowed diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index a6e919dee..6c6e62a25 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -20,17 +20,19 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { //long vid = doc[F("vid")]; // 2010020 - #ifdef WLED_USE_ETHERNET +#ifdef WLED_USE_ETHERNET JsonObject ethernet = doc[F("eth")]; CJSON(ethernetType, ethernet["type"]); // NOTE: Ethernet configuration takes priority over other use of pins WLED::instance().initEthernet(); - #endif +#endif JsonObject id = doc["id"]; getStringFromJson(cmDNS, id[F("mdns")], 33); getStringFromJson(serverDescription, id[F("name")], 33); +#ifndef WLED_DISABLE_ALEXA getStringFromJson(alexaInvocationName, id[F("inv")], 33); +#endif CJSON(simplifiedUI, id[F("sui")]); JsonObject nw = doc["nw"]; @@ -109,8 +111,8 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { uint16_t ablMilliampsMax = hw_led[F("maxpwr")] | BusManager::ablMilliampsMax(); BusManager::setMilliampsMax(ablMilliampsMax); Bus::setGlobalAWMode(hw_led[F("rgbwm")] | AW_GLOBAL_DISABLED); - CJSON(correctWB, hw_led["cct"]); - CJSON(cctFromRgb, hw_led[F("cr")]); + CJSON(strip.correctWB, hw_led["cct"]); + CJSON(strip.cctFromRgb, hw_led[F("cr")]); CJSON(cctICused, hw_led[F("ic")]); CJSON(strip.cctBlending, hw_led[F("cb")]); Bus::setCCTBlend(strip.cctBlending); @@ -431,7 +433,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { JsonObject light = doc[F("light")]; CJSON(briMultiplier, light[F("scale-bri")]); CJSON(strip.paletteBlend, light[F("pal-mode")]); - CJSON(autoSegments, light[F("aseg")]); + CJSON(strip.autoSegments, light[F("aseg")]); CJSON(gammaCorrectVal, light["gc"]["val"]); // default 2.8 float light_gc_bri = light["gc"]["bri"]; @@ -530,12 +532,12 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(arlsDisableGammaCorrection, if_live[F("no-gc")]); // false CJSON(arlsOffset, if_live[F("offset")]); // 0 +#ifndef WLED_DISABLE_ALEXA CJSON(alexaEnabled, interfaces["va"][F("alexa")]); // false - CJSON(macroAlexaOn, interfaces["va"]["macros"][0]); CJSON(macroAlexaOff, interfaces["va"]["macros"][1]); - CJSON(alexaNumPresets, interfaces["va"]["p"]); +#endif #ifndef WLED_DISABLE_MQTT JsonObject if_mqtt = interfaces["mqtt"]; @@ -739,7 +741,9 @@ void serializeConfig() { JsonObject id = root.createNestedObject("id"); id[F("mdns")] = cmDNS; id[F("name")] = serverDescription; +#ifndef WLED_DISABLE_ALEXA id[F("inv")] = alexaInvocationName; +#endif id[F("sui")] = simplifiedUI; JsonObject nw = root.createNestedObject("nw"); @@ -818,8 +822,8 @@ void serializeConfig() { hw_led[F("total")] = strip.getLengthTotal(); //provided for compatibility on downgrade and per-output ABL hw_led[F("maxpwr")] = BusManager::ablMilliampsMax(); hw_led[F("ledma")] = 0; // no longer used - hw_led["cct"] = correctWB; - hw_led[F("cr")] = cctFromRgb; + hw_led["cct"] = strip.correctWB; + hw_led[F("cr")] = strip.cctFromRgb; hw_led[F("ic")] = cctICused; hw_led[F("cb")] = strip.cctBlending; hw_led["fps"] = strip.getTargetFps(); @@ -931,7 +935,7 @@ void serializeConfig() { JsonObject light = root.createNestedObject(F("light")); light[F("scale-bri")] = briMultiplier; light[F("pal-mode")] = strip.paletteBlend; - light[F("aseg")] = autoSegments; + light[F("aseg")] = strip.autoSegments; JsonObject light_gc = light.createNestedObject("gc"); light_gc["bri"] = (gammaCorrectBri) ? gammaCorrectVal : 1.0f; // keep compatibility diff --git a/wled00/e131.cpp b/wled00/e131.cpp index 2d172e072..e28750db3 100644 --- a/wled00/e131.cpp +++ b/wled00/e131.cpp @@ -56,9 +56,9 @@ void handleDDPPacket(e131_packet_t* p) { //E1.31 and Art-Net protocol support void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ - unsigned uni = 0, dmxChannels = 0; + int uni = 0, dmxChannels = 0; uint8_t* e131_data = nullptr; - unsigned seq = 0, mde = REALTIME_MODE_E131; + int seq = 0, mde = REALTIME_MODE_E131; if (protocol == P_ARTNET) { @@ -179,7 +179,7 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ if (uni != e131Universe || availDMXLen < 2) return; // limit max. selectable preset to 250, even though DMX max. val is 255 - unsigned dmxValPreset = (e131_data[dataOffset+1] > 250 ? 250 : e131_data[dataOffset+1]); + int dmxValPreset = (e131_data[dataOffset+1] > 250 ? 250 : e131_data[dataOffset+1]); // only apply preset if value changed if (dmxValPreset != 0 && dmxValPreset != currentPreset && @@ -254,7 +254,7 @@ void handleE131Packet(e131_packet_t* p, IPAddress clientIP, byte protocol){ // Set segment opacity or global brightness if (isSegmentMode) { if (e131_data[dataOffset] != seg.opacity) seg.setOpacity(e131_data[dataOffset]); - } else if ( id == strip.getSegmentsNum()-1 ) { + } else if ( id == strip.getSegmentsNum()-1U ) { if (bri != e131_data[dataOffset]) { bri = e131_data[dataOffset]; strip.setBrightness(bri, true); diff --git a/wled00/json.cpp b/wled00/json.cpp index 670a38d13..01acc7ba3 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -37,14 +37,14 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) Segment prev = seg; //make a backup so we can tell if something changed (calling copy constructor) //DEBUG_PRINTF_P(PSTR("-- Duplicate segment: %p (%p)\n"), &prev, prev.data); - unsigned start = elem["start"] | seg.start; + int start = elem["start"] | seg.start; if (stop < 0) { int len = elem["len"]; stop = (len > 0) ? start + len : seg.stop; } // 2D segments - unsigned startY = elem["startY"] | seg.startY; - unsigned stopY = elem["stopY"] | seg.stopY; + int startY = elem["startY"] | seg.startY; + int stopY = elem["stopY"] | seg.stopY; //repeat, multiplies segment until all LEDs are used, or max segments reached bool repeat = elem["rpt"] | false; @@ -105,7 +105,7 @@ bool deserializeSegment(JsonObject elem, byte it, byte presetId) uint8_t set = elem[F("set")] | seg.set; seg.set = constrain(set, 0, 3); - unsigned len = 1; + int len = 1; if (stop > start) len = stop - start; int offset = elem[F("of")] | INT32_MAX; if (offset != INT32_MAX) { diff --git a/wled00/set.cpp b/wled00/set.cpp index 5eecb5c9a..87323407e 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -130,9 +130,9 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) unsigned ablMilliampsMax = request->arg(F("MA")).toInt(); BusManager::setMilliampsMax(ablMilliampsMax); - autoSegments = request->hasArg(F("MS")); - correctWB = request->hasArg(F("CCT")); - cctFromRgb = request->hasArg(F("CR")); + strip.autoSegments = request->hasArg(F("MS")); + strip.correctWB = request->hasArg(F("CCT")); + strip.cctFromRgb = request->hasArg(F("CR")); cctICused = request->hasArg(F("IC")); strip.cctBlending = request->arg(F("CB")).toInt(); Bus::setCCTBlend(strip.cctBlending); @@ -421,10 +421,12 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) t = request->arg(F("WO")).toInt(); if (t >= -255 && t <= 255) arlsOffset = t; + #ifndef WLED_DISABLE_ALEXA alexaEnabled = request->hasArg(F("AL")); strlcpy(alexaInvocationName, request->arg(F("AI")).c_str(), 33); t = request->arg(F("AP")).toInt(); if (t >= 0 && t <= 9) alexaNumPresets = t; + #endif #ifndef WLED_DISABLE_MQTT mqttEnabled = request->hasArg(F("MQ")); diff --git a/wled00/wled.h b/wled00/wled.h index b381a8a16..b9e675edc 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -321,19 +321,53 @@ WLED_GLOBAL bool rlyOpenDrain _INIT(RLYODRAIN); WLED_GLOBAL char ntpServerName[33] _INIT("0.wled.pool.ntp.org"); // NTP server to use // WiFi CONFIG (all these can be changed via web UI, no need to set them here) -WLED_GLOBAL uint8_t selectedWiFi _INIT(0); WLED_GLOBAL std::vector multiWiFi; WLED_GLOBAL IPAddress dnsAddress _INIT_N((( 8, 8, 8, 8))); // Google's DNS WLED_GLOBAL char cmDNS[33] _INIT(MDNS_NAME); // mDNS address (*.local, replaced by wledXXXXXX if default is used) WLED_GLOBAL char apSSID[33] _INIT(""); // AP off by default (unless setup) +#ifdef WLED_SAVE_RAM +typedef class WiFiOptions { + public: + struct { + uint8_t selectedWiFi : 4; // max 16 SSIDs + uint8_t apChannel : 4; + bool apHide : 1; + uint8_t apBehavior : 3; + bool noWifiSleep : 1; + bool force802_3g : 1; + }; + WiFiOptions(uint8_t s, uint8_t c, bool h, uint8_t b, bool sl, bool g) { + selectedWiFi = s; + apChannel = c; + apHide = h; + apBehavior = b; + noWifiSleep = sl; + force802_3g = g; + } +} __attribute__ ((aligned(1), packed)) wifi_options_t; + #ifdef ARDUINO_ARCH_ESP32 +WLED_GLOBAL wifi_options_t wifiOpt _INIT_N(({0, 1, false, AP_BEHAVIOR_BOOT_NO_CONN, true, false})); + #else +WLED_GLOBAL wifi_options_t wifiOpt _INIT_N(({0, 1, false, AP_BEHAVIOR_BOOT_NO_CONN, false, false})); + #endif +#define selectedWiFi wifiOpt.selectedWiFi +#define apChannel wifiOpt.apChannel +#define apHide wifiOpt.apHide +#define apBehavior wifiOpt.apBehavior +#define noWifiSleep wifiOpt.noWifiSleep +#define force802_3g wifiOpt.force802_3g +#else +WLED_GLOBAL uint8_t selectedWiFi _INIT(0); WLED_GLOBAL byte apChannel _INIT(1); // 2.4GHz WiFi AP channel (1-13) WLED_GLOBAL byte apHide _INIT(0); // hidden AP SSID WLED_GLOBAL byte apBehavior _INIT(AP_BEHAVIOR_BOOT_NO_CONN); // access point opens when no connection after boot by default -#ifdef ARDUINO_ARCH_ESP32 + #ifdef ARDUINO_ARCH_ESP32 WLED_GLOBAL bool noWifiSleep _INIT(true); // disabling modem sleep modes will increase heat output and power usage, but may help with connection issues -#else + #else WLED_GLOBAL bool noWifiSleep _INIT(false); -#endif + #endif +WLED_GLOBAL bool force802_3g _INIT(false); +#endif // WLED_SAVE_RAM #ifdef ARDUINO_ARCH_ESP32 #if defined(LOLIN_WIFI_FIX) && (defined(ARDUINO_ARCH_ESP32C3) || defined(ARDUINO_ARCH_ESP32S2) || defined(ARDUINO_ARCH_ESP32S3)) WLED_GLOBAL uint8_t txPower _INIT(WIFI_POWER_8_5dBm); @@ -341,7 +375,6 @@ WLED_GLOBAL uint8_t txPower _INIT(WIFI_POWER_8_5dBm); WLED_GLOBAL uint8_t txPower _INIT(WIFI_POWER_19_5dBm); #endif #endif -WLED_GLOBAL bool force802_3g _INIT(false); #define WLED_WIFI_CONFIGURED (strlen(multiWiFi[0].clientSSID) >= 1 && strcmp(multiWiFi[0].clientSSID, DEFAULT_CLIENT_SSID) != 0) #ifdef WLED_USE_ETHERNET @@ -359,14 +392,11 @@ WLED_GLOBAL byte bootPreset _INIT(0); // save preset to load //if true, a segment per bus will be created on boot and LED settings save //if false, only one segment spanning the total LEDs is created, //but not on LED settings save if there is more than one segment currently -WLED_GLOBAL bool autoSegments _INIT(false); #ifdef ESP8266 WLED_GLOBAL bool useGlobalLedBuffer _INIT(false); // double buffering disabled on ESP8266 #else WLED_GLOBAL bool useGlobalLedBuffer _INIT(true); // double buffering enabled on ESP32 #endif -WLED_GLOBAL bool correctWB _INIT(false); // CCT color correction of RGB color -WLED_GLOBAL bool cctFromRgb _INIT(false); // CCT is calculated from RGB instead of using seg.cct #ifdef WLED_USE_IC_CCT WLED_GLOBAL bool cctICused _INIT(true); // CCT IC used (Athom 15W bulbs) #else @@ -378,7 +408,6 @@ WLED_GLOBAL float gammaCorrectVal _INIT(2.8f); // gamma correction value WLED_GLOBAL byte col[] _INIT_N(({ 255, 160, 0, 0 })); // current RGB(W) primary color. col[] should be updated if you want to change the color. WLED_GLOBAL byte colSec[] _INIT_N(({ 0, 0, 0, 0 })); // current RGB(W) secondary color -WLED_GLOBAL byte briS _INIT(128); // default brightness WLED_GLOBAL byte nightlightTargetBri _INIT(0); // brightness after nightlight is over WLED_GLOBAL byte nightlightDelayMins _INIT(60); @@ -406,30 +435,14 @@ WLED_GLOBAL byte irEnabled _INIT(IRTYPE); // Infrared receiver #endif WLED_GLOBAL bool irApplyToAllSelected _INIT(true); //apply IR or ESP-NOW to all selected segments -WLED_GLOBAL uint16_t udpPort _INIT(21324); // WLED notifier default port -WLED_GLOBAL uint16_t udpPort2 _INIT(65506); // WLED notifier supplemental port -WLED_GLOBAL uint16_t udpRgbPort _INIT(19446); // Hyperion port - -WLED_GLOBAL uint8_t syncGroups _INIT(0x01); // sync groups this instance syncs (bit mapped) -WLED_GLOBAL uint8_t receiveGroups _INIT(0x01); // sync receive groups this instance belongs to (bit mapped) -WLED_GLOBAL bool receiveNotificationBrightness _INIT(true); // apply brightness from incoming notifications -WLED_GLOBAL bool receiveNotificationColor _INIT(true); // apply color -WLED_GLOBAL bool receiveNotificationEffects _INIT(true); // apply effects setup -WLED_GLOBAL bool receiveSegmentOptions _INIT(false); // apply segment options -WLED_GLOBAL bool receiveSegmentBounds _INIT(false); // apply segment bounds (start, stop, offset) -WLED_GLOBAL bool notifyDirect _INIT(false); // send notification if change via UI or HTTP API -WLED_GLOBAL bool notifyButton _INIT(false); // send if updated by button or infrared remote -WLED_GLOBAL bool notifyAlexa _INIT(false); // send notification if updated via Alexa -WLED_GLOBAL bool notifyHue _INIT(true); // send notification if Hue light changes -WLED_GLOBAL uint8_t udpNumRetries _INIT(0); // Number of times a UDP sync message is retransmitted. Increase to increase reliability - +#ifndef WLED_DISABLE_ALEXA WLED_GLOBAL bool alexaEnabled _INIT(false); // enable device discovery by Amazon Echo WLED_GLOBAL char alexaInvocationName[33] _INIT("Light"); // speech control name of device. Choose something voice-to-text can understand WLED_GLOBAL byte alexaNumPresets _INIT(0); // number of presets to expose to Alexa, starting from preset 1, up to 9 +#endif WLED_GLOBAL uint16_t realtimeTimeoutMs _INIT(2500); // ms timeout of realtime mode before returning to normal mode WLED_GLOBAL int arlsOffset _INIT(0); // realtime LED offset -WLED_GLOBAL bool receiveDirect _INIT(true); // receive UDP/Hyperion realtime WLED_GLOBAL bool arlsDisableGammaCorrection _INIT(true); // activate if gamma correction is handled by the source WLED_GLOBAL bool arlsForceMaxBri _INIT(false); // enable to force max brightness if source has very dark colors that would be black @@ -587,6 +600,7 @@ WLED_GLOBAL byte colNlT[] _INIT_N(({ 0, 0, 0, 0 })); // current nightligh // brightness WLED_GLOBAL unsigned long lastOnTime _INIT(0); WLED_GLOBAL bool offMode _INIT(!turnOnAtBoot); +WLED_GLOBAL byte briS _INIT(128); // default brightness WLED_GLOBAL byte bri _INIT(briS); // global brightness (set) WLED_GLOBAL byte briOld _INIT(0); // global brightness while in transition loop (previous iteration) WLED_GLOBAL byte briT _INIT(0); // global brightness during transition @@ -610,6 +624,77 @@ WLED_GLOBAL bool sendNotificationsRT _INIT(false); // master notifica WLED_GLOBAL unsigned long notificationSentTime _INIT(0); WLED_GLOBAL byte notificationSentCallMode _INIT(CALL_MODE_INIT); WLED_GLOBAL uint8_t notificationCount _INIT(0); +WLED_GLOBAL uint8_t syncGroups _INIT(0x01); // sync send groups this instance syncs to (bit mapped) +WLED_GLOBAL uint8_t receiveGroups _INIT(0x01); // sync receive groups this instance belongs to (bit mapped) +#ifdef WLED_SAVE_RAM +// this will save us 8 bytes of RAM while increasing code by ~400 bytes +typedef class Receive { + public: + union { + uint8_t Options; + struct { + bool Brightness : 1; + bool Color : 1; + bool Effects : 1; + bool SegmentOptions : 1; + bool SegmentBounds : 1; + bool Direct : 1; + uint8_t reserved : 2; + }; + }; + Receive(int i) { Options = i; } + Receive(bool b, bool c, bool e, bool sO, bool sB) { + Brightness = b; + Color = c; + Effects = e; + SegmentOptions = sO; + SegmentBounds = sB; + }; +} __attribute__ ((aligned(1), packed)) receive_notification_t; +typedef class Send { + public: + union { + uint8_t Options; + struct { + bool Direct : 1; + bool Button : 1; + bool Alexa : 1; + bool Hue : 1; + uint8_t reserved : 4; + }; + }; + Send(int o) { Options = o; } + Send(bool d, bool b, bool a, bool h) { + Direct = d; + Button = b; + Alexa = a; + Hue = h; + } +} __attribute__ ((aligned(1), packed)) send_notification_t; +WLED_GLOBAL receive_notification_t receiveN _INIT(0b00100111); +WLED_GLOBAL send_notification_t notifyG _INIT(0b00001111); +#define receiveNotificationBrightness receiveN.Brightness +#define receiveNotificationColor receiveN.Color +#define receiveNotificationEffects receiveN.Effects +#define receiveSegmentOptions receiveN.SegmentOptions +#define receiveSegmentBounds receiveN.SegmentBounds +#define receiveDirect receiveN.Direct +#define notifyDirect notifyG.Direct +#define notifyButton notifyG.Button +#define notifyAlexa notifyG.Alexa +#define notifyHue notifyG.Hue +#else +WLED_GLOBAL bool receiveNotificationBrightness _INIT(true); // apply brightness from incoming notifications +WLED_GLOBAL bool receiveNotificationColor _INIT(true); // apply color +WLED_GLOBAL bool receiveNotificationEffects _INIT(true); // apply effects setup +WLED_GLOBAL bool receiveSegmentOptions _INIT(false); // apply segment options +WLED_GLOBAL bool receiveSegmentBounds _INIT(false); // apply segment bounds (start, stop, offset) +WLED_GLOBAL bool receiveDirect _INIT(true); // receive UDP/Hyperion realtime +WLED_GLOBAL bool notifyDirect _INIT(false); // send notification if change via UI or HTTP API +WLED_GLOBAL bool notifyButton _INIT(false); // send if updated by button or infrared remote +WLED_GLOBAL bool notifyAlexa _INIT(false); // send notification if updated via Alexa +WLED_GLOBAL bool notifyHue _INIT(true); // send notification if Hue light changes +#endif // effects WLED_GLOBAL byte effectCurrent _INIT(0); @@ -619,7 +704,46 @@ WLED_GLOBAL byte effectPalette _INIT(0); WLED_GLOBAL bool stateChanged _INIT(false); // network -WLED_GLOBAL bool udpConnected _INIT(false), udp2Connected _INIT(false), udpRgbConnected _INIT(false); +#ifdef WLED_SAVE_RAM +// this will save us 2 bytes of RAM while increasing code by ~400 bytes +typedef class Udp { + public: + uint16_t Port; + uint16_t Port2; + uint16_t RgbPort; + struct { + uint8_t NumRetries : 5; + bool Connected : 1; + bool Connected2 : 1; + bool RgbConnected : 1; + }; + Udp(int p1, int p2, int p3, int r, bool c1, bool c2, bool c3) { + Port = p1; + Port2 = p2; + RgbPort = p3; + NumRetries = r; + Connected = c1; + Connected2 = c2; + RgbConnected = c3; + } +} __attribute__ ((aligned(1), packed)) udp_port_t; +WLED_GLOBAL udp_port_t udp _INIT_N(({21234, 65506, 19446, 0, false, false, false})); +#define udpPort udp.Port +#define udpPort2 udp.Port2 +#define udpRgbPort udp.RgbPort +#define udpNumRetries udp.NumRetries +#define udpConnected udp.Connected +#define udp2Connected udp.Connected2 +#define udpRgbConnected udp.RgbConnected +#else +WLED_GLOBAL uint16_t udpPort _INIT(21324); // WLED notifier default port +WLED_GLOBAL uint16_t udpPort2 _INIT(65506); // WLED notifier supplemental port +WLED_GLOBAL uint16_t udpRgbPort _INIT(19446); // Hyperion port +WLED_GLOBAL uint8_t udpNumRetries _INIT(0); // Number of times a UDP sync message is retransmitted. Increase to increase reliability +WLED_GLOBAL bool udpConnected _INIT(false); +WLED_GLOBAL bool udp2Connected _INIT(false); +WLED_GLOBAL bool udpRgbConnected _INIT(false); +#endif // ui style WLED_GLOBAL bool showWelcomePage _INIT(false); diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 2ae599f5d..0439b7bdc 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -361,10 +361,10 @@ void getSettingsJS(byte subPage, char* dest) oappend(itoa(WLED_MAX_ANALOG_CHANNELS,nS,10)); oappend(SET_F(");")); - sappend('c',SET_F("MS"),autoSegments); - sappend('c',SET_F("CCT"),correctWB); + sappend('c',SET_F("MS"),strip.autoSegments); + sappend('c',SET_F("CCT"),strip.correctWB); sappend('c',SET_F("IC"),cctICused); - sappend('c',SET_F("CR"),cctFromRgb); + sappend('c',SET_F("CR"),strip.cctFromRgb); sappend('v',SET_F("CB"),strip.cctBlending); sappend('v',SET_F("FR"),strip.getTargetFps()); sappend('v',SET_F("AW"),Bus::getGlobalAWMode()); @@ -533,11 +533,12 @@ void getSettingsJS(byte subPage, char* dest) sappend('c',SET_F("FB"),arlsForceMaxBri); sappend('c',SET_F("RG"),arlsDisableGammaCorrection); sappend('v',SET_F("WO"),arlsOffset); + #ifndef WLED_DISABLE_ALEXA sappend('c',SET_F("AL"),alexaEnabled); sappends('s',SET_F("AI"),alexaInvocationName); sappend('c',SET_F("SA"),notifyAlexa); sappend('v',SET_F("AP"),alexaNumPresets); - #ifdef WLED_DISABLE_ALEXA + #elese oappend(SET_F("toggle('Alexa');")); // hide Alexa settings #endif From 79b3ce141cf3ab014411860f06380a6aef9581eb Mon Sep 17 00:00:00 2001 From: srg74 <28492985+srg74@users.noreply.github.com> Date: Sat, 17 Aug 2024 14:42:44 -0400 Subject: [PATCH 062/142] correct spelling error xml.cpp --- wled00/xml.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 0439b7bdc..25c0c6298 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -538,7 +538,7 @@ void getSettingsJS(byte subPage, char* dest) sappends('s',SET_F("AI"),alexaInvocationName); sappend('c',SET_F("SA"),notifyAlexa); sappend('v',SET_F("AP"),alexaNumPresets); - #elese + #else oappend(SET_F("toggle('Alexa');")); // hide Alexa settings #endif From f6c47ac19cd9fc2b6b2861289b86f2ef963a329a Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Sun, 18 Aug 2024 10:45:16 +0200 Subject: [PATCH 063/142] Fix some compiler warnings --- wled00/FX_2Dfcn.cpp | 10 +++++----- wled00/FX_fcn.cpp | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index 0fd5bb09f..44919f925 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -185,14 +185,14 @@ void IRAM_ATTR Segment::setPixelColorXY(int x, int y, uint32_t col) x *= groupLength(); // expand to physical pixels y *= groupLength(); // expand to physical pixels - unsigned W = width(); - unsigned H = height(); + int W = width(); + int H = height(); if (x >= W || y >= H) return; // if pixel would fall out of segment just exit uint32_t tmpCol = col; - for (unsigned j = 0; j < grouping; j++) { // groupping vertically - for (unsigned g = 0; g < grouping; g++) { // groupping horizontally - unsigned xX = (x+g), yY = (y+j); + for (int j = 0; j < grouping; j++) { // groupping vertically + for (int g = 0; g < grouping; g++) { // groupping horizontally + int xX = (x+g), yY = (y+j); if (xX >= W || yY >= H) continue; // we have reached one dimension's end #ifndef WLED_DISABLE_MODE_BLEND diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index c6c0dad4d..bc5c8b051 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -905,8 +905,8 @@ uint32_t IRAM_ATTR Segment::getPixelColor(int i) const #ifndef WLED_DISABLE_2D if (is2D()) { - unsigned vH = virtualHeight(); // segment height in logical pixels - unsigned vW = virtualWidth(); + int vH = virtualHeight(); // segment height in logical pixels + int vW = virtualWidth(); switch (map1D2D) { case M12_Pixels: return getPixelColorXY(i % vW, i / vW); From 665693a513d893788b539d1ac55f12b2a34dd258 Mon Sep 17 00:00:00 2001 From: Robin Meis Date: Sun, 18 Aug 2024 13:31:14 +0200 Subject: [PATCH 064/142] Remove minimum threshold according to https://github.com/Aircoookie/WLED/pull/4081#issuecomment-2295198219 --- wled00/set.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/set.cpp b/wled00/set.cpp index 13295df21..48cf93c80 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -289,7 +289,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) #ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a fucntion to check touch state but need to attach an interrupt to do so else { - touchAttachInterrupt(btnPin[i], touchButtonISR, 256 + (touchThreshold << 4)); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000) + touchAttachInterrupt(btnPin[i], touchButtonISR, touchThreshold << 4); // threshold on Touch V2 is much higher (1500 is a value given by Espressif example, I measured changes of over 5000) } #endif } From e5a426419c78cdd51f6ebdc4555a3576e14a0256 Mon Sep 17 00:00:00 2001 From: Maximilian Mewes Date: Mon, 19 Aug 2024 22:07:08 +0200 Subject: [PATCH 065/142] Improve mqtt support, add battery percentage and voltage --- usermods/Battery/usermod_v2_Battery.h | 78 ++++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 7 deletions(-) diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h index f240d55f5..e22717db4 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/usermod_v2_Battery.h @@ -115,6 +115,58 @@ class UsermodBattery : public Usermod #endif } +#ifndef WLED_DISABLE_MQTT + void addMqttSensor(const String &name, const String &type, const String &topic, const String &deviceClass, const String &unitOfMeasurement = "", const bool &isDiagnostic = false) + { + // String t = String(F("homeassistant/sensor/")) + mqttClientID + F("/") + name + F("/config"); + + StaticJsonDocument<600> doc; + char uid[128], json_str[1024], buf[128]; + + doc[F("name")] = name; + doc[F("stat_t")] = topic; + sprintf_P(uid, PSTR("%s_%s_sensor"), name, escapedMac.c_str()); + doc[F("uniq_id")] = uid; + doc[F("dev_cla")] = deviceClass; + // doc[F("exp_aft")] = 1800; + + if(type == "binary_sensor") { + doc[F("pl_on")] = "on"; + doc[F("pl_off")] = "off"; + } + + if(unitOfMeasurement != "") + doc[F("unit_of_measurement")] = unitOfMeasurement; + + if(isDiagnostic) + doc[F("entity_category")] = "diagnostic"; + + + JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device + device[F("name")] = serverDescription; + device[F("ids")] = String(F("wled-sensor-")) + mqttClientID; + device[F("mf")] = F(WLED_BRAND); + device[F("mdl")] = F(WLED_PRODUCT_NAME); + device[F("sw")] = versionString; + + sprintf_P(buf, PSTR("homeassistant/%s/%s/%s/config"), type, mqttClientID, uid); + DEBUG_PRINTLN(buf); + size_t payload_size = serializeJson(doc, json_str); + DEBUG_PRINTLN(json_str); + + mqtt->publish(buf, 0, true, json_str, payload_size); + } + + void publishMqtt(const char* topic, const char* state) + { + if (WLED_MQTT_CONNECTED) { + char buf[128]; + snprintf_P(buf, 127, PSTR("%s/%s"), mqttDeviceTopic, topic); + mqtt->publish(buf, 0, false, state); + } + } +#endif + public: //Functions called by WLED @@ -223,13 +275,8 @@ class UsermodBattery : public Usermod turnOff(); #ifndef WLED_DISABLE_MQTT - // SmartHome stuff - // still don't know much about MQTT and/or HA - if (WLED_MQTT_CONNECTED) { - char buf[64]; // buffer for snprintf() - snprintf_P(buf, 63, PSTR("%s/voltage"), mqttDeviceTopic); - mqtt->publish(buf, 0, false, String(bat->getVoltage()).c_str()); - } + publishMqtt("battery", String(bat->getLevel(), 0).c_str()); + publishMqtt("voltage", String(bat->getVoltage()).c_str()); #endif } @@ -513,6 +560,23 @@ class UsermodBattery : public Usermod return !battery[FPSTR(_readInterval)].isNull(); } +#ifndef WLED_DISABLE_MQTT + void onMqttConnect(bool sessionPresent) + { + // Home Assistant Autodiscovery + + // battery percentage + char mqttBatteryTopic[128]; + snprintf_P(mqttBatteryTopic, 127, PSTR("%s/battery"), mqttDeviceTopic); + this->addMqttSensor(F("Battery"), "sensor", mqttBatteryTopic, "battery", "%", true); + + // voltage + char mqttVoltageTopic[128]; + snprintf_P(mqttVoltageTopic, 127, PSTR("%s/voltage"), mqttDeviceTopic); + this->addMqttSensor(F("Voltage"), "sensor", mqttVoltageTopic, "voltage", "V", true); + } +#endif + /** * TBD: Generate a preset sample for low power indication * a button on the config page would be cool, currently not possible From b8f15333d857f92b9346c16f75eab300882f87e2 Mon Sep 17 00:00:00 2001 From: Maximilian Mewes Date: Mon, 19 Aug 2024 22:12:21 +0200 Subject: [PATCH 066/142] update `readme.md` --- usermods/Battery/readme.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/usermods/Battery/readme.md b/usermods/Battery/readme.md index 84a6f5054..c3d3d8bf4 100644 --- a/usermods/Battery/readme.md +++ b/usermods/Battery/readme.md @@ -131,6 +131,11 @@ Specification from: [Molicel INR18650-M35A, 3500mAh 10A Lithium-ion battery, 3.6 ## 📝 Change Log +2024-08-19 + +- Improved MQTT support +- Added battery percentage & battery voltage as MQTT topic + 2024-05-11 - Documentation updated From cc24119a590e3256ad2e81841bfa3cf76ed00bfc Mon Sep 17 00:00:00 2001 From: Maximilian Mewes Date: Mon, 19 Aug 2024 22:22:46 +0200 Subject: [PATCH 067/142] remove unnecessary comments --- usermods/Battery/usermod_v2_Battery.h | 43 ++------------------------- 1 file changed, 3 insertions(+), 40 deletions(-) diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h index e22717db4..c9d3b639e 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/usermod_v2_Battery.h @@ -117,9 +117,7 @@ class UsermodBattery : public Usermod #ifndef WLED_DISABLE_MQTT void addMqttSensor(const String &name, const String &type, const String &topic, const String &deviceClass, const String &unitOfMeasurement = "", const bool &isDiagnostic = false) - { - // String t = String(F("homeassistant/sensor/")) + mqttClientID + F("/") + name + F("/config"); - + { StaticJsonDocument<600> doc; char uid[128], json_str[1024], buf[128]; @@ -128,11 +126,11 @@ class UsermodBattery : public Usermod sprintf_P(uid, PSTR("%s_%s_sensor"), name, escapedMac.c_str()); doc[F("uniq_id")] = uid; doc[F("dev_cla")] = deviceClass; - // doc[F("exp_aft")] = 1800; if(type == "binary_sensor") { doc[F("pl_on")] = "on"; doc[F("pl_off")] = "off"; + doc[F("exp_aft")] = 1800; } if(unitOfMeasurement != "") @@ -141,7 +139,6 @@ class UsermodBattery : public Usermod if(isDiagnostic) doc[F("entity_category")] = "diagnostic"; - JsonObject device = doc.createNestedObject(F("device")); // attach the sensor to the same device device[F("name")] = serverDescription; device[F("ids")] = String(F("wled-sensor-")) + mqttClientID; @@ -525,7 +522,6 @@ class UsermodBattery : public Usermod #ifdef ARDUINO_ARCH_ESP32 newBatteryPin = battery[F("pin")] | newBatteryPin; #endif - // calculateTimeLeftEnabled = battery[F("time-left")] | calculateTimeLeftEnabled; setMinBatteryVoltage(battery[F("min-voltage")] | bat->getMinVoltage()); setMaxBatteryVoltage(battery[F("max-voltage")] | bat->getMaxVoltage()); setCalibration(battery[F("calibration")] | bat->getCalibration()); @@ -575,40 +571,7 @@ class UsermodBattery : public Usermod snprintf_P(mqttVoltageTopic, 127, PSTR("%s/voltage"), mqttDeviceTopic); this->addMqttSensor(F("Voltage"), "sensor", mqttVoltageTopic, "voltage", "V", true); } -#endif - - /** - * TBD: Generate a preset sample for low power indication - * a button on the config page would be cool, currently not possible - */ - void generateExamplePreset() - { - // StaticJsonDocument<300> j; - // JsonObject preset = j.createNestedObject(); - // preset["mainseg"] = 0; - // JsonArray seg = preset.createNestedArray("seg"); - // JsonObject seg0 = seg.createNestedObject(); - // seg0["id"] = 0; - // seg0["start"] = 0; - // seg0["stop"] = 60; - // seg0["grp"] = 0; - // seg0["spc"] = 0; - // seg0["on"] = true; - // seg0["bri"] = 255; - - // JsonArray col0 = seg0.createNestedArray("col"); - // JsonArray col00 = col0.createNestedArray(); - // col00.add(255); - // col00.add(0); - // col00.add(0); - - // seg0["fx"] = 1; - // seg0["sx"] = 128; - // seg0["ix"] = 128; - - // savePreset(199, "Low power Indicator", preset); - } - +#endif /* * From 2d6365dc6a6a0bdb6606312478e8439eff6fa7b4 Mon Sep 17 00:00:00 2001 From: Maximilian Mewes Date: Tue, 20 Aug 2024 12:37:01 +0200 Subject: [PATCH 068/142] Add HA-discovery as config option --- usermods/Battery/usermod_v2_Battery.h | 61 +++++++++++++++++++++------ 1 file changed, 48 insertions(+), 13 deletions(-) diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h index c9d3b639e..136d3a71a 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/usermod_v2_Battery.h @@ -50,6 +50,7 @@ class UsermodBattery : public Usermod // bool initDone = false; bool initializing = true; + bool HomeAssistantDiscovery = false; // strings to reduce flash memory usage (used more than twice) static const char _name[]; @@ -59,6 +60,7 @@ class UsermodBattery : public Usermod static const char _preset[]; static const char _duration[]; static const char _init[]; + static const char _haDiscovery[]; /** * Helper for rounding floating point values @@ -69,6 +71,17 @@ class UsermodBattery : public Usermod return (float)(nx / 100); } + /** + * Helper for converting a string to lowercase + */ + String stringToLower(String str) + { + for(int i = 0; i < str.length(); i++) + if(str[i] >= 'A' && str[i] <= 'Z') + str[i] += 32; + return str; + } + /** * Turn off all leds */ @@ -123,14 +136,14 @@ class UsermodBattery : public Usermod doc[F("name")] = name; doc[F("stat_t")] = topic; - sprintf_P(uid, PSTR("%s_%s_sensor"), name, escapedMac.c_str()); + sprintf_P(uid, PSTR("%s_%s_%s"), escapedMac.c_str(), stringToLower(name).c_str(), type); doc[F("uniq_id")] = uid; doc[F("dev_cla")] = deviceClass; + doc[F("exp_aft")] = 1800; if(type == "binary_sensor") { doc[F("pl_on")] = "on"; doc[F("pl_off")] = "off"; - doc[F("exp_aft")] = 1800; } if(unitOfMeasurement != "") @@ -332,6 +345,7 @@ class UsermodBattery : public Usermod battery[F("calibration")] = bat->getCalibration(); battery[F("voltage-multiplier")] = bat->getVoltageMultiplier(); battery[FPSTR(_readInterval)] = readingInterval; + battery[FPSTR(_haDiscovery)] = HomeAssistantDiscovery; JsonObject ao = battery.createNestedObject(F("auto-off")); // auto off section ao[FPSTR(_enabled)] = autoOffEnabled; @@ -351,8 +365,8 @@ class UsermodBattery : public Usermod getJsonValue(battery[F("max-voltage")], cfg.maxVoltage); getJsonValue(battery[F("calibration")], cfg.calibration); getJsonValue(battery[F("voltage-multiplier")], cfg.voltageMultiplier); - setReadingInterval(battery[FPSTR(_readInterval)] | readingInterval); + setHomeAssistantDiscovery(battery[FPSTR(_haDiscovery)] | HomeAssistantDiscovery); JsonObject ao = battery[F("auto-off")]; setAutoOffEnabled(ao[FPSTR(_enabled)] | autoOffEnabled); @@ -464,17 +478,18 @@ class UsermodBattery : public Usermod void appendConfigData() { // Total: 462 Bytes - oappend(SET_F("td=addDropdown('Battery', 'type');")); // 35 Bytes - oappend(SET_F("addOption(td, 'Unkown', '0');")); // 30 Bytes - oappend(SET_F("addOption(td, 'LiPo', '1');")); // 28 Bytes - oappend(SET_F("addOption(td, 'LiOn', '2');")); // 28 Bytes + oappend(SET_F("td=addDropdown('Battery','type');")); // 34 Bytes + oappend(SET_F("addOption(td,'Unkown','0');")); // 28 Bytes + oappend(SET_F("addOption(td,'LiPo','1');")); // 26 Bytes + oappend(SET_F("addOption(td,'LiOn','2');")); // 26 Bytes oappend(SET_F("addInfo('Battery:type',1,'requires reboot');")); // 81 Bytes - oappend(SET_F("addInfo('Battery:min-voltage', 1, 'v');")); // 40 Bytes - oappend(SET_F("addInfo('Battery:max-voltage', 1, 'v');")); // 40 Bytes - oappend(SET_F("addInfo('Battery:interval', 1, 'ms');")); // 38 Bytes - oappend(SET_F("addInfo('Battery:auto-off:threshold', 1, '%');")); // 47 Bytes - oappend(SET_F("addInfo('Battery:indicator:threshold', 1, '%');")); // 48 Bytes - oappend(SET_F("addInfo('Battery:indicator:duration', 1, 's');")); // 47 Bytes + oappend(SET_F("addInfo('Battery:min-voltage',1,'v');")); // 38 Bytes + oappend(SET_F("addInfo('Battery:max-voltage',1,'v');")); // 38 Bytes + oappend(SET_F("addInfo('Battery:interval',1,'ms');")); // 36 Bytes + oappend(SET_F("addInfo('Battery:HA-discovery',1,'');")); // 38 Bytes + oappend(SET_F("addInfo('Battery:auto-off:threshold',1,'%');")); // 45 Bytes + oappend(SET_F("addInfo('Battery:indicator:threshold',1,'%');")); // 46 Bytes + oappend(SET_F("addInfo('Battery:indicator:duration',1,'s');")); // 45 Bytes // this option list would exeed the oappend() buffer // a list of all presets to select one from @@ -527,6 +542,7 @@ class UsermodBattery : public Usermod setCalibration(battery[F("calibration")] | bat->getCalibration()); setVoltageMultiplier(battery[F("voltage-multiplier")] | bat->getVoltageMultiplier()); setReadingInterval(battery[FPSTR(_readInterval)] | readingInterval); + setHomeAssistantDiscovery(battery[FPSTR(_haDiscovery)] | HomeAssistantDiscovery); getUsermodConfigFromJsonObject(battery); @@ -560,6 +576,8 @@ class UsermodBattery : public Usermod void onMqttConnect(bool sessionPresent) { // Home Assistant Autodiscovery + if (!HomeAssistantDiscovery) + return; // battery percentage char mqttBatteryTopic[128]; @@ -812,6 +830,22 @@ class UsermodBattery : public Usermod { return lowPowerIndicationDone; } + + /** + * Set Home Assistant auto discovery + */ + void setHomeAssistantDiscovery(bool enable) + { + HomeAssistantDiscovery = enable; + } + + /** + * Get Home Assistant auto discovery + */ + bool getHomeAssistantDiscovery() + { + return HomeAssistantDiscovery; + } }; // strings to reduce flash memory usage (used more than twice) @@ -822,3 +856,4 @@ const char UsermodBattery::_threshold[] PROGMEM = "threshold"; const char UsermodBattery::_preset[] PROGMEM = "preset"; const char UsermodBattery::_duration[] PROGMEM = "duration"; const char UsermodBattery::_init[] PROGMEM = "init"; +const char UsermodBattery::_haDiscovery[] PROGMEM = "HA-discovery"; From e7babc071ddfe84b6b7da0c5666cb2b842a396bd Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 20 Aug 2024 20:15:17 +0200 Subject: [PATCH 069/142] replaced PWM LUT with calculation --- wled00/bus_manager.cpp | 43 ++++++++++-------------------------------- 1 file changed, 10 insertions(+), 33 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index d0e32b211..2ae624fee 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -497,45 +497,22 @@ uint32_t BusPwm::getPixelColor(uint16_t pix) { return RGBW32(_data[0], _data[0], _data[0], _data[0]); } -#ifndef ESP8266 -static const uint16_t cieLUT[256] = { - 0, 2, 4, 5, 7, 9, 11, 13, 15, 16, - 18, 20, 22, 24, 26, 27, 29, 31, 33, 35, - 34, 36, 37, 39, 41, 43, 45, 47, 49, 52, - 54, 56, 59, 61, 64, 67, 69, 72, 75, 78, - 81, 84, 87, 90, 94, 97, 100, 104, 108, 111, - 115, 119, 123, 127, 131, 136, 140, 144, 149, 154, - 158, 163, 168, 173, 178, 183, 189, 194, 200, 205, - 211, 217, 223, 229, 235, 241, 247, 254, 261, 267, - 274, 281, 288, 295, 302, 310, 317, 325, 333, 341, - 349, 357, 365, 373, 382, 391, 399, 408, 417, 426, - 436, 445, 455, 464, 474, 484, 494, 505, 515, 526, - 536, 547, 558, 569, 580, 592, 603, 615, 627, 639, - 651, 663, 676, 689, 701, 714, 727, 741, 754, 768, - 781, 795, 809, 824, 838, 853, 867, 882, 897, 913, - 928, 943, 959, 975, 991, 1008, 1024, 1041, 1058, 1075, - 1092, 1109, 1127, 1144, 1162, 1180, 1199, 1217, 1236, 1255, - 1274, 1293, 1312, 1332, 1352, 1372, 1392, 1412, 1433, 1454, - 1475, 1496, 1517, 1539, 1561, 1583, 1605, 1628, 1650, 1673, - 1696, 1719, 1743, 1767, 1791, 1815, 1839, 1864, 1888, 1913, - 1939, 1964, 1990, 2016, 2042, 2068, 2095, 2121, 2148, 2176, - 2203, 2231, 2259, 2287, 2315, 2344, 2373, 2402, 2431, 2461, - 2491, 2521, 2551, 2581, 2612, 2643, 2675, 2706, 2738, 2770, - 2802, 2835, 2867, 2900, 2934, 2967, 3001, 3035, 3069, 3104, - 3138, 3174, 3209, 3244, 3280, 3316, 3353, 3389, 3426, 3463, - 3501, 3539, 3576, 3615, 3653, 3692, 3731, 3770, 3810, 3850, - 3890, 3930, 3971, 4012, 4053, 4095 -}; -#endif - void BusPwm::show() { if (!_valid) return; unsigned numPins = NUM_PWM_PINS(_type); unsigned maxBri = (1<<_depth) - 1; #ifdef ESP8266 unsigned pwmBri = (unsigned)(roundf(powf((float)_bri / 255.0f, 1.7f) * (float)maxBri)); // using gamma 1.7 to extrapolate PWM duty cycle - #else - unsigned pwmBri = cieLUT[_bri] >> (12 - _depth); // use CIE LUT + #else // use CIE brightness formula + unsigned pwmBri = (unsigned)_bri * 100; + if(pwmBri < 2040) pwmBri = ((pwmBri << _depth) + 115043) / 230087; //adding '0.5' before division for correct rounding + else { + pwmBri += 4080; + float temp = (float)pwmBri / 29580; + temp = temp * temp * temp * (1<<_depth) - 1; + pwmBri = (unsigned)temp; + } + Serial.println(pwmBri); #endif for (unsigned i = 0; i < numPins; i++) { unsigned scaled = (_data[i] * pwmBri) / 255; From 1cc47b02cf7d828c9424773478f3d1f2b9805cf5 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 21 Aug 2024 08:06:32 +0200 Subject: [PATCH 070/142] use CIE brightness also for ESP8266 --- wled00/bus_manager.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 2ae624fee..24cb7993d 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -501,9 +501,7 @@ void BusPwm::show() { if (!_valid) return; unsigned numPins = NUM_PWM_PINS(_type); unsigned maxBri = (1<<_depth) - 1; - #ifdef ESP8266 - unsigned pwmBri = (unsigned)(roundf(powf((float)_bri / 255.0f, 1.7f) * (float)maxBri)); // using gamma 1.7 to extrapolate PWM duty cycle - #else // use CIE brightness formula + // use CIE brightness formula unsigned pwmBri = (unsigned)_bri * 100; if(pwmBri < 2040) pwmBri = ((pwmBri << _depth) + 115043) / 230087; //adding '0.5' before division for correct rounding else { @@ -512,8 +510,6 @@ void BusPwm::show() { temp = temp * temp * temp * (1<<_depth) - 1; pwmBri = (unsigned)temp; } - Serial.println(pwmBri); - #endif for (unsigned i = 0; i < numPins; i++) { unsigned scaled = (_data[i] * pwmBri) / 255; if (_reversed) scaled = maxBri - scaled; From 0bbd6b7c4b677249b584f90abbacde6f44f0ea85 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Thu, 22 Aug 2024 17:08:51 +0200 Subject: [PATCH 071/142] Minor optimisation - disable JSON live - WS error string - button irelevant check --- wled00/button.cpp | 2 +- wled00/wled.h | 4 ++-- wled00/ws.cpp | 8 ++++++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/wled00/button.cpp b/wled00/button.cpp index 23d7b8a90..8b366e055 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -308,7 +308,7 @@ void handleButton() buttonLongPressed[b] = true; } - } else if (!isButtonPressed(b) && buttonPressedBefore[b]) { //released + } else if (buttonPressedBefore[b]) { //released long dur = now - buttonPressedTime[b]; // released after rising-edge short press action diff --git a/wled00/wled.h b/wled00/wled.h index b9e675edc..7ef41d961 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -36,7 +36,7 @@ #undef WLED_ENABLE_ADALIGHT // disable has priority over enable #endif //#define WLED_ENABLE_DMX // uses 3.5kb (use LEDPIN other than 2) -#define WLED_ENABLE_JSONLIVE // peek LED output via /json/live (WS binary peek is always enabled) +//#define WLED_ENABLE_JSONLIVE // peek LED output via /json/live (WS binary peek is always enabled) #ifndef WLED_DISABLE_LOXONE #define WLED_ENABLE_LOXONE // uses 1.2kb #endif @@ -331,7 +331,7 @@ typedef class WiFiOptions { struct { uint8_t selectedWiFi : 4; // max 16 SSIDs uint8_t apChannel : 4; - bool apHide : 1; + uint8_t apHide : 3; uint8_t apBehavior : 3; bool noWifiSleep : 1; bool force802_3g : 1; diff --git a/wled00/ws.cpp b/wled00/ws.cpp index d0bac144d..3dec548f4 100644 --- a/wled00/ws.cpp +++ b/wled00/ws.cpp @@ -96,6 +96,8 @@ void wsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventTyp //pong message was received (in response to a ping request maybe) DEBUG_PRINTLN(F("WS pong.")); + } else { + DEBUG_PRINTLN(F("WS unknown event.")); } } @@ -104,10 +106,11 @@ void sendDataWs(AsyncWebSocketClient * client) if (!ws.count()) return; if (!requestJSONBufferLock(12)) { + const char* error = PSTR("{\"error\":3}"); if (client) { - client->text(F("{\"error\":3}")); // ERR_NOBUF + client->text(FPSTR(error)); // ERR_NOBUF } else { - ws.textAll(F("{\"error\":3}")); // ERR_NOBUF + ws.textAll(FPSTR(error)); // ERR_NOBUF } return; } @@ -120,6 +123,7 @@ void sendDataWs(AsyncWebSocketClient * client) size_t len = measureJson(*pDoc); DEBUG_PRINTF_P(PSTR("JSON buffer size: %u for WS request (%u).\n"), pDoc->memoryUsage(), len); + // the following may no longer be necessary as heap management has been fixed by @willmmiles in AWS size_t heap1 = ESP.getFreeHeap(); DEBUG_PRINT(F("heap ")); DEBUG_PRINTLN(ESP.getFreeHeap()); #ifdef ESP8266 From 6f3267aee98095bab3381956fa1949f5c09cb2db Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Thu, 22 Aug 2024 17:15:12 +0200 Subject: [PATCH 072/142] Dynamic bus config - provide LED types from BusManager for settings Credit: @netmindz for the idea. --- wled00/bus_manager.cpp | 157 +++++++++++++++---------- wled00/bus_manager.h | 212 +++++++++++++++++----------------- wled00/cfg.cpp | 7 +- wled00/data/settings_leds.htm | 149 ++++++++++++------------ wled00/set.cpp | 4 +- wled00/xml.cpp | 2 + 6 files changed, 278 insertions(+), 253 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index d0e32b211..aef6b2ee9 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -48,38 +48,25 @@ uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, byte #define W(c) (byte((c) >> 24)) -void ColorOrderMap::add(uint16_t start, uint16_t len, uint8_t colorOrder) { - if (_count >= WLED_MAX_COLOR_ORDER_MAPPINGS) { - return; - } - if (len == 0) { - return; - } - // upper nibble contains W swap information - if ((colorOrder & 0x0F) > COL_ORDER_MAX) { - return; - } - _mappings[_count].start = start; - _mappings[_count].len = len; - _mappings[_count].colorOrder = colorOrder; - _count++; +bool ColorOrderMap::add(uint16_t start, uint16_t len, uint8_t colorOrder) { + if (count() >= WLED_MAX_COLOR_ORDER_MAPPINGS || len == 0 || (colorOrder & 0x0F) > COL_ORDER_MAX) return false; // upper nibble contains W swap information + _mappings.push_back({start,len,colorOrder}); + return true; } uint8_t IRAM_ATTR ColorOrderMap::getPixelColorOrder(uint16_t pix, uint8_t defaultColorOrder) const { - if (_count > 0) { - // upper nibble contains W swap information - // when ColorOrderMap's upper nibble contains value >0 then swap information is used from it, otherwise global swap is used - for (unsigned i = 0; i < _count; i++) { - if (pix >= _mappings[i].start && pix < (_mappings[i].start + _mappings[i].len)) { - return _mappings[i].colorOrder | ((_mappings[i].colorOrder >> 4) ? 0 : (defaultColorOrder & 0xF0)); - } + // upper nibble contains W swap information + // when ColorOrderMap's upper nibble contains value >0 then swap information is used from it, otherwise global swap is used + for (unsigned i = 0; i < count(); i++) { + if (pix >= _mappings[i].start && pix < (_mappings[i].start + _mappings[i].len)) { + return _mappings[i].colorOrder | ((_mappings[i].colorOrder >> 4) ? 0 : (defaultColorOrder & 0xF0)); } } return defaultColorOrder; } -uint32_t Bus::autoWhiteCalc(uint32_t c) { +uint32_t Bus::autoWhiteCalc(uint32_t c) const { unsigned aWM = _autoWhiteMode; if (_gAWM < AW_GLOBAL_DISABLED) aWM = _gAWM; if (aWM == RGBW_MODE_MANUAL_ONLY) return c; @@ -95,7 +82,7 @@ uint32_t Bus::autoWhiteCalc(uint32_t c) { return RGBW32(r, g, b, w); } -uint8_t *Bus::allocData(size_t size) { +uint8_t *Bus::allocateData(size_t size) { if (_data) free(_data); // should not happen, but for safety return _data = (uint8_t *)(size>0 ? calloc(size, sizeof(uint8_t)) : nullptr); } @@ -123,7 +110,7 @@ BusDigital::BusDigital(BusConfig &bc, uint8_t nr, const ColorOrderMap &com) } _iType = PolyBus::getI(bc.type, _pins, nr); if (_iType == I_NONE) return; - if (bc.doubleBuffer && !allocData(bc.count * Bus::getNumberOfChannels(bc.type))) return; + if (bc.doubleBuffer && !allocateData(bc.count * Bus::getNumberOfChannels(bc.type))) return; //_buffering = bc.doubleBuffer; uint16_t lenToCreate = bc.count; if (bc.type == TYPE_WS2812_1CH_X3) lenToCreate = NUM_ICS_WS2812_1CH_3X(bc.count); // only needs a third of "RGB" LEDs for NeoPixelBus @@ -150,7 +137,7 @@ BusDigital::BusDigital(BusConfig &bc, uint8_t nr, const ColorOrderMap &com) //I am NOT to be held liable for burned down garages or houses! // To disable brightness limiter we either set output max current to 0 or single LED current to 0 -uint8_t BusDigital::estimateCurrentAndLimitBri() { +uint8_t BusDigital::estimateCurrentAndLimitBri(void) { bool useWackyWS2815PowerModel = false; byte actualMilliampsPerLed = _milliAmpsPerLed; @@ -202,7 +189,7 @@ uint8_t BusDigital::estimateCurrentAndLimitBri() { return newBri; } -void BusDigital::show() { +void BusDigital::show(void) { _milliAmpsTotal = 0; if (!_valid) return; @@ -263,7 +250,7 @@ void BusDigital::show() { if (newBri < _bri) PolyBus::setBrightness(_busPtr, _iType, _bri); } -bool BusDigital::canShow() { +bool BusDigital::canShow(void) const { if (!_valid) return true; return PolyBus::canShow(_busPtr, _iType); } @@ -319,7 +306,7 @@ void IRAM_ATTR BusDigital::setPixelColor(uint16_t pix, uint32_t c) { } // returns original color if global buffering is enabled, else returns lossly restored color from bus -uint32_t IRAM_ATTR BusDigital::getPixelColor(uint16_t pix) { +uint32_t IRAM_ATTR BusDigital::getPixelColor(uint16_t pix) const { if (!_valid) return 0; if (_data) { size_t offset = pix * getNumberOfChannels(); @@ -349,9 +336,9 @@ uint32_t IRAM_ATTR BusDigital::getPixelColor(uint16_t pix) { } } -uint8_t BusDigital::getPins(uint8_t* pinArray) { +uint8_t BusDigital::getPins(uint8_t* pinArray) const { unsigned numPins = IS_2PIN(_type) ? 2 : 1; - for (unsigned i = 0; i < numPins; i++) pinArray[i] = _pins[i]; + if (pinArray) for (unsigned i = 0; i < numPins; i++) pinArray[i] = _pins[i]; return numPins; } @@ -361,12 +348,12 @@ void BusDigital::setColorOrder(uint8_t colorOrder) { _colorOrder = colorOrder; } -void BusDigital::reinit() { +void BusDigital::reinit(void) { if (!_valid) return; PolyBus::begin(_busPtr, _iType, _pins); } -void BusDigital::cleanup() { +void BusDigital::cleanup(void) { DEBUG_PRINTLN(F("Digital Cleanup.")); PolyBus::cleanup(_busPtr, _iType); _iType = I_NONE; @@ -477,7 +464,7 @@ void BusPwm::setPixelColor(uint16_t pix, uint32_t c) { } //does no index check -uint32_t BusPwm::getPixelColor(uint16_t pix) { +uint32_t BusPwm::getPixelColor(uint16_t pix) const { if (!_valid) return 0; // TODO getting the reverse from CCT is involved (a quick approximation when CCT blending is ste to 0 implemented) switch (_type) { @@ -528,7 +515,7 @@ static const uint16_t cieLUT[256] = { }; #endif -void BusPwm::show() { +void BusPwm::show(void) { if (!_valid) return; unsigned numPins = NUM_PWM_PINS(_type); unsigned maxBri = (1<<_depth) - 1; @@ -548,16 +535,14 @@ void BusPwm::show() { } } -uint8_t BusPwm::getPins(uint8_t* pinArray) { +uint8_t BusPwm::getPins(uint8_t* pinArray) const { if (!_valid) return 0; unsigned numPins = NUM_PWM_PINS(_type); - for (unsigned i = 0; i < numPins; i++) { - pinArray[i] = _pins[i]; - } + if (pinArray) for (unsigned i = 0; i < numPins; i++) pinArray[i] = _pins[i]; return numPins; } -void BusPwm::deallocatePins() { +void BusPwm::deallocatePins(void) { unsigned numPins = NUM_PWM_PINS(_type); for (unsigned i = 0; i < numPins; i++) { pinManager.deallocatePin(_pins[i], PinOwner::BusPwm); @@ -601,19 +586,19 @@ void BusOnOff::setPixelColor(uint16_t pix, uint32_t c) { _data[0] = bool(r|g|b|w) && bool(_bri) ? 0xFF : 0; } -uint32_t BusOnOff::getPixelColor(uint16_t pix) { +uint32_t BusOnOff::getPixelColor(uint16_t pix) const { if (!_valid) return 0; return RGBW32(_data[0], _data[0], _data[0], _data[0]); } -void BusOnOff::show() { +void BusOnOff::show(void) { if (!_valid) return; digitalWrite(_pin, _reversed ? !(bool)_data[0] : (bool)_data[0]); } -uint8_t BusOnOff::getPins(uint8_t* pinArray) { +uint8_t BusOnOff::getPins(uint8_t* pinArray) const { if (!_valid) return 0; - pinArray[0] = _pin; + if (pinArray) pinArray[0] = _pin; return 1; } @@ -642,7 +627,7 @@ BusNetwork::BusNetwork(BusConfig &bc) } _UDPchannels = _rgbw ? 4 : 3; _client = IPAddress(bc.pins[0],bc.pins[1],bc.pins[2],bc.pins[3]); - _valid = (allocData(_len * _UDPchannels) != nullptr); + _valid = (allocateData(_len * _UDPchannels) != nullptr); DEBUG_PRINTF_P(PSTR("%successfully inited virtual strip with type %u and IP %u.%u.%u.%u\n"), _valid?"S":"Uns", bc.type, bc.pins[0], bc.pins[1], bc.pins[2], bc.pins[3]); } @@ -657,27 +642,25 @@ void BusNetwork::setPixelColor(uint16_t pix, uint32_t c) { if (_rgbw) _data[offset+3] = W(c); } -uint32_t BusNetwork::getPixelColor(uint16_t pix) { +uint32_t BusNetwork::getPixelColor(uint16_t pix) const { if (!_valid || pix >= _len) return 0; unsigned offset = pix * _UDPchannels; return RGBW32(_data[offset], _data[offset+1], _data[offset+2], (_rgbw ? _data[offset+3] : 0)); } -void BusNetwork::show() { +void BusNetwork::show(void) { if (!_valid || !canShow()) return; _broadcastLock = true; realtimeBroadcast(_UDPtype, _client, _len, _data, _bri, _rgbw); _broadcastLock = false; } -uint8_t BusNetwork::getPins(uint8_t* pinArray) { - for (unsigned i = 0; i < 4; i++) { - pinArray[i] = _client[i]; - } +uint8_t BusNetwork::getPins(uint8_t* pinArray) const { + if (pinArray) for (unsigned i = 0; i < 4; i++) pinArray[i] = _client[i]; return 4; } -void BusNetwork::cleanup() { +void BusNetwork::cleanup(void) { _type = I_NONE; _valid = false; freeData(); @@ -724,13 +707,67 @@ int BusManager::add(BusConfig &bc) { return numBusses++; } +// idea by @netmindz https://github.com/Aircoookie/WLED/pull/4056 +String BusManager::getLEDTypesJSONString(void) { + struct LEDType { + uint8_t id; + const char *type; + const char *name; + } types[] = { + {TYPE_WS2812_RGB, "D", PSTR("WS281x")}, + {TYPE_SK6812_RGBW, "D", PSTR("SK6812/WS2814 RGBW")}, + {TYPE_TM1814, "D", PSTR("TM1814")}, + {TYPE_WS2811_400KHZ, "D", PSTR("400kHz")}, + {TYPE_TM1829, "D", PSTR("TM1829")}, + {TYPE_UCS8903, "D", PSTR("UCS8903")}, + {TYPE_APA106, "D", PSTR("APA106/PL9823")}, + {TYPE_TM1914, "D", PSTR("TM1914")}, + {TYPE_FW1906, "D", PSTR("FW1906 GRBCW")}, + {TYPE_UCS8904, "D", PSTR("UCS8904 RGBW")}, + {TYPE_WS2805, "D", PSTR("WS2805 RGBCW")}, + {TYPE_SM16825, "D", PSTR("SM16825 RGBCW")}, + {TYPE_WS2812_1CH_X3, "D", PSTR("WS2811 White")}, + //{TYPE_WS2812_2CH_X3, "D", PSTR("WS2811 CCT")}, + //{TYPE_WS2812_WWA, "D", PSTR("WS2811 WWA")}, + {TYPE_WS2801, "2P", PSTR("WS2801")}, + {TYPE_APA102, "2P", PSTR("APA102")}, + {TYPE_LPD8806, "2P", PSTR("LPD8806")}, + {TYPE_LPD6803, "2P", PSTR("LPD6803")}, + {TYPE_P9813, "2P", PSTR("PP9813")}, + {TYPE_ONOFF, "", PSTR("On/Off")}, + {TYPE_ANALOG_1CH, "A", PSTR("PWM White")}, + {TYPE_ANALOG_2CH, "AA", PSTR("PWM CCT")}, + {TYPE_ANALOG_3CH, "AAA", PSTR("PWM RGB")}, + {TYPE_ANALOG_4CH, "AAAA", PSTR("PWM RGBW")}, + {TYPE_ANALOG_5CH, "AAAAA", PSTR("PWM RGB+CCT")}, + //{TYPE_ANALOG_6CH, "AAAAAA", PSTR("PWM RGB+DCCT")}, + {TYPE_NET_DDP_RGB, "V", PSTR("DDP RGB (network)")}, + {TYPE_NET_ARTNET_RGB, "V", PSTR("Art-Net RGB (network)")}, + {TYPE_NET_DDP_RGBW, "V", PSTR("DDP RGBW (network)")}, + {TYPE_NET_ARTNET_RGBW, "V", PSTR("Art-Net RGBW (network)")} + }; + String json = "["; + for (const auto &type : types) { + String id = String(type.id); + json += "{i:" + id + + F(",w:") + String((int)Bus::hasWhite(type.id)) + + F(",c:") + String((int)Bus::hasCCT(type.id)) + + F(",s:") + String((int)Bus::is16bit(type.id)) + + F(",t:\"") + FPSTR(type.type) + + F("\",n:\"") + FPSTR(type.name) + F("\"},"); + } + json.setCharAt(json.length()-1, ']'); // replace last comma with bracket + return json; +} + + void BusManager::useParallelOutput(void) { _parallelOutputs = 8; // hardcoded since we use NPB I2S x8 methods PolyBus::setParallelI2S1Output(); } //do not call this method from system context (network callback) -void BusManager::removeAll() { +void BusManager::removeAll(void) { DEBUG_PRINTLN(F("Removing all.")); //prevents crashes due to deleting busses while in use. while (!canAllShow()) yield(); @@ -744,7 +781,7 @@ void BusManager::removeAll() { // #2478 // If enabled, RMT idle level is set to HIGH when off // to prevent leakage current when using an N-channel MOSFET to toggle LED power -void BusManager::esp32RMTInvertIdle() { +void BusManager::esp32RMTInvertIdle(void) { bool idle_out; unsigned rmt = 0; for (unsigned u = 0; u < numBusses(); u++) { @@ -775,7 +812,7 @@ void BusManager::esp32RMTInvertIdle() { } #endif -void BusManager::on() { +void BusManager::on(void) { #ifdef ESP8266 //Fix for turning off onboard LED breaking bus if (pinManager.getPinOwner(LED_BUILTIN) == PinOwner::BusDigital) { @@ -796,7 +833,7 @@ void BusManager::on() { #endif } -void BusManager::off() { +void BusManager::off(void) { #ifdef ESP8266 // turn off built-in LED if strip is turned off // this will break digital bus so will need to be re-initialised on On @@ -811,7 +848,7 @@ void BusManager::off() { #endif } -void BusManager::show() { +void BusManager::show(void) { _milliAmpsUsed = 0; for (unsigned i = 0; i < numBusses; i++) { busses[i]->show(); @@ -852,13 +889,13 @@ void BusManager::setSegmentCCT(int16_t cct, bool allowWBCorrection) { uint32_t BusManager::getPixelColor(uint16_t pix) { for (unsigned i = 0; i < numBusses; i++) { unsigned bstart = busses[i]->getStart(); - if (pix < bstart || pix >= bstart + busses[i]->getLength()) continue; + if (!busses[i]->containsPixel(pix)) continue; return busses[i]->getPixelColor(pix - bstart); } return 0; } -bool BusManager::canAllShow() { +bool BusManager::canAllShow(void) { for (unsigned i = 0; i < numBusses; i++) { if (!busses[i]->canShow()) return false; } @@ -871,7 +908,7 @@ Bus* BusManager::getBus(uint8_t busNr) { } //semi-duplicate of strip.getLengthTotal() (though that just returns strip._length, calculated in finalizeInit()) -uint16_t BusManager::getTotalLength() { +uint16_t BusManager::getTotalLength(void) { unsigned len = 0; for (unsigned i=0; igetLength(); return len; diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 5e516d2e1..8d23f1127 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -6,6 +6,8 @@ */ #include "const.h" +#include +#include //colors.cpp uint16_t approximateKelvinFromRGB(uint32_t rgb); @@ -73,34 +75,31 @@ struct BusConfig { // Defines an LED Strip and its color ordering. -struct ColorOrderMapEntry { +typedef struct { uint16_t start; uint16_t len; uint8_t colorOrder; -}; +} ColorOrderMapEntry; struct ColorOrderMap { - void add(uint16_t start, uint16_t len, uint8_t colorOrder); + bool add(uint16_t start, uint16_t len, uint8_t colorOrder); - uint8_t count() const { return _count; } + inline uint8_t count() const { return _mappings.size(); } void reset() { - _count = 0; - memset(_mappings, 0, sizeof(_mappings)); + _mappings.clear(); + _mappings.shrink_to_fit(); } const ColorOrderMapEntry* get(uint8_t n) const { - if (n > _count) { - return nullptr; - } + if (n >= count()) return nullptr; return &(_mappings[n]); } uint8_t getPixelColorOrder(uint16_t pix, uint8_t defaultColorOrder) const; private: - uint8_t _count; - ColorOrderMapEntry _mappings[WLED_MAX_COLOR_ORDER_MAPPINGS]; + std::vector _mappings; }; @@ -122,59 +121,61 @@ class Bus { virtual ~Bus() {} //throw the bus under the bus - virtual void show() = 0; - virtual bool canShow() { return true; } - virtual void setStatusPixel(uint32_t c) {} + virtual void show(void) = 0; + virtual bool canShow(void) const { return true; } + virtual void setStatusPixel(uint32_t c) {} virtual void setPixelColor(uint16_t pix, uint32_t c) = 0; - virtual uint32_t getPixelColor(uint16_t pix) { return 0; } - virtual void setBrightness(uint8_t b) { _bri = b; }; - virtual uint8_t getPins(uint8_t* pinArray) { return 0; } - virtual uint16_t getLength() { return isOk() ? _len : 0; } - virtual void setColorOrder(uint8_t co) {} - virtual uint8_t getColorOrder() { return COL_ORDER_RGB; } - virtual uint8_t skippedLeds() { return 0; } - virtual uint16_t getFrequency() { return 0U; } - virtual uint16_t getLEDCurrent() { return 0; } - virtual uint16_t getUsedCurrent() { return 0; } - virtual uint16_t getMaxCurrent() { return 0; } - virtual uint8_t getNumberOfChannels() { return hasWhite(_type) + 3*hasRGB(_type) + hasCCT(_type); } - static inline uint8_t getNumberOfChannels(uint8_t type) { return hasWhite(type) + 3*hasRGB(type) + hasCCT(type); } - inline void setReversed(bool reversed) { _reversed = reversed; } - inline uint16_t getStart() { return _start; } - inline void setStart(uint16_t start) { _start = start; } - inline uint8_t getType() { return _type; } - inline bool isOk() { return _valid; } - inline bool isReversed() { return _reversed; } - inline bool isOffRefreshRequired() { return _needsRefresh; } - bool containsPixel(uint16_t pix) { return pix >= _start && pix < _start+_len; } + virtual void setBrightness(uint8_t b) { _bri = b; }; + inline void setStart(uint16_t start) { _start = start; } + virtual void setColorOrder(uint8_t co) {} + virtual bool hasRGB(void) const { return Bus::hasRGB(_type); } + virtual bool hasWhite(void) const { return Bus::hasWhite(_type); } + virtual bool hasCCT(void) const { return Bus::hasCCT(_type); } + virtual bool is16bit(void) const { return Bus::is16bit(_type); } + virtual uint32_t getPixelColor(uint16_t pix) const { return 0; } + virtual uint8_t getPins(uint8_t* pinArray = nullptr) const { return 0; } + virtual uint16_t getLength(void) const { return isOk() ? _len : 0; } + virtual uint8_t getColorOrder(void) const { return COL_ORDER_RGB; } + virtual uint8_t skippedLeds(void) const { return 0; } + virtual uint16_t getFrequency(void) const { return 0U; } + virtual uint16_t getLEDCurrent(void) const { return 0; } + virtual uint16_t getUsedCurrent(void) const { return 0; } + virtual uint16_t getMaxCurrent(void) const { return 0; } + virtual uint8_t getNumberOfChannels(void) const { return hasWhite(_type) + 3*hasRGB(_type) + hasCCT(_type); } - virtual bool hasRGB(void) { return Bus::hasRGB(_type); } - static bool hasRGB(uint8_t type) { - if ((type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || type == TYPE_ANALOG_1CH || type == TYPE_ANALOG_2CH || type == TYPE_ONOFF) return false; - return true; + inline void setReversed(bool reversed) { _reversed = reversed; } + inline void setAutoWhiteMode(uint8_t m) { if (m < 5) _autoWhiteMode = m; } + inline uint8_t getAutoWhiteMode(void) const { return _autoWhiteMode; } + inline uint16_t getStart(void) const { return _start; } + inline uint8_t getType(void) const { return _type; } + inline bool isOk(void) const { return _valid; } + inline bool isReversed(void) const { return _reversed; } + inline bool isOffRefreshRequired(void) const { return _needsRefresh; } + inline bool containsPixel(uint16_t pix) const { return pix >= _start && pix < _start + _len; } + + static inline uint8_t getNumberOfChannels(uint8_t type) { return hasWhite(type) + 3*hasRGB(type) + hasCCT(type); } + static constexpr bool hasRGB(uint8_t type) { + return !((type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || type == TYPE_ANALOG_1CH || type == TYPE_ANALOG_2CH || type == TYPE_ONOFF); } - virtual bool hasWhite(void) { return Bus::hasWhite(_type); } - static bool hasWhite(uint8_t type) { - if ((type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || - type == TYPE_SK6812_RGBW || type == TYPE_TM1814 || type == TYPE_UCS8904 || - type == TYPE_FW1906 || type == TYPE_WS2805 || type == TYPE_SM16825) return true; // digital types with white channel - if (type > TYPE_ONOFF && type <= TYPE_ANALOG_5CH && type != TYPE_ANALOG_3CH) return true; // analog types with white channel - if (type == TYPE_NET_DDP_RGBW || type == TYPE_NET_ARTNET_RGBW) return true; // network types with white channel - return false; + static constexpr bool hasWhite(uint8_t type) { + return (type >= TYPE_WS2812_1CH && type <= TYPE_WS2812_WWA) || + type == TYPE_SK6812_RGBW || type == TYPE_TM1814 || type == TYPE_UCS8904 || + type == TYPE_FW1906 || type == TYPE_WS2805 || type == TYPE_SM16825 || // digital types with white channel + (type > TYPE_ONOFF && type <= TYPE_ANALOG_5CH && type != TYPE_ANALOG_3CH) || // analog types with white channel + type == TYPE_NET_DDP_RGBW || type == TYPE_NET_ARTNET_RGBW; // network types with white channel } - virtual bool hasCCT(void) { return Bus::hasCCT(_type); } - static bool hasCCT(uint8_t type) { - if (type == TYPE_WS2812_2CH_X3 || type == TYPE_WS2812_WWA || - type == TYPE_ANALOG_2CH || type == TYPE_ANALOG_5CH || - type == TYPE_FW1906 || type == TYPE_WS2805 || - type == TYPE_SM16825) return true; - return false; + static constexpr bool hasCCT(uint8_t type) { + return type == TYPE_WS2812_2CH_X3 || type == TYPE_WS2812_WWA || + type == TYPE_ANALOG_2CH || type == TYPE_ANALOG_5CH || + type == TYPE_FW1906 || type == TYPE_WS2805 || + type == TYPE_SM16825; } - static inline int16_t getCCT() { return _cct; } - static void setCCT(int16_t cct) { - _cct = cct; - } - static inline uint8_t getCCTBlend() { return _cctBlend; } + static constexpr bool is16bit(uint8_t type) { return type == TYPE_UCS8903 || type == TYPE_UCS8904 || type == TYPE_SM16825; } + static inline int16_t getCCT(void) { return _cct; } + static inline void setGlobalAWMode(uint8_t m) { if (m < 5) _gAWM = m; else _gAWM = AW_GLOBAL_DISABLED; } + static inline uint8_t getGlobalAWMode(void) { return _gAWM; } + static void setCCT(int16_t cct) { _cct = cct; } + static inline uint8_t getCCTBlend(void) { return _cctBlend; } static void setCCTBlend(uint8_t b) { if (b > 100) b = 100; _cctBlend = (b * 127) / 100; @@ -203,10 +204,6 @@ class Bus { ww = (w * ww) / 255; //brightness scaling cw = (w * cw) / 255; } - inline void setAutoWhiteMode(uint8_t m) { if (m < 5) _autoWhiteMode = m; } - inline uint8_t getAutoWhiteMode() { return _autoWhiteMode; } - inline static void setGlobalAWMode(uint8_t m) { if (m < 5) _gAWM = m; else _gAWM = AW_GLOBAL_DISABLED; } - inline static uint8_t getGlobalAWMode() { return _gAWM; } protected: uint8_t _type; @@ -231,8 +228,8 @@ class Bus { // 127 - additive CCT blending (CCT 127 => 100% warm, 100% cold) static uint8_t _cctBlend; - uint32_t autoWhiteCalc(uint32_t c); - uint8_t *allocData(size_t size = 1); + uint32_t autoWhiteCalc(uint32_t c) const; + uint8_t *allocateData(size_t size = 1); void freeData() { if (_data != nullptr) free(_data); _data = nullptr; } }; @@ -242,23 +239,22 @@ class BusDigital : public Bus { BusDigital(BusConfig &bc, uint8_t nr, const ColorOrderMap &com); ~BusDigital() { cleanup(); } - void show() override; - bool canShow() override; + void show(void) override; + bool canShow(void) const override; void setBrightness(uint8_t b) override; void setStatusPixel(uint32_t c) override; void setPixelColor(uint16_t pix, uint32_t c) override; void setColorOrder(uint8_t colorOrder) override; - uint32_t getPixelColor(uint16_t pix) override; - uint8_t getColorOrder() override { return _colorOrder; } - uint8_t getPins(uint8_t* pinArray) override; - uint8_t skippedLeds() override { return _skip; } - uint16_t getFrequency() override { return _frequencykHz; } - uint8_t estimateCurrentAndLimitBri(); - uint16_t getLEDCurrent() override { return _milliAmpsPerLed; } - uint16_t getUsedCurrent() override { return _milliAmpsTotal; } - uint16_t getMaxCurrent() override { return _milliAmpsMax; } - void reinit(); - void cleanup(); + uint32_t getPixelColor(uint16_t pix) const override; + uint8_t getColorOrder(void) const override { return _colorOrder; } + uint8_t getPins(uint8_t* pinArray) const override; + uint8_t skippedLeds(void) const override { return _skip; } + uint16_t getFrequency(void) const override { return _frequencykHz; } + uint16_t getLEDCurrent(void) const override { return _milliAmpsPerLed; } + uint16_t getUsedCurrent(void) const override { return _milliAmpsTotal; } + uint16_t getMaxCurrent(void) const override { return _milliAmpsMax; } + void reinit(void); + void cleanup(void); private: uint8_t _skip; @@ -273,7 +269,7 @@ class BusDigital : public Bus { static uint16_t _milliAmpsTotal; // is overwitten/recalculated on each show() - inline uint32_t restoreColorLossy(uint32_t c, uint8_t restoreBri) { + inline uint32_t restoreColorLossy(uint32_t c, uint8_t restoreBri) const { if (restoreBri < 255) { uint8_t* chan = (uint8_t*) &c; for (uint_fast8_t i=0; i<4; i++) { @@ -283,6 +279,8 @@ class BusDigital : public Bus { } return c; } + + uint8_t estimateCurrentAndLimitBri(void); }; @@ -292,11 +290,11 @@ class BusPwm : public Bus { ~BusPwm() { cleanup(); } void setPixelColor(uint16_t pix, uint32_t c) override; - uint32_t getPixelColor(uint16_t pix) override; //does no index check - uint8_t getPins(uint8_t* pinArray) override; - uint16_t getFrequency() override { return _frequency; } - void show() override; - void cleanup() { deallocatePins(); } + uint32_t getPixelColor(uint16_t pix) const override; //does no index check + uint8_t getPins(uint8_t* pinArray) const override; + uint16_t getFrequency(void) const override { return _frequency; } + void show(void) override; + void cleanup(void) { deallocatePins(); } private: uint8_t _pins[5]; @@ -307,7 +305,7 @@ class BusPwm : public Bus { uint8_t _depth; uint16_t _frequency; - void deallocatePins(); + void deallocatePins(void); }; @@ -317,10 +315,10 @@ class BusOnOff : public Bus { ~BusOnOff() { cleanup(); } void setPixelColor(uint16_t pix, uint32_t c) override; - uint32_t getPixelColor(uint16_t pix) override; - uint8_t getPins(uint8_t* pinArray) override; - void show() override; - void cleanup() { pinManager.deallocatePin(_pin, PinOwner::BusOnOff); } + uint32_t getPixelColor(uint16_t pix) const override; + uint8_t getPins(uint8_t* pinArray) const override; + void show(void) override; + void cleanup(void) { pinManager.deallocatePin(_pin, PinOwner::BusOnOff); } private: uint8_t _pin; @@ -333,14 +331,14 @@ class BusNetwork : public Bus { BusNetwork(BusConfig &bc); ~BusNetwork() { cleanup(); } - bool hasRGB() override { return true; } - bool hasWhite() override { return _rgbw; } - bool canShow() override { return !_broadcastLock; } // this should be a return value from UDP routine if it is still sending data out + bool hasRGB(void) const override { return true; } + bool hasWhite(void) const override { return _rgbw; } + bool canShow(void) const override { return !_broadcastLock; } // this should be a return value from UDP routine if it is still sending data out void setPixelColor(uint16_t pix, uint32_t c) override; - uint32_t getPixelColor(uint16_t pix) override; - uint8_t getPins(uint8_t* pinArray) override; - void show() override; - void cleanup(); + uint32_t getPixelColor(uint16_t pix) const override; + uint8_t getPins(uint8_t* pinArray) const override; + void show(void) override; + void cleanup(void); private: IPAddress _client; @@ -365,31 +363,31 @@ class BusManager { static void useParallelOutput(void); // workaround for inaccessible PolyBus //do not call this method from system context (network callback) - static void removeAll(); + static void removeAll(void); static void on(void); static void off(void); - static void show(); - static bool canAllShow(); + static void show(void); + static bool canAllShow(void); static void setStatusPixel(uint32_t c); static void setPixelColor(uint16_t pix, uint32_t c); static void setBrightness(uint8_t b); // for setSegmentCCT(), cct can only be in [-1,255] range; allowWBCorrection will convert it to K // WARNING: setSegmentCCT() is a misleading name!!! much better would be setGlobalCCT() or just setCCT() static void setSegmentCCT(int16_t cct, bool allowWBCorrection = false); - static void setMilliampsMax(uint16_t max) { _milliAmpsMax = max;} + static inline void setMilliampsMax(uint16_t max) { _milliAmpsMax = max;} static uint32_t getPixelColor(uint16_t pix); - static inline int16_t getSegmentCCT() { return Bus::getCCT(); } + static inline int16_t getSegmentCCT(void) { return Bus::getCCT(); } static Bus* getBus(uint8_t busNr); //semi-duplicate of strip.getLengthTotal() (though that just returns strip._length, calculated in finalizeInit()) - static uint16_t getTotalLength(); - static uint8_t getNumBusses() { return numBusses; } + static uint16_t getTotalLength(void); + static inline uint8_t getNumBusses(void) { return numBusses; } + static String getLEDTypesJSONString(void); - static void updateColorOrderMap(const ColorOrderMap &com) { memcpy(&colorOrderMap, &com, sizeof(ColorOrderMap)); } - static const ColorOrderMap& getColorOrderMap() { return colorOrderMap; } + static inline ColorOrderMap& getColorOrderMap(void) { return colorOrderMap; } private: static uint8_t numBusses; @@ -400,9 +398,9 @@ class BusManager { static uint8_t _parallelOutputs; #ifdef ESP32_DATA_IDLE_HIGH - static void esp32RMTInvertIdle(); + static void esp32RMTInvertIdle(void); #endif - static uint8_t getNumVirtualBusses() { + static uint8_t getNumVirtualBusses(void) { int j = 0; for (int i=0; igetType() >= TYPE_NET_DDP_RGB && busses[i]->getType() < 96) j++; return j; diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 89076efab..76ff4d20e 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -244,17 +244,12 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { // read color order map configuration JsonArray hw_com = hw[F("com")]; if (!hw_com.isNull()) { - ColorOrderMap com = {}; - unsigned s = 0; for (JsonObject entry : hw_com) { - if (s > WLED_MAX_COLOR_ORDER_MAPPINGS) break; uint16_t start = entry["start"] | 0; uint16_t len = entry["len"] | 0; uint8_t colorOrder = (int)entry[F("order")]; - com.add(start, len, colorOrder); - s++; + if (!BusManager::getColorOrderMap().add(start, len, colorOrder)) break; } - BusManager::updateColorOrderMap(com); } // read multiple button configuration diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index b7d2d18a7..54c16b9d9 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -7,6 +7,7 @@ ', options).replace(/<[\/]*script>/g, ''); + let js = await minifyHtml('', options); + return js.replace(/<[\/]*script>/g, ''); } else if (type == "html-minify") { return await minifyHtml(str, options); } @@ -252,6 +253,12 @@ writeChunks( str .replace("%%", "%") }, + { + file: "common.js", + name: "JS_common", + method: "gzip", + filter: "js-minify", + }, { file: "settings.htm", name: "PAGE_settings", diff --git a/wled00/data/common.js b/wled00/data/common.js new file mode 100644 index 000000000..9378ef07a --- /dev/null +++ b/wled00/data/common.js @@ -0,0 +1,118 @@ +var d=document; +var loc = false, locip, locproto = "http:"; + +function H(pg="") { window.open("https://kno.wled.ge/"+pg); } +function GH() { window.open("https://github.com/Aircoookie/WLED"); } +function gId(c) { return d.getElementById(c); } // getElementById +function cE(e) { return d.createElement(e); } // createElement +function gEBCN(c) { return d.getElementsByClassName(c); } // getElementsByClassName +function gN(s) { return d.getElementsByName(s)[0]; } // getElementsByName +function isE(o) { return Object.keys(o).length === 0; } // isEmpty +function isO(i) { return (i && typeof i === 'object' && !Array.isArray(i)); } // isObject +function isN(n) { return !isNaN(parseFloat(n)) && isFinite(n); } // isNumber +// https://stackoverflow.com/questions/3885817/how-do-i-check-that-a-number-is-float-or-integer +function isF(n) { return n === +n && n !== (n|0); } // isFloat +function isI(n) { return n === +n && n === (n|0); } // isInteger +function toggle(el) { gId(el).classList.toggle("hide"); gId('No'+el).classList.toggle("hide"); } +function tooltip(cont=null) { + d.querySelectorAll((cont?cont+" ":"")+"[title]").forEach((element)=>{ + element.addEventListener("mouseover", ()=>{ + // save title + element.setAttribute("data-title", element.getAttribute("title")); + const tooltip = d.createElement("span"); + tooltip.className = "tooltip"; + tooltip.textContent = element.getAttribute("title"); + + // prevent default title popup + element.removeAttribute("title"); + + let { top, left, width } = element.getBoundingClientRect(); + + d.body.appendChild(tooltip); + + const { offsetHeight, offsetWidth } = tooltip; + + const offset = element.classList.contains("sliderwrap") ? 4 : 10; + top -= offsetHeight + offset; + left += (width - offsetWidth) / 2; + + tooltip.style.top = top + "px"; + tooltip.style.left = left + "px"; + tooltip.classList.add("visible"); + }); + + element.addEventListener("mouseout", ()=>{ + d.querySelectorAll('.tooltip').forEach((tooltip)=>{ + tooltip.classList.remove("visible"); + d.body.removeChild(tooltip); + }); + // restore title + element.setAttribute("title", element.getAttribute("data-title")); + }); + }); +}; +// https://www.educative.io/edpresso/how-to-dynamically-load-a-js-file-in-javascript +function loadJS(FILE_URL, async = true, preGetV = undefined, postGetV = undefined) { + let scE = d.createElement("script"); + scE.setAttribute("src", FILE_URL); + scE.setAttribute("type", "text/javascript"); + scE.setAttribute("async", async); + d.body.appendChild(scE); + // success event + scE.addEventListener("load", () => { + //console.log("File loaded"); + if (preGetV) preGetV(); + GetV(); + if (postGetV) postGetV(); + }); + // error event + scE.addEventListener("error", (ev) => { + console.log("Error on loading file", ev); + alert("Loading of configuration script failed.\nIncomplete page data!"); + }); +} +function getLoc() { + let l = window.location; + if (l.protocol == "file:") { + loc = true; + locip = localStorage.getItem('locIp'); + if (!locip) { + locip = prompt("File Mode. Please enter WLED IP!"); + localStorage.setItem('locIp', locip); + } + } else { + // detect reverse proxy + let path = l.pathname; + let paths = path.slice(1,path.endsWith('/')?-1:undefined).split("/"); + if (paths.length > 1) paths.pop(); // remove subpage (or "settings") + if (paths.length > 0 && paths[paths.length-1]=="settings") paths.pop(); // remove "settings" + if (paths.length > 1) { + locproto = l.protocol; + loc = true; + locip = l.hostname + (l.port ? ":" + l.port : "") + "/" + paths.join('/'); + } + } +} +function getURL(path) { return (loc ? locproto + "//" + locip : "") + path; } +function B() { window.open(getURL("/settings"),"_self"); } +var timeout; +function showToast(text, error = false) { + var x = gId("toast"); + if (!x) return; + x.innerHTML = text; + x.className = error ? "error":"show"; + clearTimeout(timeout); + x.style.animation = 'none'; + timeout = setTimeout(function(){ x.className = x.className.replace("show", ""); }, 2900); +} +function uploadFile(fileObj, name) { + var req = new XMLHttpRequest(); + req.addEventListener('load', function(){showToast(this.responseText,this.status >= 400)}); + req.addEventListener('error', function(e){showToast(e.stack,true);}); + req.open("POST", "/upload"); + var formData = new FormData(); + formData.append("data", fileObj.files[0], name); + req.send(formData); + fileObj.value = ''; + return false; +} diff --git a/wled00/data/cpal/cpal.htm b/wled00/data/cpal/cpal.htm index a4b913592..b58c0987a 100644 --- a/wled00/data/cpal/cpal.htm +++ b/wled00/data/cpal/cpal.htm @@ -608,8 +608,8 @@ } function generatePaletteDivs() { - const palettesDiv = d.getElementById("palettes"); - const staticPalettesDiv = d.getElementById("staticPalettes"); + const palettesDiv = gId("palettes"); + const staticPalettesDiv = gId("staticPalettes"); const paletteDivs = Array.from(palettesDiv.children).filter((child) => { return child.id.match(/^palette\d$/); // match only elements with id starting with "palette" followed by a single digit }); @@ -620,25 +620,25 @@ for (let i = 0; i < paletteArray.length; i++) { const palette = paletteArray[i]; - const paletteDiv = d.createElement("div"); + const paletteDiv = cE("div"); paletteDiv.id = `palette${i}`; paletteDiv.classList.add("palette"); const thisKey = Object.keys(palette)[0]; paletteDiv.dataset.colarray = JSON.stringify(palette[thisKey]); - const gradientDiv = d.createElement("div"); + const gradientDiv = cE("div"); gradientDiv.id = `paletteGradient${i}` - const buttonsDiv = d.createElement("div"); + const buttonsDiv = cE("div"); buttonsDiv.id = `buttonsDiv${i}`; buttonsDiv.classList.add("buttonsDiv") - const sendSpan = d.createElement("span"); + const sendSpan = cE("span"); sendSpan.id = `sendSpan${i}`; sendSpan.onclick = function() {initiateUpload(i)}; sendSpan.setAttribute('title', `Send current editor to slot ${i}`); // perhaps Save instead of Send? sendSpan.innerHTML = svgSave; sendSpan.classList.add("sendSpan") - const editSpan = d.createElement("span"); + const editSpan = cE("span"); editSpan.id = `editSpan${i}`; editSpan.onclick = function() {loadForEdit(i)}; editSpan.setAttribute('title', `Copy slot ${i} palette to editor`); diff --git a/wled00/data/pxmagic/pxmagic.htm b/wled00/data/pxmagic/pxmagic.htm index d59f924cf..8ec11f454 100644 --- a/wled00/data/pxmagic/pxmagic.htm +++ b/wled00/data/pxmagic/pxmagic.htm @@ -882,10 +882,8 @@ hostnameLabel(); })(); - function gId(id) { - return d.getElementById(id); - } - + function gId(e) {return d.getElementById(e);} + function cE(e) {return d.createElement(e);} function hostnameLabel() { const link = gId("wledEdit"); link.href = WLED_URL + "/edit"; @@ -1675,7 +1673,7 @@ } function createCanvas(width, height) { - const canvas = d.createElement("canvas"); + const canvas = cE("canvas"); canvas.width = width; canvas.height = height; @@ -1719,7 +1717,7 @@ const blob = new Blob([text], { type: mimeType }); const url = URL.createObjectURL(blob); - const anchorElement = d.createElement("a"); + const anchorElement = cE("a"); anchorElement.href = url; anchorElement.download = `${filename}.${fileExtension}`; @@ -1790,7 +1788,7 @@ hideElement = "preview" ) { const hide = gId(hideElement); - const toast = d.createElement("div"); + const toast = cE("div"); const wait = 100; toast.style.animation = "fadeIn"; @@ -1799,14 +1797,14 @@ toast.classList.add("toast", type); - const body = d.createElement("span"); + const body = cE("span"); body.classList.add("toast-body"); body.textContent = message; toast.appendChild(body); - const progress = d.createElement("div"); + const progress = cE("div"); progress.classList.add("toast-progress"); progress.style.animation = "progress"; @@ -1831,7 +1829,7 @@ function carousel(id, images, delay = 3000) { let index = 0; - const carousel = d.createElement("div"); + const carousel = cE("div"); carousel.classList.add("carousel"); images.forEach((canvas, i) => { @@ -1959,7 +1957,7 @@ let errorElement = parent.querySelector(".error-message"); if (!errorElement) { - errorElement = d.createElement("div"); + errorElement = cE("div"); errorElement.classList.add("error-message"); parent.appendChild(errorElement); } diff --git a/wled00/data/settings.htm b/wled00/data/settings.htm index 52b64006b..82c778214 100644 --- a/wled00/data/settings.htm +++ b/wled00/data/settings.htm @@ -4,53 +4,12 @@ WLED Settings +
-
+

Imma firin ma lazer (if it has DMX support)

diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 206d4a8c7..54ba9d8ba 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -4,20 +4,12 @@ LED Settings +
-
+

LED & Hardware setup

@@ -861,7 +800,7 @@ Swap:  ✕
Apply IR change to main segment only:
- + IR info

Relay GPIO:  ✕
diff --git a/wled00/data/settings_sec.htm b/wled00/data/settings_sec.htm index ff8231ccb..ce9bd8aa3 100644 --- a/wled00/data/settings_sec.htm +++ b/wled00/data/settings_sec.htm @@ -4,55 +4,9 @@ Misc Settings +
-
+


diff --git a/wled00/data/settings_um.htm b/wled00/data/settings_um.htm index e2fbd5eb7..c2f0ffbf2 100644 --- a/wled00/data/settings_um.htm +++ b/wled00/data/settings_um.htm @@ -4,75 +4,55 @@ Usermod Settings +
-
+

WiFi setup

diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 291f6f5fc..9d4e4c85b 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -18,6 +18,7 @@ static const char s_unlock_ota [] PROGMEM = "Please unlock OTA in security setti static const char s_unlock_cfg [] PROGMEM = "Please unlock settings using PIN code!"; static const char s_notimplemented[] PROGMEM = "Not implemented"; static const char s_accessdenied[] PROGMEM = "Access Denied"; +static const char _common_js[] PROGMEM = "/common.js"; //Is this an IP? static bool isIp(String str) { @@ -237,6 +238,10 @@ void initServer() handleStaticContent(request, "", 200, FPSTR(CONTENT_TYPE_HTML), PAGE_liveview, PAGE_liveview_length); }); + server.on(_common_js, HTTP_GET, [](AsyncWebServerRequest *request) { + handleStaticContent(request, FPSTR(_common_js), 200, FPSTR(CONTENT_TYPE_JAVASCRIPT), JS_common, JS_common_length); + }); + //settings page server.on(F("/settings"), HTTP_GET, [](AsyncWebServerRequest *request){ serveSettings(request); @@ -511,6 +516,10 @@ void serveJsonError(AsyncWebServerRequest* request, uint16_t code, uint16_t erro void serveSettingsJS(AsyncWebServerRequest* request) { + if (request->url().indexOf(FPSTR(_common_js)) > 0) { + handleStaticContent(request, FPSTR(_common_js), 200, FPSTR(CONTENT_TYPE_JAVASCRIPT), JS_common, JS_common_length); + return; + } char buf[SETTINGS_STACK_BUF_SIZE+37]; buf[0] = 0; byte subPage = request->arg(F("p")).toInt(); From 88fb8605681a6417e27b7a09a40ae846c8f4d909 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Tue, 17 Sep 2024 16:34:38 +0200 Subject: [PATCH 138/142] SAVE_RAM bugfix introduced by #4137 --- wled00/wled.h | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/wled00/wled.h b/wled00/wled.h index 33dea8b03..31a612858 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -641,17 +641,19 @@ typedef class Receive { bool SegmentOptions : 1; bool SegmentBounds : 1; bool Direct : 1; - uint8_t reserved : 2; + bool Palette : 1; + uint8_t reserved : 1; }; }; Receive(int i) { Options = i; } - Receive(bool b, bool c, bool e, bool sO, bool sB) { - Brightness = b; - Color = c; - Effects = e; - SegmentOptions = sO; - SegmentBounds = sB; - }; + Receive(bool b, bool c, bool e, bool sO, bool sB, bool p) + : Brightness(b) + , Color(c) + , Effects(e) + , SegmentOptions(sO) + , SegmentBounds(sB) + , Palette(p) + {}; } __attribute__ ((aligned(1), packed)) receive_notification_t; typedef class Send { public: @@ -673,7 +675,7 @@ typedef class Send { Hue = h; } } __attribute__ ((aligned(1), packed)) send_notification_t; -WLED_GLOBAL receive_notification_t receiveN _INIT(0b00100111); +WLED_GLOBAL receive_notification_t receiveN _INIT(0b01100111); WLED_GLOBAL send_notification_t notifyG _INIT(0b00001111); #define receiveNotificationBrightness receiveN.Brightness #define receiveNotificationColor receiveN.Color From 72455ccde1f35c390e86a1038d2c2fd63d5e86fb Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Tue, 17 Sep 2024 19:47:24 +0200 Subject: [PATCH 139/142] Missing "not" --- wled00/data/settings_sync.htm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/data/settings_sync.htm b/wled00/data/settings_sync.htm index bf5ce3979..34b9fc6cd 100644 --- a/wled00/data/settings_sync.htm +++ b/wled00/data/settings_sync.htm @@ -206,7 +206,7 @@ Hue status: Disabled in this build

Serial

- This firmware build does support Serial interface.
+ This firmware build does not support Serial interface.
Baud rate: From b50e6e0d90ce47adcc4e7ecb865b671c2c9dec1f Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Thu, 19 Sep 2024 21:44:11 +0200 Subject: [PATCH 140/142] Static PinManager & UsermodManager - saves a few bytes of flash --- .../Animated_Staircase/Animated_Staircase.h | 10 +- usermods/Animated_Staircase/README.md | 2 +- usermods/Battery/usermod_v2_Battery.h | 4 +- usermods/EXAMPLE_v2/usermod_v2_example.h | 2 +- .../Fix_unreachable_netservices_v2/readme.md | 8 +- .../usermod_LDR_Dusk_Dawn_v2.h | 6 +- usermods/PIR_sensor_switch/readme.md | 2 +- .../usermod_PIR_sensor_switch.h | 4 +- usermods/PWM_fan/usermod_PWM_fan.h | 16 +- usermods/SN_Photoresistor/usermods_list.cpp | 2 +- usermods/ST7789_display/ST7789_display.h | 6 +- usermods/Temperature/usermod_temperature.h | 10 +- usermods/audioreactive/audio_reactive.old.h | 2071 +++++++++++++++++ usermods/audioreactive/audio_source.h | 20 +- usermods/mpu6050_imu/readme.md | 2 +- usermods/mpu6050_imu/usermod_gyro_surge.h | 2 +- usermods/mpu6050_imu/usermod_mpu6050_imu.h | 4 +- usermods/mqtt_switch_v2/README.md | 2 +- usermods/multi_relay/readme.md | 8 +- usermods/multi_relay/usermod_multi_relay.h | 4 +- usermods/pixels_dice_tray/pixels_dice_tray.h | 6 +- usermods/pwm_outputs/usermod_pwm_outputs.h | 10 +- usermods/quinled-an-penta/quinled-an-penta.h | 18 +- .../rgb-rotary-encoder/rgb-rotary-encoder.h | 10 +- usermods/sd_card/usermod_sd_card.h | 10 +- .../usermod_seven_segment_reloaded.h | 2 +- .../usermod_v2_auto_save.h | 2 +- .../usermod_v2_four_line_display_ALT.h | 10 +- .../usermod_v2_rotary_encoder_ui_ALT.h | 14 +- wled00/FX.cpp | 8 +- wled00/FX_fcn.cpp | 8 +- wled00/bus_manager.cpp | 26 +- wled00/bus_manager.h | 2 +- wled00/button.cpp | 2 +- wled00/cfg.cpp | 28 +- wled00/fcn_declare.h | 44 +- wled00/json.cpp | 6 +- wled00/led.cpp | 2 +- wled00/mqtt.cpp | 6 +- wled00/overlay.cpp | 2 +- wled00/pin_manager.cpp | 109 +- wled00/pin_manager.h | 87 +- wled00/set.cpp | 34 +- wled00/udp.cpp | 2 +- wled00/um_manager.cpp | 3 + wled00/usermods_list.cpp | 112 +- wled00/wled.cpp | 22 +- wled00/wled_server.cpp | 4 +- wled00/xml.cpp | 12 +- 49 files changed, 2401 insertions(+), 385 deletions(-) create mode 100644 usermods/audioreactive/audio_reactive.old.h diff --git a/usermods/Animated_Staircase/Animated_Staircase.h b/usermods/Animated_Staircase/Animated_Staircase.h index 8953756d3..d1ec9bb7f 100644 --- a/usermods/Animated_Staircase/Animated_Staircase.h +++ b/usermods/Animated_Staircase/Animated_Staircase.h @@ -332,7 +332,7 @@ class Animated_Staircase : public Usermod { }; // NOTE: this *WILL* return TRUE if all the pins are set to -1. // this is *BY DESIGN*. - if (!pinManager.allocateMultiplePins(pins, 4, PinOwner::UM_AnimatedStaircase)) { + if (!PinManager::allocateMultiplePins(pins, 4, PinOwner::UM_AnimatedStaircase)) { topPIRorTriggerPin = -1; topEchoPin = -1; bottomPIRorTriggerPin = -1; @@ -513,10 +513,10 @@ class Animated_Staircase : public Usermod { (oldBottomAPin != bottomPIRorTriggerPin) || (oldBottomBPin != bottomEchoPin)) { changed = true; - pinManager.deallocatePin(oldTopAPin, PinOwner::UM_AnimatedStaircase); - pinManager.deallocatePin(oldTopBPin, PinOwner::UM_AnimatedStaircase); - pinManager.deallocatePin(oldBottomAPin, PinOwner::UM_AnimatedStaircase); - pinManager.deallocatePin(oldBottomBPin, PinOwner::UM_AnimatedStaircase); + PinManager::deallocatePin(oldTopAPin, PinOwner::UM_AnimatedStaircase); + PinManager::deallocatePin(oldTopBPin, PinOwner::UM_AnimatedStaircase); + PinManager::deallocatePin(oldBottomAPin, PinOwner::UM_AnimatedStaircase); + PinManager::deallocatePin(oldBottomBPin, PinOwner::UM_AnimatedStaircase); } if (changed) setup(); } diff --git a/usermods/Animated_Staircase/README.md b/usermods/Animated_Staircase/README.md index 320b744a5..2ad66b5ae 100644 --- a/usermods/Animated_Staircase/README.md +++ b/usermods/Animated_Staircase/README.md @@ -18,7 +18,7 @@ Before compiling, you have to make the following modifications: Edit `usermods_list.cpp`: 1. Open `wled00/usermods_list.cpp` 2. add `#include "../usermods/Animated_Staircase/Animated_Staircase.h"` to the top of the file -3. add `usermods.add(new Animated_Staircase());` to the end of the `void registerUsermods()` function. +3. add `UsermodManager::add(new Animated_Staircase());` to the end of the `void registerUsermods()` function. You can configure usermod using the Usermods settings page. Please enter GPIO pins for PIR or ultrasonic sensors (trigger and echo). diff --git a/usermods/Battery/usermod_v2_Battery.h b/usermods/Battery/usermod_v2_Battery.h index 136d3a71a..e91de850c 100644 --- a/usermods/Battery/usermod_v2_Battery.h +++ b/usermods/Battery/usermod_v2_Battery.h @@ -200,7 +200,7 @@ class UsermodBattery : public Usermod bool success = false; DEBUG_PRINTLN(F("Allocating battery pin...")); if (batteryPin >= 0 && digitalPinToAnalogChannel(batteryPin) >= 0) - if (pinManager.allocatePin(batteryPin, false, PinOwner::UM_Battery)) { + if (PinManager::allocatePin(batteryPin, false, PinOwner::UM_Battery)) { DEBUG_PRINTLN(F("Battery pin allocation succeeded.")); success = true; } @@ -561,7 +561,7 @@ class UsermodBattery : public Usermod if (newBatteryPin != batteryPin) { // deallocate pin - pinManager.deallocatePin(batteryPin, PinOwner::UM_Battery); + PinManager::deallocatePin(batteryPin, PinOwner::UM_Battery); batteryPin = newBatteryPin; // initialise setup(); diff --git a/usermods/EXAMPLE_v2/usermod_v2_example.h b/usermods/EXAMPLE_v2/usermod_v2_example.h index 32374fde2..3d562b585 100644 --- a/usermods/EXAMPLE_v2/usermod_v2_example.h +++ b/usermods/EXAMPLE_v2/usermod_v2_example.h @@ -71,7 +71,7 @@ class MyExampleUsermod : public Usermod { // #endif // in setup() // #ifdef USERMOD_EXAMPLE - // UM = (MyExampleUsermod*) usermods.lookup(USERMOD_ID_EXAMPLE); + // UM = (MyExampleUsermod*) UsermodManager::lookup(USERMOD_ID_EXAMPLE); // #endif // somewhere in loop() or other member method // #ifdef USERMOD_EXAMPLE diff --git a/usermods/Fix_unreachable_netservices_v2/readme.md b/usermods/Fix_unreachable_netservices_v2/readme.md index 006eaf9f9..07d64bc67 100644 --- a/usermods/Fix_unreachable_netservices_v2/readme.md +++ b/usermods/Fix_unreachable_netservices_v2/readme.md @@ -59,10 +59,10 @@ void registerUsermods() * || || || * \/ \/ \/ */ - //usermods.add(new MyExampleUsermod()); - //usermods.add(new UsermodTemperature()); - //usermods.add(new UsermodRenameMe()); - usermods.add(new FixUnreachableNetServices()); + //UsermodManager::add(new MyExampleUsermod()); + //UsermodManager::add(new UsermodTemperature()); + //UsermodManager::add(new UsermodRenameMe()); + UsermodManager::add(new FixUnreachableNetServices()); } ``` diff --git a/usermods/LDR_Dusk_Dawn_v2/usermod_LDR_Dusk_Dawn_v2.h b/usermods/LDR_Dusk_Dawn_v2/usermod_LDR_Dusk_Dawn_v2.h index 393fc2232..03f4c078a 100644 --- a/usermods/LDR_Dusk_Dawn_v2/usermod_LDR_Dusk_Dawn_v2.h +++ b/usermods/LDR_Dusk_Dawn_v2/usermod_LDR_Dusk_Dawn_v2.h @@ -30,7 +30,7 @@ class LDR_Dusk_Dawn_v2 : public Usermod { void setup() { // register ldrPin if ((ldrPin >= 0) && (digitalPinToAnalogChannel(ldrPin) >= 0)) { - if(!pinManager.allocatePin(ldrPin, false, PinOwner::UM_LDR_DUSK_DAWN)) ldrEnabled = false; // pin already in use -> disable usermod + if(!PinManager::allocatePin(ldrPin, false, PinOwner::UM_LDR_DUSK_DAWN)) ldrEnabled = false; // pin already in use -> disable usermod else pinMode(ldrPin, INPUT); // alloc success -> configure pin for input } else ldrEnabled = false; // invalid pin -> disable usermod initDone = true; @@ -110,7 +110,7 @@ class LDR_Dusk_Dawn_v2 : public Usermod { if (initDone && (ldrPin != oldLdrPin)) { // pin changed - un-register previous pin, register new pin - if (oldLdrPin >= 0) pinManager.deallocatePin(oldLdrPin, PinOwner::UM_LDR_DUSK_DAWN); + if (oldLdrPin >= 0) PinManager::deallocatePin(oldLdrPin, PinOwner::UM_LDR_DUSK_DAWN); setup(); // setup new pin } return configComplete; @@ -139,7 +139,7 @@ class LDR_Dusk_Dawn_v2 : public Usermod { //LDR_Off_Count.add(ldrOffCount); //bool pinValid = ((ldrPin >= 0) && (digitalPinToAnalogChannel(ldrPin) >= 0)); - //if (pinManager.getPinOwner(ldrPin) != PinOwner::UM_LDR_DUSK_DAWN) pinValid = false; + //if (PinManager::getPinOwner(ldrPin) != PinOwner::UM_LDR_DUSK_DAWN) pinValid = false; //JsonArray LDR_valid = user.createNestedArray(F("LDR pin")); //LDR_valid.add(ldrPin); //LDR_valid.add(pinValid ? F(" OK"): F(" invalid")); diff --git a/usermods/PIR_sensor_switch/readme.md b/usermods/PIR_sensor_switch/readme.md index 4dfdb07bd..fac5419f0 100644 --- a/usermods/PIR_sensor_switch/readme.md +++ b/usermods/PIR_sensor_switch/readme.md @@ -52,7 +52,7 @@ class MyUsermod : public Usermod { void togglePIRSensor() { #ifdef USERMOD_PIR_SENSOR_SWITCH - PIRsensorSwitch *PIRsensor = (PIRsensorSwitch::*) usermods.lookup(USERMOD_ID_PIRSWITCH); + PIRsensorSwitch *PIRsensor = (PIRsensorSwitch::*) UsermodManager::lookup(USERMOD_ID_PIRSWITCH); if (PIRsensor != nullptr) { PIRsensor->EnablePIRsensor(!PIRsensor->PIRsensorEnabled()); } diff --git a/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h b/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h index 7a67dd749..29070cf84 100644 --- a/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h +++ b/usermods/PIR_sensor_switch/usermod_PIR_sensor_switch.h @@ -375,7 +375,7 @@ void PIRsensorSwitch::setup() sensorPinState[i] = LOW; if (PIRsensorPin[i] < 0) continue; // pin retrieved from cfg.json (readFromConfig()) prior to running setup() - if (pinManager.allocatePin(PIRsensorPin[i], false, PinOwner::UM_PIR)) { + if (PinManager::allocatePin(PIRsensorPin[i], false, PinOwner::UM_PIR)) { // PIR Sensor mode INPUT_PULLDOWN #ifdef ESP8266 pinMode(PIRsensorPin[i], PIRsensorPin[i]==16 ? INPUT_PULLDOWN_16 : INPUT_PULLUP); // ESP8266 has INPUT_PULLDOWN on GPIO16 only @@ -564,7 +564,7 @@ bool PIRsensorSwitch::readFromConfig(JsonObject &root) DEBUG_PRINTLN(F(" config loaded.")); } else { for (int i = 0; i < PIR_SENSOR_MAX_SENSORS; i++) - if (oldPin[i] >= 0) pinManager.deallocatePin(oldPin[i], PinOwner::UM_PIR); + if (oldPin[i] >= 0) PinManager::deallocatePin(oldPin[i], PinOwner::UM_PIR); setup(); DEBUG_PRINTLN(F(" config (re)loaded.")); } diff --git a/usermods/PWM_fan/usermod_PWM_fan.h b/usermods/PWM_fan/usermod_PWM_fan.h index 1b78cfd4c..c3ef24fe4 100644 --- a/usermods/PWM_fan/usermod_PWM_fan.h +++ b/usermods/PWM_fan/usermod_PWM_fan.h @@ -75,7 +75,7 @@ class PWMFanUsermod : public Usermod { static const char _lock[]; void initTacho(void) { - if (tachoPin < 0 || !pinManager.allocatePin(tachoPin, false, PinOwner::UM_Unspecified)){ + if (tachoPin < 0 || !PinManager::allocatePin(tachoPin, false, PinOwner::UM_Unspecified)){ tachoPin = -1; return; } @@ -88,7 +88,7 @@ class PWMFanUsermod : public Usermod { void deinitTacho(void) { if (tachoPin < 0) return; detachInterrupt(digitalPinToInterrupt(tachoPin)); - pinManager.deallocatePin(tachoPin, PinOwner::UM_Unspecified); + PinManager::deallocatePin(tachoPin, PinOwner::UM_Unspecified); tachoPin = -1; } @@ -111,7 +111,7 @@ class PWMFanUsermod : public Usermod { // https://randomnerdtutorials.com/esp32-pwm-arduino-ide/ void initPWMfan(void) { - if (pwmPin < 0 || !pinManager.allocatePin(pwmPin, true, PinOwner::UM_Unspecified)) { + if (pwmPin < 0 || !PinManager::allocatePin(pwmPin, true, PinOwner::UM_Unspecified)) { enabled = false; pwmPin = -1; return; @@ -121,7 +121,7 @@ class PWMFanUsermod : public Usermod { analogWriteRange(255); analogWriteFreq(WLED_PWM_FREQ); #else - pwmChannel = pinManager.allocateLedc(1); + pwmChannel = PinManager::allocateLedc(1); if (pwmChannel == 255) { //no more free LEDC channels deinitPWMfan(); return; } @@ -136,9 +136,9 @@ class PWMFanUsermod : public Usermod { void deinitPWMfan(void) { if (pwmPin < 0) return; - pinManager.deallocatePin(pwmPin, PinOwner::UM_Unspecified); + PinManager::deallocatePin(pwmPin, PinOwner::UM_Unspecified); #ifdef ARDUINO_ARCH_ESP32 - pinManager.deallocateLedc(pwmChannel, 1); + PinManager::deallocateLedc(pwmChannel, 1); #endif pwmPin = -1; } @@ -191,9 +191,9 @@ class PWMFanUsermod : public Usermod { void setup() override { #ifdef USERMOD_DALLASTEMPERATURE // This Usermod requires Temperature usermod - tempUM = (UsermodTemperature*) usermods.lookup(USERMOD_ID_TEMPERATURE); + tempUM = (UsermodTemperature*) UsermodManager::lookup(USERMOD_ID_TEMPERATURE); #elif defined(USERMOD_SHT) - tempUM = (ShtUsermod*) usermods.lookup(USERMOD_ID_SHT); + tempUM = (ShtUsermod*) UsermodManager::lookup(USERMOD_ID_SHT); #endif initTacho(); initPWMfan(); diff --git a/usermods/SN_Photoresistor/usermods_list.cpp b/usermods/SN_Photoresistor/usermods_list.cpp index 649e19739..a2c6ca165 100644 --- a/usermods/SN_Photoresistor/usermods_list.cpp +++ b/usermods/SN_Photoresistor/usermods_list.cpp @@ -9,6 +9,6 @@ void registerUsermods() { #ifdef USERMOD_SN_PHOTORESISTOR - usermods.add(new Usermod_SN_Photoresistor()); + UsermodManager::add(new Usermod_SN_Photoresistor()); #endif } \ No newline at end of file diff --git a/usermods/ST7789_display/ST7789_display.h b/usermods/ST7789_display/ST7789_display.h index 59f6d9271..0dbada382 100644 --- a/usermods/ST7789_display/ST7789_display.h +++ b/usermods/ST7789_display/ST7789_display.h @@ -138,10 +138,10 @@ class St7789DisplayUsermod : public Usermod { void setup() override { PinManagerPinType spiPins[] = { { spi_mosi, true }, { spi_miso, false}, { spi_sclk, true } }; - if (!pinManager.allocateMultiplePins(spiPins, 3, PinOwner::HW_SPI)) { enabled = false; return; } + if (!PinManager::allocateMultiplePins(spiPins, 3, PinOwner::HW_SPI)) { enabled = false; return; } PinManagerPinType displayPins[] = { { TFT_CS, true}, { TFT_DC, true}, { TFT_RST, true }, { TFT_BL, true } }; - if (!pinManager.allocateMultiplePins(displayPins, sizeof(displayPins)/sizeof(PinManagerPinType), PinOwner::UM_FourLineDisplay)) { - pinManager.deallocateMultiplePins(spiPins, 3, PinOwner::HW_SPI); + if (!PinManager::allocateMultiplePins(displayPins, sizeof(displayPins)/sizeof(PinManagerPinType), PinOwner::UM_FourLineDisplay)) { + PinManager::deallocateMultiplePins(spiPins, 3, PinOwner::HW_SPI); enabled = false; return; } diff --git a/usermods/Temperature/usermod_temperature.h b/usermods/Temperature/usermod_temperature.h index d7a9d82a4..ad755eaee 100644 --- a/usermods/Temperature/usermod_temperature.h +++ b/usermods/Temperature/usermod_temperature.h @@ -73,7 +73,7 @@ class UsermodTemperature : public Usermod { void publishHomeAssistantAutodiscovery(); #endif - static UsermodTemperature* _instance; // to overcome nonstatic getTemperatureC() method and avoid usermods.lookup(USERMOD_ID_TEMPERATURE); + static UsermodTemperature* _instance; // to overcome nonstatic getTemperatureC() method and avoid UsermodManager::lookup(USERMOD_ID_TEMPERATURE); public: @@ -223,14 +223,14 @@ void UsermodTemperature::setup() { // config says we are enabled DEBUG_PRINTLN(F("Allocating temperature pin...")); // pin retrieved from cfg.json (readFromConfig()) prior to running setup() - if (temperaturePin >= 0 && pinManager.allocatePin(temperaturePin, true, PinOwner::UM_Temperature)) { + if (temperaturePin >= 0 && PinManager::allocatePin(temperaturePin, true, PinOwner::UM_Temperature)) { oneWire = new OneWire(temperaturePin); if (oneWire->reset()) { while (!findSensor() && retries--) { delay(25); // try to find sensor } } - if (parasite && pinManager.allocatePin(parasitePin, true, PinOwner::UM_Temperature)) { + if (parasite && PinManager::allocatePin(parasitePin, true, PinOwner::UM_Temperature)) { pinMode(parasitePin, OUTPUT); digitalWrite(parasitePin, LOW); // deactivate power (close MOSFET) } else { @@ -423,9 +423,9 @@ bool UsermodTemperature::readFromConfig(JsonObject &root) { DEBUG_PRINTLN(F("Re-init temperature.")); // deallocate pin and release memory delete oneWire; - pinManager.deallocatePin(temperaturePin, PinOwner::UM_Temperature); + PinManager::deallocatePin(temperaturePin, PinOwner::UM_Temperature); temperaturePin = newTemperaturePin; - pinManager.deallocatePin(parasitePin, PinOwner::UM_Temperature); + PinManager::deallocatePin(parasitePin, PinOwner::UM_Temperature); // initialise setup(); } diff --git a/usermods/audioreactive/audio_reactive.old.h b/usermods/audioreactive/audio_reactive.old.h new file mode 100644 index 000000000..4f2e04c08 --- /dev/null +++ b/usermods/audioreactive/audio_reactive.old.h @@ -0,0 +1,2071 @@ +#pragma once + +#include "wled.h" + +#ifdef ARDUINO_ARCH_ESP32 + +#include +#include + +#ifdef WLED_ENABLE_DMX + #error This audio reactive usermod is not compatible with DMX Out. +#endif + +#endif + +#if defined(ARDUINO_ARCH_ESP32) && (defined(WLED_DEBUG) || defined(SR_DEBUG)) +#include +#endif + +/* + * Usermods allow you to add own functionality to WLED more easily + * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality + * + * This is an audioreactive v2 usermod. + * .... + */ + +#if !defined(FFTTASK_PRIORITY) +#define FFTTASK_PRIORITY 1 // standard: looptask prio +//#define FFTTASK_PRIORITY 2 // above looptask, below asyc_tcp +//#define FFTTASK_PRIORITY 4 // above asyc_tcp +#endif + +// Comment/Uncomment to toggle usb serial debugging +// #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) DEBUGOUT.print(x) + #define DEBUGSR_PRINTLN(x) DEBUGOUT.println(x) + #define DEBUGSR_PRINTF(x...) DEBUGOUT.printf(x) +#else + #define DEBUGSR_PRINT(x) + #define DEBUGSR_PRINTLN(x) + #define DEBUGSR_PRINTF(x...) +#endif + +#if defined(MIC_LOGGER) || defined(FFT_SAMPLING_LOG) + #define PLOT_PRINT(x) DEBUGOUT.print(x) + #define PLOT_PRINTLN(x) DEBUGOUT.println(x) + #define PLOT_PRINTF(x...) DEBUGOUT.printf(x) +#else + #define PLOT_PRINT(x) + #define PLOT_PRINTLN(x) + #define PLOT_PRINTF(x...) +#endif + +#define MAX_PALETTES 3 + +static volatile bool disableSoundProcessing = false; // if true, sound processing (FFT, filters, AGC) will be suspended. "volatile" as its shared between tasks. +static uint8_t audioSyncEnabled = 0; // bit field: bit 0 - send, bit 1 - receive (config value) +static bool udpSyncConnected = false; // UDP connection status -> true if connected to multicast group + +#define NUM_GEQ_CHANNELS 16 // number of frequency channels. Don't change !! + +// audioreactive variables +#ifdef ARDUINO_ARCH_ESP32 +static float micDataReal = 0.0f; // MicIn data with full 24bit resolution - lowest 8bit after decimal point +static float multAgc = 1.0f; // sample * multAgc = sampleAgc. Our AGC multiplier +static float sampleAvg = 0.0f; // Smoothed Average sample - sampleAvg < 1 means "quiet" (simple noise gate) +static float sampleAgc = 0.0f; // Smoothed AGC sample +static uint8_t soundAgc = 0; // Automagic gain control: 0 - none, 1 - normal, 2 - vivid, 3 - lazy (config value) +#endif +//static float volumeSmth = 0.0f; // either sampleAvg or sampleAgc depending on soundAgc; smoothed sample +static float FFT_MajorPeak = 1.0f; // FFT: strongest (peak) frequency +static float FFT_Magnitude = 0.0f; // FFT: volume (magnitude) of peak frequency +static bool samplePeak = false; // Boolean flag for peak - used in effects. Responding routine may reset this flag. Auto-reset after WS2812FX::getMinShowDelay() +static bool udpSamplePeak = false; // Boolean flag for peak. Set at the same time as samplePeak, but reset by transmitAudioData +static unsigned long timeOfPeak = 0; // time of last sample peak detection. +static uint8_t fftResult[NUM_GEQ_CHANNELS]= {0};// Our calculated freq. channel result table to be used by effects + +// 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 + +// user settable parameters for limitSoundDynamics() +#ifdef UM_AUDIOREACTIVE_DYNAMICS_LIMITER_OFF +static bool limiterOn = false; // bool: enable / disable dynamics limiter +#else +static bool limiterOn = true; +#endif +static uint16_t attackTime = 80; // int: attack time in milliseconds. Default 0.08sec +static uint16_t decayTime = 1400; // int: decay time in milliseconds. Default 1.40sec + +// peak detection +#ifdef ARDUINO_ARCH_ESP32 +static void detectSamplePeak(void); // peak detection function (needs scaled FFT results in vReal[]) - no used for 8266 receive-only mode +#endif +static void autoResetPeak(void); // peak auto-reset function +static uint8_t maxVol = 31; // (was 10) Reasonable value for constant volume for 'peak detector', as it won't always trigger (deprecated) +static uint8_t binNum = 8; // Used to select the bin for FFT based beat detection (deprecated) + +#ifdef ARDUINO_ARCH_ESP32 + +// use audio source class (ESP32 specific) +#include "audio_source.h" +constexpr i2s_port_t I2S_PORT = I2S_NUM_0; // I2S port to use (do not change !) +constexpr int BLOCK_SIZE = 128; // I2S buffer size (samples) + +// globals +static uint8_t inputLevel = 128; // UI slider value +#ifndef SR_SQUELCH + uint8_t soundSquelch = 10; // squelch value for volume reactive routines (config value) +#else + uint8_t soundSquelch = SR_SQUELCH; // squelch value for volume reactive routines (config value) +#endif +#ifndef SR_GAIN + uint8_t sampleGain = 60; // sample gain (config value) +#else + uint8_t sampleGain = SR_GAIN; // sample gain (config value) +#endif +// user settable options for FFTResult scaling +static uint8_t FFTScalingMode = 3; // 0 none; 1 optimized logarithmic; 2 optimized linear; 3 optimized square root + +// +// AGC presets +// Note: in C++, "const" implies "static" - no need to explicitly declare everything as "static const" +// +#define AGC_NUM_PRESETS 3 // AGC presets: normal, vivid, lazy +const double agcSampleDecay[AGC_NUM_PRESETS] = { 0.9994f, 0.9985f, 0.9997f}; // decay factor for sampleMax, in case the current sample is below sampleMax +const float agcZoneLow[AGC_NUM_PRESETS] = { 32, 28, 36}; // low volume emergency zone +const float agcZoneHigh[AGC_NUM_PRESETS] = { 240, 240, 248}; // high volume emergency zone +const float agcZoneStop[AGC_NUM_PRESETS] = { 336, 448, 304}; // disable AGC integrator if we get above this level +const float agcTarget0[AGC_NUM_PRESETS] = { 112, 144, 164}; // first AGC setPoint -> between 40% and 65% +const float agcTarget0Up[AGC_NUM_PRESETS] = { 88, 64, 116}; // setpoint switching value (a poor man's bang-bang) +const float agcTarget1[AGC_NUM_PRESETS] = { 220, 224, 216}; // second AGC setPoint -> around 85% +const double agcFollowFast[AGC_NUM_PRESETS] = { 1/192.f, 1/128.f, 1/256.f}; // quickly follow setpoint - ~0.15 sec +const double agcFollowSlow[AGC_NUM_PRESETS] = {1/6144.f,1/4096.f,1/8192.f}; // slowly follow setpoint - ~2-15 secs +const double agcControlKp[AGC_NUM_PRESETS] = { 0.6f, 1.5f, 0.65f}; // AGC - PI control, proportional gain parameter +const double agcControlKi[AGC_NUM_PRESETS] = { 1.7f, 1.85f, 1.2f}; // AGC - PI control, integral gain parameter +const float agcSampleSmooth[AGC_NUM_PRESETS] = { 1/12.f, 1/6.f, 1/16.f}; // smoothing factor for sampleAgc (use rawSampleAgc if you want the non-smoothed value) +// AGC presets end + +static AudioSource *audioSource = nullptr; +static bool useBandPassFilter = false; // if true, enables a bandpass filter 80Hz-16Khz to remove noise. Applies before FFT. + +//////////////////// +// Begin FFT Code // +//////////////////// + +// some prototypes, to ensure consistent interfaces +static float mapf(float x, float in_min, float in_max, float out_min, float out_max); // map function for float +static float fftAddAvg(int from, int to); // average of several FFT result bins +static void FFTcode(void * parameter); // audio processing task: read samples, run FFT, fill GEQ channels from FFT results +static void runMicFilter(uint16_t numSamples, float *sampleBuffer); // pre-filtering of raw samples (band-pass) +static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels); // post-processing and post-amp of GEQ channels + +static TaskHandle_t FFT_Task = nullptr; + +// Table of multiplication factors so that we can even out the frequency response. +static float fftResultPink[NUM_GEQ_CHANNELS] = { 1.70f, 1.71f, 1.73f, 1.78f, 1.68f, 1.56f, 1.55f, 1.63f, 1.79f, 1.62f, 1.80f, 2.06f, 2.47f, 3.35f, 6.83f, 9.55f }; + +// globals and FFT Output variables shared with animations +#if defined(WLED_DEBUG) || defined(SR_DEBUG) +static uint64_t fftTime = 0; +static uint64_t sampleTime = 0; +#endif + +// FFT Task variables (filtering and post-processing) +static float fftCalc[NUM_GEQ_CHANNELS] = {0.0f}; // Try and normalize fftBin values to a max of 4096, so that 4096/16 = 256. +static float fftAvg[NUM_GEQ_CHANNELS] = {0.0f}; // Calculated frequency channel results, with smoothing (used if dynamics limiter is ON) +#ifdef SR_DEBUG +static float fftResultMax[NUM_GEQ_CHANNELS] = {0.0f}; // A table used for testing to determine how our post-processing is working. +#endif + +// audio source parameters and constant +#ifdef ARDUINO_ARCH_ESP32C3 +constexpr SRate_t SAMPLE_RATE = 16000; // 16kHz - use if FFTtask takes more than 20ms. Physical sample time -> 32ms +#define FFT_MIN_CYCLE 30 // Use with 16Khz sampling +#else +constexpr SRate_t SAMPLE_RATE = 22050; // Base sample rate in Hz - 22Khz is a standard rate. Physical sample time -> 23ms +//constexpr SRate_t SAMPLE_RATE = 16000; // 16kHz - use if FFTtask takes more than 20ms. Physical sample time -> 32ms +//constexpr SRate_t SAMPLE_RATE = 20480; // Base sample rate in Hz - 20Khz is experimental. Physical sample time -> 25ms +//constexpr SRate_t SAMPLE_RATE = 10240; // Base sample rate in Hz - previous default. Physical sample time -> 50ms +#define FFT_MIN_CYCLE 21 // minimum time before FFT task is repeated. Use with 22Khz sampling +//#define FFT_MIN_CYCLE 30 // Use with 16Khz sampling +//#define FFT_MIN_CYCLE 23 // minimum time before FFT task is repeated. Use with 20Khz sampling +//#define FFT_MIN_CYCLE 46 // minimum time before FFT task is repeated. Use with 10Khz sampling +#endif + +// FFT Constants +constexpr uint16_t samplesFFT = 512; // Samples in an FFT batch - This value MUST ALWAYS be a power of 2 +constexpr uint16_t samplesFFT_2 = 256; // meaningfull part of FFT results - only the "lower half" contains useful information. +// the following are observed values, supported by a bit of "educated guessing" +//#define FFT_DOWNSCALE 0.65f // 20kHz - downscaling factor for FFT results - "Flat-Top" window @20Khz, old freq channels +#define FFT_DOWNSCALE 0.46f // downscaling factor for FFT results - for "Flat-Top" window @22Khz, new freq channels +#define LOG_256 5.54517744f // log(256) + +// These are the input and output vectors. Input vectors receive computed results from FFT. +static float vReal[samplesFFT] = {0.0f}; // FFT sample inputs / freq output - these are our raw result bins +static float vImag[samplesFFT] = {0.0f}; // imaginary parts + +// Create FFT object +// lib_deps += https://github.com/kosme/arduinoFFT#develop @ 2.0.1 +// these options actually cause slow-downs on all esp32 processors, don't use them. +// #define FFT_SPEED_OVER_PRECISION // enables use of reciprocals (1/x etc) - not faster on ESP32 +// #define FFT_SQRT_APPROXIMATION // enables "quake3" style inverse sqrt - slower on ESP32 +// Below options are forcing ArduinoFFT to use sqrtf() instead of sqrt() +#define sqrt(x) sqrtf(x) // little hack that reduces FFT time by 10-50% on ESP32 +#define sqrt_internal sqrtf // see https://github.com/kosme/arduinoFFT/pull/83 + +#include + +/* Create FFT object with weighing factor storage */ +static ArduinoFFT FFT = ArduinoFFT( vReal, vImag, samplesFFT, SAMPLE_RATE, true); + +// Helper functions + +// float version of map() +static float mapf(float x, float in_min, float in_max, float out_min, float out_max){ + return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; +} + +// compute average of several FFT result bins +static float fftAddAvg(int from, int to) { + float result = 0.0f; + for (int i = from; i <= to; i++) { + result += vReal[i]; + } + return result / float(to - from + 1); +} + +// +// FFT main task +// +void FFTcode(void * parameter) +{ + DEBUGSR_PRINT("FFT started on core: "); DEBUGSR_PRINTLN(xPortGetCoreID()); + + // see https://www.freertos.org/vtaskdelayuntil.html + const TickType_t xFrequency = FFT_MIN_CYCLE * portTICK_PERIOD_MS; + + TickType_t xLastWakeTime = xTaskGetTickCount(); + 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. + + // Don't run FFT computing code if we're in Receive mode or in realtime mode + if (disableSoundProcessing || (audioSyncEnabled & 0x02)) { + vTaskDelayUntil( &xLastWakeTime, xFrequency); // release CPU, and let I2S fill its buffers + continue; + } + +#if defined(WLED_DEBUG) || defined(SR_DEBUG) + uint64_t start = esp_timer_get_time(); + bool haveDoneFFT = false; // indicates if second measurement (FFT time) is valid +#endif + + // get a fresh batch of samples from I2S + if (audioSource) audioSource->getSamples(vReal, samplesFFT); + +#if defined(WLED_DEBUG) || defined(SR_DEBUG) + if (start < esp_timer_get_time()) { // filter out overflows + uint64_t sampleTimeInMillis = (esp_timer_get_time() - start +5ULL) / 10ULL; // "+5" to ensure proper rounding + sampleTime = (sampleTimeInMillis*3 + sampleTime*7)/10; // smooth + } + start = esp_timer_get_time(); // start measuring FFT time +#endif + + xLastWakeTime = xTaskGetTickCount(); // update "last unblocked time" for vTaskDelay + + // band pass filter - can reduce noise floor by a factor of 50 + // downside: frequencies below 100Hz will be ignored + if (useBandPassFilter) runMicFilter(samplesFFT, vReal); + + // find highest sample in the batch + float maxSample = 0.0f; // max sample from 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 (fabsf((float)vReal[i]) > maxSample) maxSample = fabsf((float)vReal[i]); + } + // 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; + +#ifdef SR_DEBUG + if (true) { // this allows measure FFT runtimes, as it disables the "only when needed" optimization +#else + if (sampleAvg > 0.25f) { // noise gate open means that FFT results will be used. Don't run FFT if results are not needed. +#endif + + // run FFT (takes 3-5ms on ESP32, ~12ms on ESP32-S2) + FFT.dcRemoval(); // remove DC offset + FFT.windowing( FFTWindow::Flat_top, FFTDirection::Forward); // Weigh data using "Flat Top" function - better amplitude accuracy + //FFT.windowing(FFTWindow::Blackman_Harris, FFTDirection::Forward); // Weigh data using "Blackman- Harris" window - sharp peaks due to excellent sideband rejection + FFT.compute( FFTDirection::Forward ); // Compute FFT + FFT.complexToMagnitude(); // Compute magnitudes + vReal[0] = 0.0f; // The remaining DC offset on the signal produces a strong spike on position 0 that should be eliminated to avoid issues. + + FFT.majorPeak(&FFT_MajorPeak, &FFT_Magnitude); // let the effects know which freq was most dominant + FFT_MajorPeak = constrain(FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects + +#if defined(WLED_DEBUG) || defined(SR_DEBUG) + haveDoneFFT = true; +#endif + + } else { // noise gate closed - only clear results as FFT was skipped. MIC samples are still valid when we do this. + memset(vReal, 0, sizeof(vReal)); + FFT_MajorPeak = 1.0f; + FFT_Magnitude = 0.001f; + } + + for (int i = 0; i < samplesFFT; i++) { + float t = fabsf(vReal[i]); // just to be sure - values in fft bins should be positive any way + vReal[i] = t / 16.0f; // Reduce magnitude. Want end result to be scaled linear and ~4096 max. + } // for() + + // mapping of FFT result bins to frequency channels + if (fabsf(sampleAvg) > 0.5f) { // noise gate open +#if 0 + /* 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, 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 determine the bins. + * End frequency = Start frequency * multiplier ^ 16 + * Multiplier = (End frequency/ Start frequency) ^ 1/16 + * Multiplier = 1.320367784 + */ // Range + fftCalc[ 0] = fftAddAvg(2,4); // 60 - 100 + fftCalc[ 1] = fftAddAvg(4,5); // 80 - 120 + fftCalc[ 2] = fftAddAvg(5,7); // 100 - 160 + fftCalc[ 3] = fftAddAvg(7,9); // 140 - 200 + fftCalc[ 4] = fftAddAvg(9,12); // 180 - 260 + fftCalc[ 5] = fftAddAvg(12,16); // 240 - 340 + fftCalc[ 6] = fftAddAvg(16,21); // 320 - 440 + fftCalc[ 7] = fftAddAvg(21,29); // 420 - 600 + fftCalc[ 8] = fftAddAvg(29,37); // 580 - 760 + fftCalc[ 9] = fftAddAvg(37,48); // 740 - 980 + fftCalc[10] = fftAddAvg(48,64); // 960 - 1300 + fftCalc[11] = fftAddAvg(64,84); // 1280 - 1700 + fftCalc[12] = fftAddAvg(84,111); // 1680 - 2240 + fftCalc[13] = fftAddAvg(111,147); // 2220 - 2960 + fftCalc[14] = fftAddAvg(147,194); // 2940 - 3900 + fftCalc[15] = fftAddAvg(194,250); // 3880 - 5000 // avoid the last 5 bins, which are usually inaccurate +#else + /* new mapping, optimized for 22050 Hz by softhack007 */ + // bins frequency range + if (useBandPassFilter) { + // skip frequencies below 100hz + fftCalc[ 0] = 0.8f * fftAddAvg(3,4); + fftCalc[ 1] = 0.9f * fftAddAvg(4,5); + fftCalc[ 2] = fftAddAvg(5,6); + fftCalc[ 3] = fftAddAvg(6,7); + // don't use the last bins from 206 to 255. + fftCalc[15] = fftAddAvg(165,205) * 0.75f; // 40 7106 - 8828 high -- with some damping + } else { + fftCalc[ 0] = fftAddAvg(1,2); // 1 43 - 86 sub-bass + fftCalc[ 1] = fftAddAvg(2,3); // 1 86 - 129 bass + fftCalc[ 2] = fftAddAvg(3,5); // 2 129 - 216 bass + fftCalc[ 3] = fftAddAvg(5,7); // 2 216 - 301 bass + midrange + // don't use the last bins from 216 to 255. They are usually contaminated by aliasing (aka noise) + fftCalc[15] = fftAddAvg(165,215) * 0.70f; // 50 7106 - 9259 high -- with some damping + } + fftCalc[ 4] = fftAddAvg(7,10); // 3 301 - 430 midrange + fftCalc[ 5] = fftAddAvg(10,13); // 3 430 - 560 midrange + fftCalc[ 6] = fftAddAvg(13,19); // 5 560 - 818 midrange + fftCalc[ 7] = fftAddAvg(19,26); // 7 818 - 1120 midrange -- 1Khz should always be the center ! + fftCalc[ 8] = fftAddAvg(26,33); // 7 1120 - 1421 midrange + fftCalc[ 9] = fftAddAvg(33,44); // 9 1421 - 1895 midrange + fftCalc[10] = fftAddAvg(44,56); // 12 1895 - 2412 midrange + high mid + fftCalc[11] = fftAddAvg(56,70); // 14 2412 - 3015 high mid + fftCalc[12] = fftAddAvg(70,86); // 16 3015 - 3704 high mid + fftCalc[13] = fftAddAvg(86,104); // 18 3704 - 4479 high mid + fftCalc[14] = fftAddAvg(104,165) * 0.88f; // 61 4479 - 7106 high mid + high -- with slight damping +#endif + } else { // noise gate closed - just decay old values + for (int i=0; i < NUM_GEQ_CHANNELS; i++) { + fftCalc[i] *= 0.85f; // decay to zero + if (fftCalc[i] < 4.0f) fftCalc[i] = 0.0f; + } + } + + // post-processing of frequency channels (pink noise adjustment, AGC, smoothing, scaling) + postProcessFFTResults((fabsf(sampleAvg) > 0.25f)? true : false , NUM_GEQ_CHANNELS); + +#if defined(WLED_DEBUG) || defined(SR_DEBUG) + if (haveDoneFFT && (start < esp_timer_get_time())) { // filter out overflows + uint64_t fftTimeInMillis = ((esp_timer_get_time() - start) +5ULL) / 10ULL; // "+5" to ensure proper rounding + fftTime = (fftTimeInMillis*3 + fftTime*7)/10; // smooth + } +#endif + // run peak detection + autoResetPeak(); + detectSamplePeak(); + + #if !defined(I2S_GRAB_ADC1_COMPLETELY) + if ((audioSource == nullptr) || (audioSource->getType() != AudioSource::Type_I2SAdc)) // the "delay trick" does not help for analog ADC + #endif + vTaskDelayUntil( &xLastWakeTime, xFrequency); // release CPU, and let I2S fill its buffers + + } // for(;;)ever +} // FFTcode() task end + + +/////////////////////////// +// Pre / Postprocessing // +/////////////////////////// + +static void runMicFilter(uint16_t numSamples, float *sampleBuffer) // pre-filtering of raw samples (band-pass) +{ + // low frequency cutoff parameter - see https://dsp.stackexchange.com/questions/40462/exponential-moving-average-cut-off-frequency + //constexpr float alpha = 0.04f; // 150Hz + //constexpr float alpha = 0.03f; // 110Hz + constexpr float alpha = 0.0225f; // 80hz + //constexpr float alpha = 0.01693f;// 60hz + // high frequency cutoff parameter + //constexpr float beta1 = 0.75f; // 11Khz + //constexpr float beta1 = 0.82f; // 15Khz + //constexpr float beta1 = 0.8285f; // 18Khz + constexpr float beta1 = 0.85f; // 20Khz + + constexpr float beta2 = (1.0f - beta1) / 2.0f; + static float last_vals[2] = { 0.0f }; // FIR high freq cutoff filter + static float lowfilt = 0.0f; // IIR low frequency cutoff filter + + for (int i=0; i < numSamples; i++) { + // FIR lowpass, to remove high frequency noise + float highFilteredSample; + if (i < (numSamples-1)) highFilteredSample = beta1*sampleBuffer[i] + beta2*last_vals[0] + beta2*sampleBuffer[i+1]; // smooth out spikes + else highFilteredSample = beta1*sampleBuffer[i] + beta2*last_vals[0] + beta2*last_vals[1]; // special handling for last sample in array + last_vals[1] = last_vals[0]; + last_vals[0] = sampleBuffer[i]; + sampleBuffer[i] = highFilteredSample; + // IIR highpass, to remove low frequency noise + lowfilt += alpha * (sampleBuffer[i] - lowfilt); + sampleBuffer[i] = sampleBuffer[i] - lowfilt; + } +} + +static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels) // post-processing and post-amp of GEQ channels +{ + for (int i=0; i < numberOfChannels; i++) { + + if (noiseGateOpen) { // noise gate open + // Adjustment for frequency curves. + fftCalc[i] *= fftResultPink[i]; + if (FFTScalingMode > 0) fftCalc[i] *= FFT_DOWNSCALE; // adjustment related to FFT windowing function + // Manual linear adjustment of gain using sampleGain adjustment for different input types. + fftCalc[i] *= soundAgc ? multAgc : ((float)sampleGain/40.0f * (float)inputLevel/128.0f + 1.0f/16.0f); //apply gain, with inputLevel adjustment + if(fftCalc[i] < 0) fftCalc[i] = 0.0f; + } + + // smooth results - rise fast, fall slower + if (fftCalc[i] > fftAvg[i]) fftAvg[i] = fftCalc[i]*0.75f + 0.25f*fftAvg[i]; // rise fast; will need approx 2 cycles (50ms) for converging against fftCalc[i] + else { // fall slow + if (decayTime < 1000) fftAvg[i] = fftCalc[i]*0.22f + 0.78f*fftAvg[i]; // approx 5 cycles (225ms) for falling to zero + else if (decayTime < 2000) fftAvg[i] = fftCalc[i]*0.17f + 0.83f*fftAvg[i]; // default - approx 9 cycles (225ms) for falling to zero + else if (decayTime < 3000) fftAvg[i] = fftCalc[i]*0.14f + 0.86f*fftAvg[i]; // approx 14 cycles (350ms) for falling to zero + else fftAvg[i] = fftCalc[i]*0.1f + 0.9f*fftAvg[i]; // approx 20 cycles (500ms) for falling to zero + } + // constrain internal vars - just to be sure + fftCalc[i] = constrain(fftCalc[i], 0.0f, 1023.0f); + fftAvg[i] = constrain(fftAvg[i], 0.0f, 1023.0f); + + float currentResult; + if(limiterOn == true) + currentResult = fftAvg[i]; + else + currentResult = fftCalc[i]; + + switch (FFTScalingMode) { + case 1: + // Logarithmic scaling + currentResult *= 0.42f; // 42 is the answer ;-) + currentResult -= 8.0f; // this skips the lowest row, giving some room for peaks + if (currentResult > 1.0f) currentResult = logf(currentResult); // log to base "e", which is the fastest log() function + else currentResult = 0.0f; // special handling, because log(1) = 0; log(0) = undefined + currentResult *= 0.85f + (float(i)/18.0f); // extra up-scaling for high frequencies + currentResult = mapf(currentResult, 0.0f, LOG_256, 0.0f, 255.0f); // map [log(1) ... log(255)] to [0 ... 255] + break; + case 2: + // Linear scaling + currentResult *= 0.30f; // needs a bit more damping, get stay below 255 + currentResult -= 4.0f; // giving a bit more room for peaks (WLEDMM uses -2) + if (currentResult < 1.0f) currentResult = 0.0f; + currentResult *= 0.85f + (float(i)/1.8f); // extra up-scaling for high frequencies + break; + case 3: + // square root scaling + currentResult *= 0.38f; + currentResult -= 6.0f; + if (currentResult > 1.0f) currentResult = sqrtf(currentResult); + else currentResult = 0.0f; // special handling, because sqrt(0) = undefined + currentResult *= 0.85f + (float(i)/4.5f); // extra up-scaling for high frequencies + currentResult = mapf(currentResult, 0.0f, 16.0f, 0.0f, 255.0f); // map [sqrt(1) ... sqrt(256)] to [0 ... 255] + break; + + case 0: + default: + // no scaling - leave freq bins as-is + currentResult -= 4; // just a bit more room for peaks (WLEDMM uses -2) + break; + } + + // Now, let's dump it all into fftResult. Need to do this, otherwise other routines might grab fftResult values prematurely. + if (soundAgc > 0) { // apply extra "GEQ Gain" if set by user + float post_gain = (float)inputLevel/128.0f; + if (post_gain < 1.0f) post_gain = ((post_gain -1.0f) * 0.8f) +1.0f; + currentResult *= post_gain; + } + fftResult[i] = constrain((int)currentResult, 0, 255); + } +} +//////////////////// +// Peak detection // +//////////////////// + +// peak detection is called from FFT task when vReal[] contains valid FFT results +static void detectSamplePeak(void) { + bool havePeak = false; + // softhack007: this code continuously triggers while amplitude in the selected bin is above a certain threshold. So it does not detect peaks - it detects high activity in a frequency bin. + // Poor man's beat detection by seeing if sample > Average + some value. + // This goes through ALL of the 255 bins - but ignores stupid settings + // 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. + if ((sampleAvg > 1) && (maxVol > 0) && (binNum > 4) && (vReal[binNum] > maxVol) && ((millis() - timeOfPeak) > 100)) { + havePeak = true; + } + + if (havePeak) { + samplePeak = true; + timeOfPeak = millis(); + udpSamplePeak = true; + } +} + +#endif + +static void autoResetPeak(void) { + uint16_t MinShowDelay = MAX(50, WS2812FX::getMinShowDelay()); // Fixes private class variable compiler error. Unsure if this is the correct way of fixing the root problem. -THATDONFC + if (millis() - timeOfPeak > MinShowDelay) { // Auto-reset of samplePeak after a complete frame has passed. + samplePeak = false; + if (audioSyncEnabled == 0) udpSamplePeak = false; // this is normally reset by transmitAudioData + } +} + + +//////////////////// +// usermod class // +//////////////////// + +//class name. Use something descriptive and leave the ": public Usermod" part :) +class AudioReactive : public Usermod { + + private: +#ifdef ARDUINO_ARCH_ESP32 + + #ifndef AUDIOPIN + int8_t audioPin = -1; + #else + int8_t audioPin = AUDIOPIN; + #endif + #ifndef SR_DMTYPE // I2S mic type + uint8_t dmType = 1; // 0=none/disabled/analog; 1=generic I2S + #define SR_DMTYPE 1 // default type = I2S + #else + uint8_t dmType = SR_DMTYPE; + #endif + #ifndef I2S_SDPIN // aka DOUT + int8_t i2ssdPin = 32; + #else + int8_t i2ssdPin = I2S_SDPIN; + #endif + #ifndef I2S_WSPIN // aka LRCL + int8_t i2swsPin = 15; + #else + int8_t i2swsPin = I2S_WSPIN; + #endif + #ifndef I2S_CKPIN // aka BCLK + int8_t i2sckPin = 14; /*PDM: set to I2S_PIN_NO_CHANGE*/ + #else + int8_t i2sckPin = I2S_CKPIN; + #endif + #ifndef MCLK_PIN + int8_t mclkPin = I2S_PIN_NO_CHANGE; /* ESP32: only -1, 0, 1, 3 allowed*/ + #else + int8_t mclkPin = MCLK_PIN; + #endif +#endif + + // new "V2" audiosync struct - 44 Bytes + struct __attribute__ ((packed)) audioSyncPacket { // "packed" ensures that there are no additional gaps + char header[6]; // 06 Bytes offset 0 + uint8_t reserved1[2]; // 02 Bytes, offset 6 - gap required by the compiler - not used yet + float sampleRaw; // 04 Bytes offset 8 - either "sampleRaw" or "rawSampleAgc" depending on soundAgc setting + float sampleSmth; // 04 Bytes offset 12 - either "sampleAvg" or "sampleAgc" depending on soundAgc setting + uint8_t samplePeak; // 01 Bytes offset 16 - 0 no peak; >=1 peak detected. In future, this will also provide peak Magnitude + uint8_t reserved2; // 01 Bytes offset 17 - for future extensions - not used yet + uint8_t fftResult[16]; // 16 Bytes offset 18 + uint16_t reserved3; // 02 Bytes, offset 34 - gap required by the compiler - not used yet + float FFT_Magnitude; // 04 Bytes offset 36 + float FFT_MajorPeak; // 04 Bytes offset 40 + }; + + // old "V1" audiosync struct - 83 Bytes payload, 88 bytes total (with padding added by compiler) - for backwards compatibility + struct audioSyncPacket_v1 { + char header[6]; // 06 Bytes + uint8_t myVals[32]; // 32 Bytes + int sampleAgc; // 04 Bytes + int sampleRaw; // 04 Bytes + float sampleAvg; // 04 Bytes + bool samplePeak; // 01 Bytes + uint8_t fftResult[16]; // 16 Bytes + double FFT_Magnitude; // 08 Bytes + double FFT_MajorPeak; // 08 Bytes + }; + + constexpr static unsigned UDPSOUND_MAX_PACKET = MAX(sizeof(audioSyncPacket), sizeof(audioSyncPacket_v1)); + + // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer) + #ifdef UM_AUDIOREACTIVE_ENABLE + bool enabled = true; + #else + bool enabled = false; + #endif + + bool initDone = false; + bool addPalettes = false; + int8_t palettes = 0; + + // variables for UDP sound sync + WiFiUDP fftUdp; // UDP object for sound sync (from WiFi UDP, not Async UDP!) + unsigned long lastTime = 0; // last time of running UDP Microphone Sync + const uint16_t delayMs = 10; // I don't want to sample too often and overload WLED + uint16_t audioSyncPort= 11988;// default port for UDP sound sync + + bool updateIsRunning = false; // true during OTA. + +#ifdef ARDUINO_ARCH_ESP32 + // used for AGC + int last_soundAgc = -1; // used to detect AGC mode change (for resetting AGC internal error buffers) + float control_integrated = 0.0f; // persistent across calls to agcAvg(); "integrator control" = accumulated error + // variables used by getSample() and agcAvg() + int16_t micIn = 0; // Current sample starts with negative values and large values, which is why it's 16 bit signed + float sampleMax = 0.0f; // Max sample over a few seconds. Needed for AGC controller. + float micLev = 0.0f; // Used to convert returned value to have '0' as minimum. A leveller + float expAdjF = 0.0f; // Used for exponential filter. + float sampleReal = 0.0f; // "sampleRaw" as float, to provide bits that are lost otherwise (before amplification by sampleGain or inputLevel). Needed for AGC. + int16_t sampleRaw = 0; // Current sample. Must only be updated ONCE!!! (amplified mic value by sampleGain and inputLevel) + int16_t rawSampleAgc = 0; // not smoothed AGC sample +#endif + + // variables used in effects + float volumeSmth = 0.0f; // either sampleAvg or sampleAgc depending on soundAgc; smoothed sample + int16_t volumeRaw = 0; // either sampleRaw or rawSampleAgc depending on soundAgc + float my_magnitude =0.0f; // FFT_Magnitude, scaled by multAgc + + // used to feed "Info" Page + unsigned long last_UDPTime = 0; // time of last valid UDP sound sync datapacket + int receivedFormat = 0; // last received UDP sound sync format - 0=none, 1=v1 (0.13.x), 2=v2 (0.14.x) + float maxSample5sec = 0.0f; // max sample (after AGC) in last 5 seconds + unsigned long sampleMaxTimer = 0; // last time maxSample5sec was reset + #define CYCLE_SAMPLEMAX 3500 // time window for merasuring + + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _enabled[]; + static const char _config[]; + static const char _dynamics[]; + static const char _frequency[]; + static const char _inputLvl[]; +#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) + static const char _analogmic[]; +#endif + static const char _digitalmic[]; + static const char _addPalettes[]; + static const char UDP_SYNC_HEADER[]; + static const char UDP_SYNC_HEADER_v1[]; + + // private methods + void removeAudioPalettes(void); + void createAudioPalettes(void); + CRGB getCRGBForBand(int x, int pal); + void fillAudioPalettes(void); + + //////////////////// + // Debug support // + //////////////////// + void logAudio() + { + if (disableSoundProcessing && (!udpSyncConnected || ((audioSyncEnabled & 0x02) == 0))) return; // no audio availeable + #ifdef MIC_LOGGER + // Debugging functions for audio input and sound processing. Comment out the values you want to see + PLOT_PRINT("micReal:"); PLOT_PRINT(micDataReal); PLOT_PRINT("\t"); + PLOT_PRINT("volumeSmth:"); PLOT_PRINT(volumeSmth); PLOT_PRINT("\t"); + //PLOT_PRINT("volumeRaw:"); PLOT_PRINT(volumeRaw); PLOT_PRINT("\t"); + PLOT_PRINT("DC_Level:"); PLOT_PRINT(micLev); PLOT_PRINT("\t"); + //PLOT_PRINT("sampleAgc:"); PLOT_PRINT(sampleAgc); PLOT_PRINT("\t"); + //PLOT_PRINT("sampleAvg:"); PLOT_PRINT(sampleAvg); PLOT_PRINT("\t"); + //PLOT_PRINT("sampleReal:"); PLOT_PRINT(sampleReal); PLOT_PRINT("\t"); + #ifdef ARDUINO_ARCH_ESP32 + //PLOT_PRINT("micIn:"); PLOT_PRINT(micIn); PLOT_PRINT("\t"); + //PLOT_PRINT("sample:"); PLOT_PRINT(sample); PLOT_PRINT("\t"); + //PLOT_PRINT("sampleMax:"); PLOT_PRINT(sampleMax); PLOT_PRINT("\t"); + //PLOT_PRINT("samplePeak:"); PLOT_PRINT((samplePeak!=0) ? 128:0); PLOT_PRINT("\t"); + //PLOT_PRINT("multAgc:"); PLOT_PRINT(multAgc, 4); PLOT_PRINT("\t"); + #endif + PLOT_PRINTLN(); + #endif + + #ifdef FFT_SAMPLING_LOG + #if 0 + for(int i=0; i maxVal) maxVal = fftResult[i]; + if(fftResult[i] < minVal) minVal = fftResult[i]; + } + for(int i = 0; i < NUM_GEQ_CHANNELS; i++) { + PLOT_PRINT(i); PLOT_PRINT(":"); + PLOT_PRINTF("%04ld ", map(fftResult[i], 0, (scaleValuesFromCurrentMaxVal ? maxVal : defaultScalingFromHighValue), (mapValuesToPlotterSpace*i*scalingToHighValue)+0, (mapValuesToPlotterSpace*i*scalingToHighValue)+scalingToHighValue-1)); + } + if(printMaxVal) { + PLOT_PRINTF("maxVal:%04d ", maxVal + (mapValuesToPlotterSpace ? 16*256 : 0)); + } + if(printMinVal) { + PLOT_PRINTF("%04d:minVal ", minVal); // printed with value first, then label, so negative values can be seen in Serial Monitor but don't throw off y axis in Serial Plotter + } + if(mapValuesToPlotterSpace) + PLOT_PRINTF("max:%04d ", (printMaxVal ? 17 : 16)*256); // print line above the maximum value we expect to see on the plotter to avoid autoscaling y axis + else { + PLOT_PRINTF("max:%04d ", 256); + } + PLOT_PRINTLN(); + #endif // FFT_SAMPLING_LOG + } // logAudio() + + +#ifdef ARDUINO_ARCH_ESP32 + ////////////////////// + // Audio Processing // + ////////////////////// + + /* + * A "PI controller" 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 zone (<10% or >90%) - very fast adjustment + */ + 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 + + float lastMultAgc = multAgc; // last multiplier used + float multAgcTemp = multAgc; // new multiplier + float tmpAgc = sampleReal * multAgc; // what-if amplified signal + + float control_error; // "control error" input for PI control + + if (last_soundAgc != soundAgc) control_integrated = 0.0f; // new preset - reset integrator + + // For PI controller, we need to have a constant "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 ((fabsf(sampleReal) < 2.0f) || (sampleMax < 1.0f)) { + // MIC signal is "squelched" - deliver silence + tmpAgc = 0; + // we need to "spin down" the intgrated error buffer + if (fabs(control_integrated) < 0.01f) control_integrated = 0.0f; + else control_integrated *= 0.91f; + } 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.0f) multAgcTemp = 32.0f; + if (multAgcTemp < 1.0f/64.0f) multAgcTemp = 1.0f/64.0f; + + // compute error terms + control_error = multAgcTemp - lastMultAgc; + + if (((multAgcTemp > 0.085f) && (multAgcTemp < 6.5f)) //integrator anti-windup by clamping + && (multAgc*sampleMax < agcZoneStop[AGC_preset])) //integrator ceiling (>140% of max) + control_integrated += control_error * 0.002f * 0.25f; // 2ms = integration time; 0.25 for damping + else + control_integrated *= 0.9f; // 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 energy 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; + } + + // limit amplification again - PI controller sometimes "overshoots" + //multAgcTemp = constrain(multAgcTemp, 0.015625f, 32.0f); // 1/64 < multAgcTemp < 32 + if (multAgcTemp > 32.0f) multAgcTemp = 32.0f; + if (multAgcTemp < 1.0f/64.0f) multAgcTemp = 1.0f/64.0f; + } + + // NOW finally amplify the signal + tmpAgc = sampleReal * multAgcTemp; // apply gain to signal + if (fabsf(sampleReal) < 2.0f) tmpAgc = 0.0f; // apply squelch threshold + //tmpAgc = constrain(tmpAgc, 0, 255); + if (tmpAgc > 255) tmpAgc = 255.0f; // limit to 8bit + if (tmpAgc < 1) tmpAgc = 0.0f; // just to be sure + + // update global vars ONCE - multAgc, sampleAGC, rawSampleAgc + multAgc = multAgcTemp; + rawSampleAgc = 0.8f * tmpAgc + 0.2f * (float)rawSampleAgc; + // update smoothed AGC sample + if (fabsf(tmpAgc) < 1.0f) + sampleAgc = 0.5f * tmpAgc + 0.5f * sampleAgc; // fast path to zero + else + sampleAgc += agcSampleSmooth[AGC_preset] * (tmpAgc - sampleAgc); // smooth path + + sampleAgc = fabsf(sampleAgc); // // make sure we have a positive value + last_soundAgc = soundAgc; + } // agcAvg() + + // post-processing and filtering of MIC sample (micDataReal) from FFTcode() + void getSample() + { + float sampleAdj; // Gain adjusted sample value + float tmpSample; // An interim sample variable used for calculations. + const float weighting = 0.2f; // Exponential filter weighting. Will be adjustable in a future release. + 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; + #else + #ifdef ARDUINO_ARCH_ESP32 + micIn = int(micDataReal); // micDataSm = ((micData * 3) + micData)/4; + #else + // this is the minimal code for reading analog mic input on 8266. + // warning!! Absolutely experimental code. Audio on 8266 is still not working. Expects a million follow-on problems. + static unsigned long lastAnalogTime = 0; + static float lastAnalogValue = 0.0f; + if (millis() - lastAnalogTime > 20) { + micDataReal = analogRead(A0); // read one sample with 10bit resolution. This is a dirty hack, supporting volumereactive effects only. + lastAnalogTime = millis(); + lastAnalogValue = micDataReal; + yield(); + } else micDataReal = lastAnalogValue; + micIn = int(micDataReal); + #endif + #endif + + micLev += (micDataReal-micLev) / 12288.0f; + if (micIn < micLev) micLev = ((micLev * 31.0f) + micDataReal) / 32.0f; // 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 = fabsf(micDataReal - micLev); + expAdjF = (weighting * micInNoDC + (1.0f-weighting) * expAdjF); + expAdjF = fabsf(expAdjF); // Now (!) take the absolute value + + expAdjF = (expAdjF <= soundSquelch) ? 0.0f : expAdjF; // simple noise gate + if ((soundSquelch == 0) && (expAdjF < 0.25f)) expAdjF = 0.0f; // do something meaningfull when "squelch = 0" + + tmpSample = expAdjF; + micIn = abs(micIn); // And get the absolute value of each sample + + sampleAdj = tmpSample * sampleGain * inputLevel / 5120.0f /* /40 /128 */ + tmpSample / 16.0f; // Adjust the gain. with inputLevel adjustment + sampleReal = tmpSample; + + sampleAdj = fmax(fmin(sampleAdj, 255.0f), 0.0f); // Question: why are we limiting the value to 8 bits ??? + sampleRaw = (int16_t)sampleAdj; // ONLY update sample ONCE!!!! + + // keep "peak" sample, but decay value if current sample is below peak + if ((sampleMax < sampleReal) && (sampleReal > 0.5f)) { + sampleMax += 0.5f * (sampleReal - sampleMax); // new peak - with some filtering + // another simple way to detect samplePeak - cannot detect beats, but reacts on peak volume + if (((binNum < 12) || ((maxVol < 1))) && (millis() - timeOfPeak > 80) && (sampleAvg > 1)) { + samplePeak = true; + timeOfPeak = millis(); + udpSamplePeak = true; + } + } else { + if ((multAgc*sampleMax > agcZoneStop[AGC_preset]) && (soundAgc > 0)) + sampleMax += 0.5f * (sampleReal - sampleMax); // over AGC Zone - get back quickly + else + sampleMax *= agcSampleDecay[AGC_preset]; // signal to zero --> 5-8sec + } + if (sampleMax < 0.5f) sampleMax = 0.0f; + + sampleAvg = ((sampleAvg * 15.0f) + sampleAdj) / 16.0f; // Smooth it out over the last 16 samples. + sampleAvg = fabsf(sampleAvg); // make sure we have a positive value + } // getSample() + +#endif + + /* Limits the dynamics of volumeSmth (= sampleAvg or sampleAgc). + * does not affect FFTResult[] or volumeRaw ( = sample or rawSampleAgc) + */ + // effects: Gravimeter, Gravcenter, Gravcentric, Noisefire, Plasmoid, Freqpixels, Freqwave, Gravfreq, (2D Swirl, 2D Waverly) + void limitSampleDynamics(void) { + const float bigChange = 196.0f; // just a representative number - a large, expected sample value + static unsigned long last_time = 0; + static float last_volumeSmth = 0.0f; + + if (limiterOn == false) return; + + long delta_time = millis() - last_time; + delta_time = constrain(delta_time , 1, 1000); // below 1ms -> 1ms; above 1sec -> sily lil hick-up + float deltaSample = volumeSmth - last_volumeSmth; + + if (attackTime > 0) { // user has defined attack time > 0 + float maxAttack = bigChange * float(delta_time) / float(attackTime); + if (deltaSample > maxAttack) deltaSample = maxAttack; + } + if (decayTime > 0) { // user has defined decay time > 0 + float maxDecay = - bigChange * float(delta_time) / float(decayTime); + if (deltaSample < maxDecay) deltaSample = maxDecay; + } + + volumeSmth = last_volumeSmth + deltaSample; + + last_volumeSmth = volumeSmth; + last_time = millis(); + } + + + ////////////////////// + // UDP Sound Sync // + ////////////////////// + + // try to establish UDP sound sync connection + void connectUDPSoundSync(void) { + // This function tries to establish a UDP sync connection if needed + // necessary as we also want to transmit in "AP Mode", but the standard "connected()" callback only reacts on STA connection + static unsigned long last_connection_attempt = 0; + + if ((audioSyncPort <= 0) || ((audioSyncEnabled & 0x03) == 0)) return; // Sound Sync not enabled + if (udpSyncConnected) return; // already connected + if (!(apActive || interfacesInited)) return; // neither AP nor other connections availeable + if (millis() - last_connection_attempt < 15000) return; // only try once in 15 seconds + if (updateIsRunning) return; + + // if we arrive here, we need a UDP connection but don't have one + last_connection_attempt = millis(); + connected(); // try to start UDP + } + +#ifdef ARDUINO_ARCH_ESP32 + void transmitAudioData() + { + //DEBUGSR_PRINTLN("Transmitting UDP Mic Packet"); + + audioSyncPacket transmitData; + memset(reinterpret_cast(&transmitData), 0, sizeof(transmitData)); // make sure that the packet - including "invisible" padding bytes added by the compiler - is fully initialized + + strncpy_P(transmitData.header, PSTR(UDP_SYNC_HEADER), 6); + // transmit samples that were not modified by limitSampleDynamics() + transmitData.sampleRaw = (soundAgc) ? rawSampleAgc: sampleRaw; + transmitData.sampleSmth = (soundAgc) ? sampleAgc : sampleAvg; + transmitData.samplePeak = udpSamplePeak ? 1:0; + udpSamplePeak = false; // Reset udpSamplePeak after we've transmitted it + + for (int i = 0; i < NUM_GEQ_CHANNELS; i++) { + transmitData.fftResult[i] = (uint8_t)constrain(fftResult[i], 0, 254); + } + + transmitData.FFT_Magnitude = my_magnitude; + transmitData.FFT_MajorPeak = FFT_MajorPeak; + +#ifndef WLED_DISABLE_ESPNOW + if (useESPNowSync && statusESPNow == ESP_NOW_STATE_ON) { + EspNowPartialPacket buffer = {{'W','L','E','D'}, 0, 1, {0}}; + //DEBUGSR_PRINTLN(F("ESP-NOW Sending audio packet.")); + size_t packetSize = sizeof(EspNowPartialPacket) - sizeof(EspNowPartialPacket::data) + sizeof(transmitData); + memcpy(buffer.data, &transmitData, sizeof(transmitData)); + quickEspNow.send(ESPNOW_BROADCAST_ADDRESS, reinterpret_cast(&buffer), packetSize); + } +#endif + + if (udpSyncConnected && fftUdp.beginMulticastPacket() != 0) { // beginMulticastPacket returns 0 in case of error + fftUdp.write(reinterpret_cast(&transmitData), sizeof(transmitData)); + fftUdp.endPacket(); + } + return; + } // transmitAudioData() + +#endif + + static inline bool isValidUdpSyncVersion(const char *header) { + return strncmp_P(header, UDP_SYNC_HEADER, 6) == 0; + } + static inline bool isValidUdpSyncVersion_v1(const char *header) { + return strncmp_P(header, UDP_SYNC_HEADER_v1, 6) == 0; + } + + void decodeAudioData(int packetSize, uint8_t *fftBuff) { + audioSyncPacket receivedPacket; + memset(&receivedPacket, 0, sizeof(receivedPacket)); // start clean + memcpy(&receivedPacket, fftBuff, min((unsigned)packetSize, (unsigned)sizeof(receivedPacket))); // don't violate alignment - thanks @willmmiles# + + // update samples for effects + volumeSmth = fmaxf(receivedPacket.sampleSmth, 0.0f); + volumeRaw = fmaxf(receivedPacket.sampleRaw, 0.0f); +#ifdef ARDUINO_ARCH_ESP32 + // update internal samples + sampleRaw = volumeRaw; + sampleAvg = volumeSmth; + rawSampleAgc = volumeRaw; + sampleAgc = volumeSmth; + multAgc = 1.0f; +#endif + // Only change samplePeak IF it's currently false. + // If it's true already, then the animation still needs to respond. + autoResetPeak(); + if (!samplePeak) { + samplePeak = receivedPacket.samplePeak > 0; + if (samplePeak) timeOfPeak = millis(); + } + //These values are only computed by ESP32 + for (int i = 0; i < NUM_GEQ_CHANNELS; i++) fftResult[i] = receivedPacket.fftResult[i]; + my_magnitude = fmaxf(receivedPacket.FFT_Magnitude, 0.0f); + FFT_Magnitude = my_magnitude; + FFT_MajorPeak = constrain(receivedPacket.FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects + } + + void decodeAudioData_v1(int packetSize, uint8_t *fftBuff) { + audioSyncPacket_v1 *receivedPacket = reinterpret_cast(fftBuff); + // update samples for effects + volumeSmth = fmaxf(receivedPacket->sampleAgc, 0.0f); + volumeRaw = volumeSmth; // V1 format does not have "raw" AGC sample +#ifdef ARDUINO_ARCH_ESP32 + // update internal samples + sampleRaw = fmaxf(receivedPacket->sampleRaw, 0.0f); + sampleAvg = fmaxf(receivedPacket->sampleAvg, 0.0f);; + sampleAgc = volumeSmth; + rawSampleAgc = volumeRaw; + multAgc = 1.0f; +#endif + // Only change samplePeak IF it's currently false. + // If it's true already, then the animation still needs to respond. + autoResetPeak(); + if (!samplePeak) { + samplePeak = receivedPacket->samplePeak > 0; + if (samplePeak) timeOfPeak = millis(); + } + //These values are only available on the ESP32 + for (int i = 0; i < NUM_GEQ_CHANNELS; i++) fftResult[i] = receivedPacket->fftResult[i]; + my_magnitude = fmaxf(receivedPacket->FFT_Magnitude, 0.0f); + FFT_Magnitude = my_magnitude; + FFT_MajorPeak = constrain(receivedPacket->FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects + } + + bool receiveAudioData() // check & process new data. return TRUE in case that new audio data was received. + { + if (!udpSyncConnected) return false; + bool haveFreshData = false; + + size_t packetSize = fftUdp.parsePacket(); +#ifdef ARDUINO_ARCH_ESP32 + if ((packetSize > 0) && ((packetSize < 5) || (packetSize > UDPSOUND_MAX_PACKET))) fftUdp.flush(); // discard invalid packets (too small or too big) - only works on esp32 +#endif + if ((packetSize > 5) && (packetSize <= UDPSOUND_MAX_PACKET)) { + //DEBUGSR_PRINTLN("Received UDP Sync Packet"); + uint8_t fftBuff[UDPSOUND_MAX_PACKET+1] = { 0 }; // fixed-size buffer for receiving (stack), to avoid heap fragmentation caused by variable sized arrays + fftUdp.read(fftBuff, packetSize); + + // VERIFY THAT THIS IS A COMPATIBLE PACKET + if (packetSize == sizeof(audioSyncPacket) && (isValidUdpSyncVersion((const char *)fftBuff))) { + decodeAudioData(packetSize, fftBuff); + //DEBUGSR_PRINTLN("Finished parsing UDP Sync Packet v2"); + haveFreshData = true; + receivedFormat = 2; + } else { + if (packetSize == sizeof(audioSyncPacket_v1) && (isValidUdpSyncVersion_v1((const char *)fftBuff))) { + decodeAudioData_v1(packetSize, fftBuff); + //DEBUGSR_PRINTLN("Finished parsing UDP Sync Packet v1"); + haveFreshData = true; + receivedFormat = 1; + } else receivedFormat = 0; // unknown format + } + } + return haveFreshData; + } + + + ////////////////////// + // usermod functions// + ////////////////////// + + public: + //Functions called by WLED or other usermods + + /* + * setup() is called once at boot. WiFi is not yet connected at this point. + * You can use it to initialize variables, sensors or similar. + * It is called *AFTER* readFromConfig() + */ + void setup() override + { + disableSoundProcessing = true; // just to be sure + if (!initDone) { + // 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 = 8; + 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) + um_data->u_type[0] = UMT_FLOAT; + um_data->u_data[1] = &volumeRaw; // used (New) + um_data->u_type[1] = UMT_UINT16; + um_data->u_data[2] = fftResult; //*used (Blurz, DJ Light, Noisemove, GEQ_base, 2D Funky Plank, Akemi) + um_data->u_type[2] = UMT_BYTE_ARR; + um_data->u_data[3] = &samplePeak; //*used (Puddlepeak, Ripplepeak, Waterfall) + um_data->u_type[3] = UMT_BYTE; + um_data->u_data[4] = &FFT_MajorPeak; //*used (Ripplepeak, Freqmap, Freqmatrix, Freqpixels, Freqwave, Gravfreq, Rocktaves, Waterfall) + um_data->u_type[4] = UMT_FLOAT; + um_data->u_data[5] = &my_magnitude; // used (New) + um_data->u_type[5] = UMT_FLOAT; + um_data->u_data[6] = &maxVol; // assigned in effect function from UI element!!! (Puddlepeak, Ripplepeak, Waterfall) + um_data->u_type[6] = UMT_BYTE; + um_data->u_data[7] = &binNum; // assigned in effect function from UI element!!! (Puddlepeak, Ripplepeak, Waterfall) + um_data->u_type[7] = UMT_BYTE; + } + + +#ifdef ARDUINO_ARCH_ESP32 + + // Reset I2S peripheral for good measure + i2s_driver_uninstall(I2S_NUM_0); // E (696) I2S: i2s_driver_uninstall(2006): I2S port 0 has not installed + #if !defined(CONFIG_IDF_TARGET_ESP32C3) + delay(100); + periph_module_reset(PERIPH_I2S0_MODULE); // not possible on -C3 + #endif + delay(100); // Give that poor microphone some time to setup. + + useBandPassFilter = false; + + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + if ((i2sckPin == I2S_PIN_NO_CHANGE) && (i2ssdPin >= 0) && (i2swsPin >= 0) && ((dmType == 1) || (dmType == 4)) ) dmType = 5; // dummy user support: SCK == -1 --means--> PDM microphone + #endif + + switch (dmType) { + #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) + // stub cases for not-yet-supported I2S modes on other ESP32 chips + case 0: //ADC analog + #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) + case 5: //PDM Microphone + #endif + #endif + case 1: + DEBUGSR_PRINT(F("AR: Generic I2S Microphone - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT)); + audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE); + delay(100); + if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin); + break; + case 2: + DEBUGSR_PRINTLN(F("AR: ES7243 Microphone (right channel only).")); + audioSource = new ES7243(SAMPLE_RATE, BLOCK_SIZE); + delay(100); + if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + break; + case 3: + DEBUGSR_PRINT(F("AR: SPH0645 Microphone - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT)); + audioSource = new SPH0654(SAMPLE_RATE, BLOCK_SIZE); + delay(100); + audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin); + break; + case 4: + DEBUGSR_PRINT(F("AR: Generic I2S Microphone with Master Clock - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT)); + audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f/24.0f); + delay(100); + if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + break; + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + case 5: + DEBUGSR_PRINT(F("AR: I2S PDM Microphone - ")); DEBUGSR_PRINTLN(F(I2S_PDM_MIC_CHANNEL_TEXT)); + audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f/4.0f); + useBandPassFilter = true; // this reduces the noise floor on SPM1423 from 5% Vpp (~380) down to 0.05% Vpp (~5) + delay(100); + if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin); + break; + #endif + case 6: + DEBUGSR_PRINTLN(F("AR: ES8388 Source")); + audioSource = new ES8388Source(SAMPLE_RATE, BLOCK_SIZE); + delay(100); + if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); + break; + + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) + // ADC over I2S is only possible on "classic" ESP32 + case 0: + default: + DEBUGSR_PRINTLN(F("AR: Analog Microphone (left channel only).")); + audioSource = new I2SAdcSource(SAMPLE_RATE, BLOCK_SIZE); + delay(100); + useBandPassFilter = true; // PDM bandpass filter seems to help for bad quality analog + if (audioSource) audioSource->initialize(audioPin); + break; + #endif + } + delay(250); // give microphone enough time to initialise + + if (!audioSource) enabled = false; // audio failed to initialise +#endif + if (enabled) onUpdateBegin(false); // create FFT task, and initialize network + if (enabled) disableSoundProcessing = false; // all good - enable audio processing +#ifdef ARDUINO_ARCH_ESP32 + if (FFT_Task == nullptr) enabled = false; // FFT task creation failed + if ((!audioSource) || (!audioSource->isInitialized())) { + // audio source failed to initialize. Still stay "enabled", as there might be input arriving via UDP Sound Sync + #ifdef WLED_DEBUG + DEBUG_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings.")); + #else + DEBUGSR_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings.")); + #endif + disableSoundProcessing = true; + } +#endif + if (enabled) connectUDPSoundSync(); + if (enabled && addPalettes) createAudioPalettes(); + initDone = true; + } + + + /* + * connected() is called every time the WiFi is (re)connected + * Use it to initialize network interfaces + */ + void connected() override + { + if (udpSyncConnected) { // clean-up: if open, close old UDP sync connection + udpSyncConnected = false; + fftUdp.stop(); + } + + if (audioSyncPort > 0 && (audioSyncEnabled & 0x03)) { + #ifdef ARDUINO_ARCH_ESP32 + udpSyncConnected = fftUdp.beginMulticast(IPAddress(239, 0, 0, 1), audioSyncPort); + #else + udpSyncConnected = fftUdp.beginMulticast(WiFi.localIP(), IPAddress(239, 0, 0, 1), audioSyncPort); + #endif + } + } + + + /* + * 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() override + { + static unsigned long lastUMRun = millis(); + + if (!enabled) { + disableSoundProcessing = true; // keep processing suspended (FFT task) + lastUMRun = millis(); // update time keeping + return; + } + // We cannot wait indefinitely before processing audio data + if (WS2812FX::isUpdating() && (millis() - lastUMRun < 2)) return; // be nice, but not too nice + + // suspend local sound processing when "real time mode" is active (E131, UDP, ADALIGHT, ARTNET) + if ( (realtimeOverride == REALTIME_OVERRIDE_NONE) // please add other overrides here if needed + &&( (realtimeMode == REALTIME_MODE_GENERIC) + ||(realtimeMode == REALTIME_MODE_E131) + ||(realtimeMode == REALTIME_MODE_UDP) + ||(realtimeMode == REALTIME_MODE_ADALIGHT) + ||(realtimeMode == REALTIME_MODE_ARTNET) ) ) // please add other modes here if needed + { + #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG) + if ((disableSoundProcessing == false) && (audioSyncEnabled == 0)) { // we just switched to "disabled" + DEBUG_PRINTLN(F("[AR userLoop] realtime mode active - audio processing suspended.")); + DEBUG_PRINTF_P(PSTR(" RealtimeMode = %d; RealtimeOverride = %d\n"), int(realtimeMode), int(realtimeOverride)); + } + #endif + disableSoundProcessing = true; + } else { + #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG) + if ((disableSoundProcessing == true) && (audioSyncEnabled == 0) && audioSource->isInitialized()) { // we just switched to "enabled" + DEBUG_PRINTLN(F("[AR userLoop] realtime mode ended - audio processing resumed.")); + DEBUG_PRINTF_P(PSTR(" RealtimeMode = %d; RealtimeOverride = %d\n"), int(realtimeMode), int(realtimeOverride)); + } + #endif + if ((disableSoundProcessing == true) && (audioSyncEnabled == 0)) lastUMRun = millis(); // just left "realtime mode" - update timekeeping + disableSoundProcessing = false; + } + + if (audioSyncEnabled & 0x02) disableSoundProcessing = true; // make sure everything is disabled IF in audio Receive mode + if (audioSyncEnabled & 0x01) disableSoundProcessing = false; // keep running audio IF we're in audio Transmit mode +#ifdef ARDUINO_ARCH_ESP32 + if (!audioSource->isInitialized()) disableSoundProcessing = true; // no audio source + + + // Only run the sampling code IF we're not in Receive mode or realtime mode + if (!(audioSyncEnabled & 0x02) && !disableSoundProcessing) { + if (soundAgc > AGC_NUM_PRESETS) soundAgc = 0; // make sure that AGC preset is valid (to avoid array bounds violation) + + unsigned long t_now = millis(); // remember current time + int userloopDelay = int(t_now - lastUMRun); + if (lastUMRun == 0) userloopDelay=0; // startup - don't have valid data from last run. + + #ifdef WLED_DEBUG + // complain when audio userloop has been delayed for long time. Currently we need userloop running between 500 and 1500 times per second. + // softhack007 disabled temporarily - avoid serial console spam with MANY leds and low FPS + //if ((userloopDelay > 65) && !disableSoundProcessing && (audioSyncEnabled == 0)) { + // DEBUG_PRINTF_P(PSTR("[AR userLoop] hiccup detected -> was inactive for last %d millis!\n"), userloopDelay); + //} + #endif + + // run filters, and repeat in case of loop delays (hick-up compensation) + if (userloopDelay <2) userloopDelay = 0; // minor glitch, no problem + if (userloopDelay >200) userloopDelay = 200; // limit number of filter re-runs + do { + getSample(); // run microphone sampling filters + agcAvg(t_now - userloopDelay); // Calculated the PI adjusted value as sampleAvg + userloopDelay -= 2; // advance "simulated time" by 2ms + } while (userloopDelay > 0); + lastUMRun = t_now; // update time keeping + + // update samples for effects (raw, smooth) + volumeSmth = (soundAgc) ? sampleAgc : sampleAvg; + volumeRaw = (soundAgc) ? rawSampleAgc: sampleRaw; + // update FFTMagnitude, taking into account AGC amplification + my_magnitude = FFT_Magnitude; // / 16.0f, 8.0f, 4.0f done in effects + if (soundAgc) my_magnitude *= multAgc; + if (volumeSmth < 1 ) my_magnitude = 0.001f; // noise gate closed - mute + + limitSampleDynamics(); + } // if (!disableSoundProcessing) +#endif + + autoResetPeak(); // auto-reset sample peak after strip minShowDelay + if (!udpSyncConnected) udpSamplePeak = false; // reset UDP samplePeak while UDP is unconnected + + connectUDPSoundSync(); // ensure we have a connection - if needed + + // UDP Microphone Sync - receive mode + if ((audioSyncEnabled & 0x02) && udpSyncConnected) { + // Only run the audio listener code if we're in Receive mode + static float syncVolumeSmth = 0; + bool have_new_sample = false; + if (millis() - lastTime > delayMs) { + have_new_sample = receiveAudioData(); + if (have_new_sample) last_UDPTime = millis(); +#ifdef ARDUINO_ARCH_ESP32 + else fftUdp.flush(); // Flush udp input buffers if we haven't read it - avoids hickups in receive mode. Does not work on 8266. +#endif + lastTime = millis(); + } + if (have_new_sample) syncVolumeSmth = volumeSmth; // remember received sample + else volumeSmth = syncVolumeSmth; // restore originally received sample for next run of dynamics limiter + limitSampleDynamics(); // run dynamics limiter on received volumeSmth, to hide jumps and hickups + } + + #if defined(MIC_LOGGER) || defined(MIC_SAMPLING_LOG) || defined(FFT_SAMPLING_LOG) + static unsigned long lastMicLoggerTime = 0; + if (millis()-lastMicLoggerTime > 20) { + lastMicLoggerTime = millis(); + logAudio(); + } + #endif + + // Info Page: keep max sample from last 5 seconds +#ifdef ARDUINO_ARCH_ESP32 + if ((millis() - sampleMaxTimer) > CYCLE_SAMPLEMAX) { + sampleMaxTimer = millis(); + maxSample5sec = (0.15f * maxSample5sec) + 0.85f *((soundAgc) ? sampleAgc : sampleAvg); // reset, and start with some smoothing + if (sampleAvg < 1) maxSample5sec = 0; // noise gate + } else { + if ((sampleAvg >= 1)) maxSample5sec = fmaxf(maxSample5sec, (soundAgc) ? rawSampleAgc : sampleRaw); // follow maximum volume + } +#else // similar functionality for 8266 receive only - use VolumeSmth instead of raw sample data + if ((millis() - sampleMaxTimer) > CYCLE_SAMPLEMAX) { + sampleMaxTimer = millis(); + maxSample5sec = (0.15 * maxSample5sec) + 0.85 * volumeSmth; // reset, and start with some smoothing + if (volumeSmth < 1.0f) maxSample5sec = 0; // noise gate + if (maxSample5sec < 0.0f) maxSample5sec = 0; // avoid negative values + } else { + if (volumeSmth >= 1.0f) maxSample5sec = fmaxf(maxSample5sec, volumeRaw); // follow maximum volume + } +#endif + +#ifdef ARDUINO_ARCH_ESP32 + //UDP Microphone Sync - transmit mode + if ((audioSyncEnabled & 0x01) && (millis() - lastTime > 20)) { + // Only run the transmit code IF we're in Transmit mode + transmitAudioData(); + lastTime = millis(); + } +#endif + + fillAudioPalettes(); + } + + + bool getUMData(um_data_t **data) override + { + if (!data || !enabled) return false; // no pointer provided by caller or not enabled -> exit + *data = um_data; + return true; + } + +#ifdef ARDUINO_ARCH_ESP32 + void onUpdateBegin(bool init) override + { +#ifdef WLED_DEBUG + fftTime = sampleTime = 0; +#endif + // gracefully suspend FFT task (if running) + disableSoundProcessing = true; + + // reset sound data + micDataReal = 0.0f; + volumeRaw = 0; volumeSmth = 0.0f; + sampleAgc = 0.0f; sampleAvg = 0.0f; + sampleRaw = 0; rawSampleAgc = 0.0f; + my_magnitude = 0.0f; FFT_Magnitude = 0.0f; FFT_MajorPeak = 1.0f; + multAgc = 1.0f; + // reset FFT data + memset(fftCalc, 0, sizeof(fftCalc)); + memset(fftAvg, 0, sizeof(fftAvg)); + memset(fftResult, 0, sizeof(fftResult)); + for(int i=(init?0:1); i don't process audio + updateIsRunning = init; + } +#endif + +#ifdef ARDUINO_ARCH_ESP32 + /** + * handleButton() can be used to override default button behaviour. Returning true + * will prevent button working in a default way. + */ + bool handleButton(uint8_t b) override { + yield(); + // crude way of determining if audio input is analog + // better would be for AudioSource to implement getType() + if (enabled + && dmType == 0 && audioPin>=0 + && (buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED) + ) { + return true; + } + return false; + } + +#endif + //////////////////////////// + // Settings and Info Page // + //////////////////////////// + + /* + * 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) override + { +#ifdef ARDUINO_ARCH_ESP32 + char myStringBuffer[16]; // buffer for snprintf() - not used yet on 8266 +#endif + JsonObject user = root["u"]; + if (user.isNull()) user = root.createNestedObject("u"); + + JsonArray infoArr = user.createNestedArray(FPSTR(_name)); + + String uiDomString = F(""); + infoArr.add(uiDomString); + + if (enabled) { +#ifdef ARDUINO_ARCH_ESP32 + // Input Level Slider + if (disableSoundProcessing == false) { // only show slider when audio processing is running + if (soundAgc > 0) { + infoArr = user.createNestedArray(F("GEQ Input Level")); // if AGC is on, this slider only affects fftResult[] frequencies + } else { + infoArr = user.createNestedArray(F("Audio Input Level")); + } + uiDomString = F("
"); // + infoArr.add(uiDomString); + } +#endif + // The following can be used for troubleshooting user errors and is so not enclosed in #ifdef WLED_DEBUG + + // current Audio input + infoArr = user.createNestedArray(F("Audio Source")); + if (audioSyncEnabled & 0x02) { + // UDP sound sync - receive mode + infoArr.add(F("UDP sound sync")); + if (udpSyncConnected) { + if (millis() - last_UDPTime < 2500) + infoArr.add(F(" - receiving")); + else + infoArr.add(F(" - idle")); + } else { + infoArr.add(F(" - no connection")); + } +#ifndef ARDUINO_ARCH_ESP32 // substitute for 8266 + } else { + infoArr.add(F("sound sync Off")); + } +#else // ESP32 only + } else { + // Analog or I2S digital input + if (audioSource && (audioSource->isInitialized())) { + // audio source successfully configured + if (audioSource->getType() == AudioSource::Type_I2SAdc) { + infoArr.add(F("ADC analog")); + } else { + infoArr.add(F("I2S digital")); + } + // input level or "silence" + if (maxSample5sec > 1.0f) { + float my_usage = 100.0f * (maxSample5sec / 255.0f); + snprintf_P(myStringBuffer, 15, PSTR(" - peak %3d%%"), int(my_usage)); + infoArr.add(myStringBuffer); + } else { + infoArr.add(F(" - quiet")); + } + } else { + // error during audio source setup + infoArr.add(F("not initialized")); + infoArr.add(F(" - check pin settings")); + } + } + + // Sound processing (FFT and input filters) + infoArr = user.createNestedArray(F("Sound Processing")); + if (audioSource && (disableSoundProcessing == false)) { + infoArr.add(F("running")); + } else { + infoArr.add(F("suspended")); + } + + // AGC or manual Gain + if ((soundAgc==0) && (disableSoundProcessing == false) && !(audioSyncEnabled & 0x02)) { + infoArr = user.createNestedArray(F("Manual Gain")); + float myGain = ((float)sampleGain/40.0f * (float)inputLevel/128.0f) + 1.0f/16.0f; // non-AGC gain from presets + infoArr.add(roundf(myGain*100.0f) / 100.0f); + infoArr.add("x"); + } + if (soundAgc && (disableSoundProcessing == false) && !(audioSyncEnabled & 0x02)) { + infoArr = user.createNestedArray(F("AGC Gain")); + infoArr.add(roundf(multAgc*100.0f) / 100.0f); + infoArr.add("x"); + } +#endif + // UDP Sound Sync status + infoArr = user.createNestedArray(F("UDP Sound Sync")); + if (audioSyncEnabled) { + if (audioSyncEnabled & 0x01) { + infoArr.add(F("send mode")); + if ((udpSyncConnected) && (millis() - lastTime < 2500)) infoArr.add(F(" v2")); + } else if (audioSyncEnabled & 0x02) { + infoArr.add(F("receive mode")); + } + } else + infoArr.add("off"); + if (audioSyncEnabled && !udpSyncConnected) infoArr.add(" (unconnected)"); + if (audioSyncEnabled && udpSyncConnected && (millis() - last_UDPTime < 2500)) { + if (receivedFormat == 1) infoArr.add(F(" v1")); + if (receivedFormat == 2) infoArr.add(F(" v2")); + } + + #if defined(WLED_DEBUG) || defined(SR_DEBUG) + #ifdef ARDUINO_ARCH_ESP32 + infoArr = user.createNestedArray(F("Sampling time")); + infoArr.add(float(sampleTime)/100.0f); + infoArr.add(" ms"); + + infoArr = user.createNestedArray(F("FFT time")); + infoArr.add(float(fftTime)/100.0f); + if ((fftTime/100) >= FFT_MIN_CYCLE) // FFT time over budget -> I2S buffer will overflow + infoArr.add("! ms"); + else if ((fftTime/80 + sampleTime/80) >= FFT_MIN_CYCLE) // FFT time >75% of budget -> risk of instability + infoArr.add(" ms!"); + else + infoArr.add(" ms"); + + DEBUGSR_PRINTF("AR Sampling time: %5.2f ms\n", float(sampleTime)/100.0f); + DEBUGSR_PRINTF("AR FFT time : %5.2f ms\n", float(fftTime)/100.0f); + #endif + #endif + } + } + + + /* + * 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) override + { + if (!initDone) return; // prevent crash on boot applyPreset() + JsonObject usermod = root[FPSTR(_name)]; + if (usermod.isNull()) { + usermod = root.createNestedObject(FPSTR(_name)); + } + usermod["on"] = enabled; + } + + + /* + * 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) override + { + if (!initDone) return; // prevent crash on boot applyPreset() + bool prevEnabled = enabled; + JsonObject usermod = root[FPSTR(_name)]; + if (!usermod.isNull()) { + if (usermod[FPSTR(_enabled)].is()) { + enabled = usermod[FPSTR(_enabled)].as(); + if (prevEnabled != enabled) onUpdateBegin(!enabled); + if (addPalettes) { + // add/remove custom/audioreactive palettes + if (prevEnabled && !enabled) removeAudioPalettes(); + if (!prevEnabled && enabled) createAudioPalettes(); + } + } +#ifdef ARDUINO_ARCH_ESP32 + if (usermod[FPSTR(_inputLvl)].is()) { + inputLevel = min(255,max(0,usermod[FPSTR(_inputLvl)].as())); + } +#endif + } + if (root.containsKey(F("rmcpal")) && root[F("rmcpal")].as()) { + // handle removal of custom palettes from JSON call so we don't break things + removeAudioPalettes(); + } + } + + void onStateChange(uint8_t callMode) override { + if (initDone && enabled && addPalettes && palettes==0 && WS2812FX::customPalettes.size()<10) { + // if palettes were removed during JSON call re-add them + createAudioPalettes(); + } + } + + /* + * 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) override + { + JsonObject top = root.createNestedObject(FPSTR(_name)); + top[FPSTR(_enabled)] = enabled; + top[FPSTR(_addPalettes)] = addPalettes; + +#ifdef ARDUINO_ARCH_ESP32 + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) + JsonObject amic = top.createNestedObject(FPSTR(_analogmic)); + amic["pin"] = audioPin; + #endif + + JsonObject dmic = top.createNestedObject(FPSTR(_digitalmic)); + dmic["type"] = dmType; + JsonArray pinArray = dmic.createNestedArray("pin"); + pinArray.add(i2ssdPin); + pinArray.add(i2swsPin); + pinArray.add(i2sckPin); + pinArray.add(mclkPin); + + JsonObject cfg = top.createNestedObject(FPSTR(_config)); + cfg[F("squelch")] = soundSquelch; + cfg[F("gain")] = sampleGain; + cfg[F("AGC")] = soundAgc; + + JsonObject freqScale = top.createNestedObject(FPSTR(_frequency)); + freqScale[F("scale")] = FFTScalingMode; +#endif + + JsonObject dynLim = top.createNestedObject(FPSTR(_dynamics)); + dynLim[F("limiter")] = limiterOn; + dynLim[F("rise")] = attackTime; + dynLim[F("fall")] = decayTime; + + JsonObject sync = top.createNestedObject("sync"); + sync["port"] = audioSyncPort; + sync["mode"] = audioSyncEnabled; + } + + + /* + * 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) override + { + JsonObject top = root[FPSTR(_name)]; + bool configComplete = !top.isNull(); + bool oldEnabled = enabled; + bool oldAddPalettes = addPalettes; + + configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled); + configComplete &= getJsonValue(top[FPSTR(_addPalettes)], addPalettes); + +#ifdef ARDUINO_ARCH_ESP32 + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) + configComplete &= getJsonValue(top[FPSTR(_analogmic)]["pin"], audioPin); + #else + audioPin = -1; // MCU does not support analog mic + #endif + + configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["type"], dmType); + #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) + if (dmType == 0) dmType = SR_DMTYPE; // MCU does not support analog + #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) + if (dmType == 5) dmType = SR_DMTYPE; // MCU does not support PDM + #endif + #endif + + configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][0], i2ssdPin); + configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][1], i2swsPin); + configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][2], i2sckPin); + configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][3], mclkPin); + + configComplete &= getJsonValue(top[FPSTR(_config)][F("squelch")], soundSquelch); + configComplete &= getJsonValue(top[FPSTR(_config)][F("gain")], sampleGain); + configComplete &= getJsonValue(top[FPSTR(_config)][F("AGC")], soundAgc); + + configComplete &= getJsonValue(top[FPSTR(_frequency)][F("scale")], FFTScalingMode); + + configComplete &= getJsonValue(top[FPSTR(_dynamics)][F("limiter")], limiterOn); + configComplete &= getJsonValue(top[FPSTR(_dynamics)][F("rise")], attackTime); + configComplete &= getJsonValue(top[FPSTR(_dynamics)][F("fall")], decayTime); +#endif + configComplete &= getJsonValue(top["sync"]["port"], audioSyncPort); + configComplete &= getJsonValue(top["sync"]["mode"], audioSyncEnabled); + + if (initDone) { + // add/remove custom/audioreactive palettes + if ((oldAddPalettes && !addPalettes) || (oldAddPalettes && !enabled)) removeAudioPalettes(); + if ((addPalettes && !oldAddPalettes && enabled) || (addPalettes && !oldEnabled && enabled)) createAudioPalettes(); + } // else setup() will create palettes + return configComplete; + } + + + void appendConfigData() override + { +#ifdef ARDUINO_ARCH_ESP32 + oappend(SET_F("dd=addDropdown('AudioReactive','digitalmic:type');")); + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) + oappend(SET_F("addOption(dd,'Generic Analog',0);")); + #endif + oappend(SET_F("addOption(dd,'Generic I2S',1);")); + oappend(SET_F("addOption(dd,'ES7243',2);")); + oappend(SET_F("addOption(dd,'SPH0654',3);")); + oappend(SET_F("addOption(dd,'Generic I2S with Mclk',4);")); + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) + oappend(SET_F("addOption(dd,'Generic I2S PDM',5);")); + #endif + oappend(SET_F("addOption(dd,'ES8388',6);")); + + oappend(SET_F("dd=addDropdown('AudioReactive','config:AGC');")); + oappend(SET_F("addOption(dd,'Off',0);")); + oappend(SET_F("addOption(dd,'Normal',1);")); + oappend(SET_F("addOption(dd,'Vivid',2);")); + oappend(SET_F("addOption(dd,'Lazy',3);")); + + oappend(SET_F("dd=addDropdown('AudioReactive','dynamics:limiter');")); + oappend(SET_F("addOption(dd,'Off',0);")); + oappend(SET_F("addOption(dd,'On',1);")); + oappend(SET_F("addInfo('AudioReactive:dynamics:limiter',0,' On ');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('AudioReactive:dynamics:rise',1,'ms (♪ effects only)');")); + oappend(SET_F("addInfo('AudioReactive:dynamics:fall',1,'ms (♪ effects only)');")); + + oappend(SET_F("dd=addDropdown('AudioReactive','frequency:scale');")); + oappend(SET_F("addOption(dd,'None',0);")); + oappend(SET_F("addOption(dd,'Linear (Amplitude)',2);")); + oappend(SET_F("addOption(dd,'Square Root (Energy)',3);")); + oappend(SET_F("addOption(dd,'Logarithmic (Loudness)',1);")); +#endif + + oappend(SET_F("dd=addDropdown('AudioReactive','sync:mode');")); + oappend(SET_F("addOption(dd,'Off',0);")); +#ifdef ARDUINO_ARCH_ESP32 + oappend(SET_F("addOption(dd,'Send',1);")); +#endif + oappend(SET_F("addOption(dd,'Receive',2);")); +#ifdef ARDUINO_ARCH_ESP32 + oappend(SET_F("addInfo('AudioReactive:digitalmic:type',1,'requires reboot!');")); // 0 is field type, 1 is actual field + oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',0,'sd/data/dout','I2S SD');")); + oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',1,'ws/clk/lrck','I2S WS');")); + oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',2,'sck/bclk','I2S SCK');")); + #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) + oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',3,'only use -1, 0, 1 or 3','I2S MCLK');")); + #else + oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',3,'master clock','I2S MCLK');")); + #endif +#endif + } + + + /* + * 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() override + //{ + //WS2812FX::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!). + * This could be used in the future for the system to determine whether your usermod is installed. + */ + uint16_t getId() override + { + return USERMOD_ID_AUDIOREACTIVE; + } +}; + +void AudioReactive::removeAudioPalettes(void) { + DEBUG_PRINTLN(F("Removing audio palettes.")); + while (palettes>0) { + WS2812FX::customPalettes.pop_back(); + DEBUG_PRINTLN(palettes); + palettes--; + } + DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(WS2812FX::customPalettes.size()); +} + +void AudioReactive::createAudioPalettes(void) { + DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(WS2812FX::customPalettes.size()); + if (palettes) return; + DEBUG_PRINTLN(F("Adding audio palettes.")); + for (int i=0; i= palettes) lastCustPalette -= palettes; + for (int pal=0; pal= 0) { - irqBound = pinManager.allocatePin(config.interruptPin, false, PinOwner::UM_IMU); + irqBound = PinManager::allocatePin(config.interruptPin, false, PinOwner::UM_IMU); if (!irqBound) { DEBUG_PRINTLN(F("MPU6050: IRQ pin already in use.")); return; } pinMode(config.interruptPin, INPUT); }; @@ -408,7 +408,7 @@ class MPU6050Driver : public Usermod { // Previously loaded and config changed if (irqBound && ((old_cfg.interruptPin != config.interruptPin) || !config.enabled)) { detachInterrupt(old_cfg.interruptPin); - pinManager.deallocatePin(old_cfg.interruptPin, PinOwner::UM_IMU); + PinManager::deallocatePin(old_cfg.interruptPin, PinOwner::UM_IMU); irqBound = false; } diff --git a/usermods/mqtt_switch_v2/README.md b/usermods/mqtt_switch_v2/README.md index 4cb7ef0e8..382f72d0e 100644 --- a/usermods/mqtt_switch_v2/README.md +++ b/usermods/mqtt_switch_v2/README.md @@ -19,7 +19,7 @@ Example `usermods_list.cpp`: void registerUsermods() { - usermods.add(new UsermodMqttSwitch()); + UsermodManager::add(new UsermodMqttSwitch()); } ``` diff --git a/usermods/multi_relay/readme.md b/usermods/multi_relay/readme.md index 24dd394b8..eaa069ae7 100644 --- a/usermods/multi_relay/readme.md +++ b/usermods/multi_relay/readme.md @@ -41,7 +41,7 @@ When a relay is switched, a message is published: ## Usermod installation -1. Register the usermod by adding `#include "../usermods/multi_relay/usermod_multi_relay.h"` at the top and `usermods.add(new MultiRelay());` at the bottom of `usermods_list.cpp`. +1. Register the usermod by adding `#include "../usermods/multi_relay/usermod_multi_relay.h"` at the top and `UsermodManager::add(new MultiRelay());` at the bottom of `usermods_list.cpp`. or 2. Use `#define USERMOD_MULTI_RELAY` in wled.h or `-D USERMOD_MULTI_RELAY` in your platformio.ini @@ -90,9 +90,9 @@ void registerUsermods() * || || || * \/ \/ \/ */ - //usermods.add(new MyExampleUsermod()); - //usermods.add(new UsermodTemperature()); - usermods.add(new MultiRelay()); + //UsermodManager::add(new MyExampleUsermod()); + //UsermodManager::add(new UsermodTemperature()); + UsermodManager::add(new MultiRelay()); } ``` diff --git a/usermods/multi_relay/usermod_multi_relay.h b/usermods/multi_relay/usermod_multi_relay.h index efb3c8ae1..33a6cf85e 100644 --- a/usermods/multi_relay/usermod_multi_relay.h +++ b/usermods/multi_relay/usermod_multi_relay.h @@ -516,7 +516,7 @@ void MultiRelay::setup() { if (!_relay[i].external) _relay[i].state = !offMode; state |= (uint8_t)(_relay[i].invert ? !_relay[i].state : _relay[i].state) << pin; } else if (_relay[i].pin<100 && _relay[i].pin>=0) { - if (pinManager.allocatePin(_relay[i].pin,true, PinOwner::UM_MultiRelay)) { + if (PinManager::allocatePin(_relay[i].pin,true, PinOwner::UM_MultiRelay)) { if (!_relay[i].external) _relay[i].state = !offMode; switchRelay(i, _relay[i].state); _relay[i].active = false; @@ -817,7 +817,7 @@ bool MultiRelay::readFromConfig(JsonObject &root) { // deallocate all pins 1st for (int i=0; i=0 && oldPin[i]<100) { - pinManager.deallocatePin(oldPin[i], PinOwner::UM_MultiRelay); + PinManager::deallocatePin(oldPin[i], PinOwner::UM_MultiRelay); } // allocate new pins setup(); diff --git a/usermods/pixels_dice_tray/pixels_dice_tray.h b/usermods/pixels_dice_tray/pixels_dice_tray.h index 238af314e..a1e45ba33 100644 --- a/usermods/pixels_dice_tray/pixels_dice_tray.h +++ b/usermods/pixels_dice_tray/pixels_dice_tray.h @@ -112,15 +112,15 @@ class PixelsDiceTrayUsermod : public Usermod { SetSPIPinsFromMacros(); PinManagerPinType spiPins[] = { {spi_mosi, true}, {spi_miso, false}, {spi_sclk, true}}; - if (!pinManager.allocateMultiplePins(spiPins, 3, PinOwner::HW_SPI)) { + if (!PinManager::allocateMultiplePins(spiPins, 3, PinOwner::HW_SPI)) { enabled = false; } else { PinManagerPinType displayPins[] = { {TFT_CS, true}, {TFT_DC, true}, {TFT_RST, true}, {TFT_BL, true}}; - if (!pinManager.allocateMultiplePins( + if (!PinManager::allocateMultiplePins( displayPins, sizeof(displayPins) / sizeof(PinManagerPinType), PinOwner::UM_FourLineDisplay)) { - pinManager.deallocateMultiplePins(spiPins, 3, PinOwner::HW_SPI); + PinManager::deallocateMultiplePins(spiPins, 3, PinOwner::HW_SPI); enabled = false; } } diff --git a/usermods/pwm_outputs/usermod_pwm_outputs.h b/usermods/pwm_outputs/usermod_pwm_outputs.h index 1880308c4..09232f043 100644 --- a/usermods/pwm_outputs/usermod_pwm_outputs.h +++ b/usermods/pwm_outputs/usermod_pwm_outputs.h @@ -29,13 +29,13 @@ class PwmOutput { return; DEBUG_PRINTF("pwm_output[%d]: setup to freq %d\n", pin_, freq_); - if (!pinManager.allocatePin(pin_, true, PinOwner::UM_PWM_OUTPUTS)) + if (!PinManager::allocatePin(pin_, true, PinOwner::UM_PWM_OUTPUTS)) return; - channel_ = pinManager.allocateLedc(1); + channel_ = PinManager::allocateLedc(1); if (channel_ == 255) { DEBUG_PRINTF("pwm_output[%d]: failed to quire ledc\n", pin_); - pinManager.deallocatePin(pin_, PinOwner::UM_PWM_OUTPUTS); + PinManager::deallocatePin(pin_, PinOwner::UM_PWM_OUTPUTS); return; } @@ -49,9 +49,9 @@ class PwmOutput { DEBUG_PRINTF("pwm_output[%d]: close\n", pin_); if (!enabled_) return; - pinManager.deallocatePin(pin_, PinOwner::UM_PWM_OUTPUTS); + PinManager::deallocatePin(pin_, PinOwner::UM_PWM_OUTPUTS); if (channel_ != 255) - pinManager.deallocateLedc(channel_, 1); + PinManager::deallocateLedc(channel_, 1); channel_ = 255; duty_ = 0.0f; enabled_ = false; diff --git a/usermods/quinled-an-penta/quinled-an-penta.h b/usermods/quinled-an-penta/quinled-an-penta.h index 10b784334..e44672039 100644 --- a/usermods/quinled-an-penta/quinled-an-penta.h +++ b/usermods/quinled-an-penta/quinled-an-penta.h @@ -129,7 +129,7 @@ class QuinLEDAnPentaUsermod : public Usermod void initOledDisplay() { PinManagerPinType pins[5] = { { oledSpiClk, true }, { oledSpiData, true }, { oledSpiCs, true }, { oledSpiDc, true }, { oledSpiRst, true } }; - if (!pinManager.allocateMultiplePins(pins, 5, PinOwner::UM_QuinLEDAnPenta)) { + if (!PinManager::allocateMultiplePins(pins, 5, PinOwner::UM_QuinLEDAnPenta)) { DEBUG_PRINTF("[%s] OLED pin allocation failed!\n", _name); oledEnabled = oledInitDone = false; return; @@ -164,11 +164,11 @@ class QuinLEDAnPentaUsermod : public Usermod oledDisplay->clear(); } - pinManager.deallocatePin(oledSpiClk, PinOwner::UM_QuinLEDAnPenta); - pinManager.deallocatePin(oledSpiData, PinOwner::UM_QuinLEDAnPenta); - pinManager.deallocatePin(oledSpiCs, PinOwner::UM_QuinLEDAnPenta); - pinManager.deallocatePin(oledSpiDc, PinOwner::UM_QuinLEDAnPenta); - pinManager.deallocatePin(oledSpiRst, PinOwner::UM_QuinLEDAnPenta); + PinManager::deallocatePin(oledSpiClk, PinOwner::UM_QuinLEDAnPenta); + PinManager::deallocatePin(oledSpiData, PinOwner::UM_QuinLEDAnPenta); + PinManager::deallocatePin(oledSpiCs, PinOwner::UM_QuinLEDAnPenta); + PinManager::deallocatePin(oledSpiDc, PinOwner::UM_QuinLEDAnPenta); + PinManager::deallocatePin(oledSpiRst, PinOwner::UM_QuinLEDAnPenta); delete oledDisplay; @@ -184,7 +184,7 @@ class QuinLEDAnPentaUsermod : public Usermod void initSht30TempHumiditySensor() { PinManagerPinType pins[2] = { { shtSda, true }, { shtScl, true } }; - if (!pinManager.allocateMultiplePins(pins, 2, PinOwner::UM_QuinLEDAnPenta)) { + if (!PinManager::allocateMultiplePins(pins, 2, PinOwner::UM_QuinLEDAnPenta)) { DEBUG_PRINTF("[%s] SHT30 pin allocation failed!\n", _name); shtEnabled = shtInitDone = false; return; @@ -212,8 +212,8 @@ class QuinLEDAnPentaUsermod : public Usermod sht30TempHumidSensor->reset(); } - pinManager.deallocatePin(shtSda, PinOwner::UM_QuinLEDAnPenta); - pinManager.deallocatePin(shtScl, PinOwner::UM_QuinLEDAnPenta); + PinManager::deallocatePin(shtSda, PinOwner::UM_QuinLEDAnPenta); + PinManager::deallocatePin(shtScl, PinOwner::UM_QuinLEDAnPenta); delete sht30TempHumidSensor; diff --git a/usermods/rgb-rotary-encoder/rgb-rotary-encoder.h b/usermods/rgb-rotary-encoder/rgb-rotary-encoder.h index e57641bf9..00fc22725 100644 --- a/usermods/rgb-rotary-encoder/rgb-rotary-encoder.h +++ b/usermods/rgb-rotary-encoder/rgb-rotary-encoder.h @@ -40,7 +40,7 @@ class RgbRotaryEncoderUsermod : public Usermod void initRotaryEncoder() { PinManagerPinType pins[2] = { { eaIo, false }, { ebIo, false } }; - if (!pinManager.allocateMultiplePins(pins, 2, PinOwner::UM_RGBRotaryEncoder)) { + if (!PinManager::allocateMultiplePins(pins, 2, PinOwner::UM_RGBRotaryEncoder)) { eaIo = -1; ebIo = -1; cleanup(); @@ -108,11 +108,11 @@ class RgbRotaryEncoderUsermod : public Usermod { // Only deallocate pins if we allocated them ;) if (eaIo != -1) { - pinManager.deallocatePin(eaIo, PinOwner::UM_RGBRotaryEncoder); + PinManager::deallocatePin(eaIo, PinOwner::UM_RGBRotaryEncoder); eaIo = -1; } if (ebIo != -1) { - pinManager.deallocatePin(ebIo, PinOwner::UM_RGBRotaryEncoder); + PinManager::deallocatePin(ebIo, PinOwner::UM_RGBRotaryEncoder); ebIo = -1; } @@ -303,8 +303,8 @@ class RgbRotaryEncoderUsermod : public Usermod } if (eaIo != oldEaIo || ebIo != oldEbIo || stepsPerClick != oldStepsPerClick || incrementPerClick != oldIncrementPerClick) { - pinManager.deallocatePin(oldEaIo, PinOwner::UM_RGBRotaryEncoder); - pinManager.deallocatePin(oldEbIo, PinOwner::UM_RGBRotaryEncoder); + PinManager::deallocatePin(oldEaIo, PinOwner::UM_RGBRotaryEncoder); + PinManager::deallocatePin(oldEbIo, PinOwner::UM_RGBRotaryEncoder); delete rotaryEncoder; initRotaryEncoder(); diff --git a/usermods/sd_card/usermod_sd_card.h b/usermods/sd_card/usermod_sd_card.h index 5dac79159..da1999d9b 100644 --- a/usermods/sd_card/usermod_sd_card.h +++ b/usermods/sd_card/usermod_sd_card.h @@ -45,7 +45,7 @@ class UsermodSdCard : public Usermod { { configPinPico, true } }; - if (!pinManager.allocateMultiplePins(pins, 4, PinOwner::UM_SdCard)) { + if (!PinManager::allocateMultiplePins(pins, 4, PinOwner::UM_SdCard)) { DEBUG_PRINTF("[%s] SD (SPI) pin allocation failed!\n", _name); sdInitDone = false; return; @@ -75,10 +75,10 @@ class UsermodSdCard : public Usermod { SD_ADAPTER.end(); DEBUG_PRINTF("[%s] deallocate pins!\n", _name); - pinManager.deallocatePin(configPinSourceSelect, PinOwner::UM_SdCard); - pinManager.deallocatePin(configPinSourceClock, PinOwner::UM_SdCard); - pinManager.deallocatePin(configPinPoci, PinOwner::UM_SdCard); - pinManager.deallocatePin(configPinPico, PinOwner::UM_SdCard); + PinManager::deallocatePin(configPinSourceSelect, PinOwner::UM_SdCard); + PinManager::deallocatePin(configPinSourceClock, PinOwner::UM_SdCard); + PinManager::deallocatePin(configPinPoci, PinOwner::UM_SdCard); + PinManager::deallocatePin(configPinPico, PinOwner::UM_SdCard); sdInitDone = false; } diff --git a/usermods/seven_segment_display_reloaded/usermod_seven_segment_reloaded.h b/usermods/seven_segment_display_reloaded/usermod_seven_segment_reloaded.h index 111df2967..1436f8fc4 100644 --- a/usermods/seven_segment_display_reloaded/usermod_seven_segment_reloaded.h +++ b/usermods/seven_segment_display_reloaded/usermod_seven_segment_reloaded.h @@ -385,7 +385,7 @@ public: _setAllFalse(); #ifdef USERMOD_SN_PHOTORESISTOR - ptr = (Usermod_SN_Photoresistor*) usermods.lookup(USERMOD_ID_SN_PHOTORESISTOR); + ptr = (Usermod_SN_Photoresistor*) UsermodManager::lookup(USERMOD_ID_SN_PHOTORESISTOR); #endif DEBUG_PRINTLN(F("Setup done")); } 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 52ff3cc1d..a257413b4 100644 --- a/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h +++ b/usermods/usermod_v2_auto_save/usermod_v2_auto_save.h @@ -103,7 +103,7 @@ class AutoSaveUsermod : public Usermod { #ifdef USERMOD_FOUR_LINE_DISPLAY // This Usermod has enhanced functionality if // FourLineDisplayUsermod is available. - display = (FourLineDisplayUsermod*) usermods.lookup(USERMOD_ID_FOUR_LINE_DISP); + display = (FourLineDisplayUsermod*) UsermodManager::lookup(USERMOD_ID_FOUR_LINE_DISP); #endif initDone = true; if (enabled && applyAutoSaveOnBoot) applyPreset(autoSavePreset); diff --git a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h index 008647fa7..dfab7e6ff 100644 --- a/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h +++ b/usermods/usermod_v2_four_line_display_ALT/usermod_v2_four_line_display_ALT.h @@ -543,7 +543,7 @@ void FourLineDisplayUsermod::setup() { type = NONE; } else { PinManagerPinType cspins[3] = { { ioPin[0], true }, { ioPin[1], true }, { ioPin[2], true } }; - if (!pinManager.allocateMultiplePins(cspins, 3, PinOwner::UM_FourLineDisplay)) { type = NONE; } + if (!PinManager::allocateMultiplePins(cspins, 3, PinOwner::UM_FourLineDisplay)) { type = NONE; } } } else { if (i2c_scl<0 || i2c_sda<0) { type=NONE; } @@ -569,7 +569,7 @@ void FourLineDisplayUsermod::setup() { if (nullptr == u8x8) { DEBUG_PRINTLN(F("Display init failed.")); if (isSPI) { - pinManager.deallocateMultiplePins((const uint8_t*)ioPin, 3, PinOwner::UM_FourLineDisplay); + PinManager::deallocateMultiplePins((const uint8_t*)ioPin, 3, PinOwner::UM_FourLineDisplay); } type = NONE; return; @@ -1307,7 +1307,7 @@ bool FourLineDisplayUsermod::readFromConfig(JsonObject& root) { bool isSPI = (type == SSD1306_SPI || type == SSD1306_SPI64 || type == SSD1309_SPI64); bool newSPI = (newType == SSD1306_SPI || newType == SSD1306_SPI64 || newType == SSD1309_SPI64); if (isSPI) { - if (pinsChanged || !newSPI) pinManager.deallocateMultiplePins((const uint8_t*)oldPin, 3, PinOwner::UM_FourLineDisplay); + if (pinsChanged || !newSPI) PinManager::deallocateMultiplePins((const uint8_t*)oldPin, 3, PinOwner::UM_FourLineDisplay); if (!newSPI) { // was SPI but is no longer SPI if (i2c_scl<0 || i2c_sda<0) { newType=NONE; } @@ -1315,7 +1315,7 @@ bool FourLineDisplayUsermod::readFromConfig(JsonObject& root) { // still SPI but pins changed PinManagerPinType cspins[3] = { { ioPin[0], true }, { ioPin[1], true }, { ioPin[2], true } }; if (ioPin[0]<0 || ioPin[1]<0 || ioPin[1]<0) { newType=NONE; } - else if (!pinManager.allocateMultiplePins(cspins, 3, PinOwner::UM_FourLineDisplay)) { newType=NONE; } + else if (!PinManager::allocateMultiplePins(cspins, 3, PinOwner::UM_FourLineDisplay)) { newType=NONE; } } } else if (newSPI) { // was I2C but is now SPI @@ -1324,7 +1324,7 @@ bool FourLineDisplayUsermod::readFromConfig(JsonObject& root) { } else { PinManagerPinType pins[3] = { { ioPin[0], true }, { ioPin[1], true }, { ioPin[2], true } }; if (ioPin[0]<0 || ioPin[1]<0 || ioPin[1]<0) { newType=NONE; } - else if (!pinManager.allocateMultiplePins(pins, 3, PinOwner::UM_FourLineDisplay)) { newType=NONE; } + else if (!PinManager::allocateMultiplePins(pins, 3, PinOwner::UM_FourLineDisplay)) { newType=NONE; } } } else { // just I2C type changed diff --git a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h index 5756fbb69..55715b7c7 100644 --- a/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h +++ b/usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h @@ -489,7 +489,7 @@ void RotaryEncoderUIUsermod::setup() enabled = false; return; } else { - if (pinIRQ >= 0 && pinManager.allocatePin(pinIRQ, false, PinOwner::UM_RotaryEncoderUI)) { + if (pinIRQ >= 0 && PinManager::allocatePin(pinIRQ, false, PinOwner::UM_RotaryEncoderUI)) { pinMode(pinIRQ, INPUT_PULLUP); attachInterrupt(pinIRQ, i2cReadingISR, FALLING); // RISING, FALLING, CHANGE, ONLOW, ONHIGH DEBUG_PRINTLN(F("Interrupt attached.")); @@ -502,7 +502,7 @@ void RotaryEncoderUIUsermod::setup() } } else { PinManagerPinType pins[3] = { { pinA, false }, { pinB, false }, { pinC, false } }; - if (pinA<0 || pinB<0 || !pinManager.allocateMultiplePins(pins, 3, PinOwner::UM_RotaryEncoderUI)) { + if (pinA<0 || pinB<0 || !PinManager::allocateMultiplePins(pins, 3, PinOwner::UM_RotaryEncoderUI)) { pinA = pinB = pinC = -1; enabled = false; return; @@ -525,7 +525,7 @@ void RotaryEncoderUIUsermod::setup() #ifdef USERMOD_FOUR_LINE_DISPLAY // This Usermod uses FourLineDisplayUsermod for the best experience. // But it's optional. But you want it. - display = (FourLineDisplayUsermod*) usermods.lookup(USERMOD_ID_FOUR_LINE_DISP); + display = (FourLineDisplayUsermod*) UsermodManager::lookup(USERMOD_ID_FOUR_LINE_DISP); if (display != nullptr) { display->setMarkLine(1, 0); } @@ -1138,14 +1138,14 @@ bool RotaryEncoderUIUsermod::readFromConfig(JsonObject &root) { if (oldPcf8574) { if (pinIRQ >= 0) { detachInterrupt(pinIRQ); - pinManager.deallocatePin(pinIRQ, PinOwner::UM_RotaryEncoderUI); + PinManager::deallocatePin(pinIRQ, PinOwner::UM_RotaryEncoderUI); DEBUG_PRINTLN(F("Deallocated old IRQ pin.")); } pinIRQ = newIRQpin<100 ? newIRQpin : -1; // ignore PCF8574 pins } else { - pinManager.deallocatePin(pinA, PinOwner::UM_RotaryEncoderUI); - pinManager.deallocatePin(pinB, PinOwner::UM_RotaryEncoderUI); - pinManager.deallocatePin(pinC, PinOwner::UM_RotaryEncoderUI); + PinManager::deallocatePin(pinA, PinOwner::UM_RotaryEncoderUI); + PinManager::deallocatePin(pinB, PinOwner::UM_RotaryEncoderUI); + PinManager::deallocatePin(pinC, PinOwner::UM_RotaryEncoderUI); DEBUG_PRINTLN(F("Deallocated old pins.")); } pinA = newDTpin; diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 0084b09e0..ad843f0f9 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -75,7 +75,7 @@ int8_t tristate_square8(uint8_t x, uint8_t pulsewidth, uint8_t attdec) { static um_data_t* getAudioData() { um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { // add support for no audio um_data = simulateSound(SEGMENT.soundSim); } @@ -6298,7 +6298,7 @@ static const char _data_FX_MODE_2DPLASMAROTOZOOM[] PROGMEM = "Rotozoomer@!,Scale uint8_t *fftResult = nullptr; float *fftBin = nullptr; um_data_t *um_data; - if (usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + if (UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { volumeSmth = *(float*) um_data->u_data[0]; volumeRaw = *(float*) um_data->u_data[1]; fftResult = (uint8_t*) um_data->u_data[2]; @@ -6911,7 +6911,7 @@ uint16_t mode_pixels(void) { // Pixels. By Andrew Tuline. uint8_t *myVals = reinterpret_cast(SEGENV.data); // Used to store a pile of samples because WLED frame rate and WLED sample rate are not synchronized. Frame rate is too low. um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { um_data = simulateSound(SEGMENT.soundSim); } float volumeSmth = *(float*) um_data->u_data[0]; @@ -7494,7 +7494,7 @@ uint16_t mode_2DAkemi(void) { const float normalFactor = 0.4f; um_data_t *um_data; - if (!usermods.getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { + if (!UsermodManager::getUMData(&um_data, USERMOD_ID_AUDIOREACTIVE)) { um_data = simulateSound(SEGMENT.soundSim); } uint8_t *fftResult = (uint8_t*)um_data->u_data[2]; diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 0c4ec6570..0f197e80d 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -183,11 +183,7 @@ void IRAM_ATTR_YN Segment::deallocateData() { if ((Segment::getUsedSegmentData() > 0) && (_dataLen > 0)) { // check that we don't have a dangling / inconsistent data pointer free(data); } else { - DEBUG_PRINT(F("---- Released data ")); - DEBUG_PRINTF_P(PSTR("(%p): "), this); - DEBUG_PRINT(F("inconsistent UsedSegmentData ")); - DEBUG_PRINTF_P(PSTR("(%d/%d)"), _dataLen, Segment::getUsedSegmentData()); - DEBUG_PRINTLN(F(", cowardly refusing to free nothing.")); + DEBUG_PRINTF_P(PSTR("---- Released data (%p): inconsistent UsedSegmentData (%d/%d), cowardly refusing to free nothing.\n"), this, _dataLen, Segment::getUsedSegmentData()); } data = nullptr; Segment::addUsedSegmentData(_dataLen <= Segment::getUsedSegmentData() ? -_dataLen : -Segment::getUsedSegmentData()); @@ -1251,7 +1247,7 @@ void WS2812FX::finalizeInit() { // When booting without config (1st boot) we need to make sure GPIOs defined for LED output don't clash with hardware // i.e. DEBUG (GPIO1), DMX (2), SPI RAM/FLASH (16&17 on ESP32-WROVER/PICO), read/only pins, etc. // Pin should not be already allocated, read/only or defined for current bus - while (pinManager.isPinAllocated(defPin[j]) || !pinManager.isPinOk(defPin[j],true)) { + while (PinManager::isPinAllocated(defPin[j]) || !PinManager::isPinOk(defPin[j],true)) { if (validPin) { DEBUG_PRINTLN(F("Some of the provided pins cannot be used to configure this LED output.")); defPin[j] = 1; // start with GPIO1 and work upwards diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index b20095d4c..3766975f1 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -130,11 +130,11 @@ BusDigital::BusDigital(BusConfig &bc, uint8_t nr, const ColorOrderMap &com) , _colorOrderMap(com) { if (!isDigital(bc.type) || !bc.count) return; - if (!pinManager.allocatePin(bc.pins[0], true, PinOwner::BusDigital)) return; + if (!PinManager::allocatePin(bc.pins[0], true, PinOwner::BusDigital)) return; _frequencykHz = 0U; _pins[0] = bc.pins[0]; if (is2Pin(bc.type)) { - if (!pinManager.allocatePin(bc.pins[1], true, PinOwner::BusDigital)) { + if (!PinManager::allocatePin(bc.pins[1], true, PinOwner::BusDigital)) { cleanup(); return; } @@ -422,8 +422,8 @@ void BusDigital::cleanup() { _valid = false; _busPtr = nullptr; if (_data != nullptr) freeData(); - pinManager.deallocatePin(_pins[1], PinOwner::BusDigital); - pinManager.deallocatePin(_pins[0], PinOwner::BusDigital); + PinManager::deallocatePin(_pins[1], PinOwner::BusDigital); + PinManager::deallocatePin(_pins[0], PinOwner::BusDigital); } @@ -464,16 +464,16 @@ BusPwm::BusPwm(BusConfig &bc) managed_pin_type pins[numPins]; for (unsigned i = 0; i < numPins; i++) pins[i] = {(int8_t)bc.pins[i], true}; - if (!pinManager.allocateMultiplePins(pins, numPins, PinOwner::BusPwm)) return; + if (!PinManager::allocateMultiplePins(pins, numPins, PinOwner::BusPwm)) return; #ifdef ESP8266 analogWriteRange((1<<_depth)-1); analogWriteFreq(_frequency); #else // for 2 pin PWM CCT strip pinManager will make sure both LEDC channels are in the same speed group and sharing the same timer - _ledcStart = pinManager.allocateLedc(numPins); + _ledcStart = PinManager::allocateLedc(numPins); if (_ledcStart == 255) { //no more free LEDC channels - pinManager.deallocateMultiplePins(pins, numPins, PinOwner::BusPwm); + PinManager::deallocateMultiplePins(pins, numPins, PinOwner::BusPwm); return; } // if _needsRefresh is true (UI hack) we are using dithering (credit @dedehai & @zalatnaicsongor) @@ -640,8 +640,8 @@ std::vector BusPwm::getLEDTypes() { void BusPwm::deallocatePins() { unsigned numPins = getPins(); for (unsigned i = 0; i < numPins; i++) { - pinManager.deallocatePin(_pins[i], PinOwner::BusPwm); - if (!pinManager.isPinOk(_pins[i])) continue; + PinManager::deallocatePin(_pins[i], PinOwner::BusPwm); + if (!PinManager::isPinOk(_pins[i])) continue; #ifdef ESP8266 digitalWrite(_pins[i], LOW); //turn off PWM interrupt #else @@ -649,7 +649,7 @@ void BusPwm::deallocatePins() { #endif } #ifdef ARDUINO_ARCH_ESP32 - pinManager.deallocateLedc(_ledcStart, numPins); + PinManager::deallocateLedc(_ledcStart, numPins); #endif } @@ -661,7 +661,7 @@ BusOnOff::BusOnOff(BusConfig &bc) if (!Bus::isOnOff(bc.type)) return; uint8_t currentPin = bc.pins[0]; - if (!pinManager.allocatePin(currentPin, true, PinOwner::BusOnOff)) { + if (!PinManager::allocatePin(currentPin, true, PinOwner::BusOnOff)) { return; } _pin = currentPin; //store only after allocatePin() succeeds @@ -904,7 +904,7 @@ void BusManager::esp32RMTInvertIdle() { void BusManager::on() { #ifdef ESP8266 //Fix for turning off onboard LED breaking bus - if (pinManager.getPinOwner(LED_BUILTIN) == PinOwner::BusDigital) { + if (PinManager::getPinOwner(LED_BUILTIN) == PinOwner::BusDigital) { for (unsigned i = 0; i < numBusses; i++) { uint8_t pins[2] = {255,255}; if (busses[i]->isDigital() && busses[i]->getPins(pins)) { @@ -926,7 +926,7 @@ void BusManager::off() { #ifdef ESP8266 // turn off built-in LED if strip is turned off // this will break digital bus so will need to be re-initialised on On - if (pinManager.getPinOwner(LED_BUILTIN) == PinOwner::BusDigital) { + if (PinManager::getPinOwner(LED_BUILTIN) == PinOwner::BusDigital) { for (unsigned i = 0; i < numBusses; i++) if (busses[i]->isOffRefreshRequired()) return; pinMode(LED_BUILTIN, OUTPUT); digitalWrite(LED_BUILTIN, HIGH); diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 24f10f0a7..40fe61f40 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -280,7 +280,7 @@ class BusOnOff : public Bus { uint32_t getPixelColor(uint16_t pix) const override; uint8_t getPins(uint8_t* pinArray) const override; void show() override; - void cleanup() { pinManager.deallocatePin(_pin, PinOwner::BusOnOff); } + void cleanup() { PinManager::deallocatePin(_pin, PinOwner::BusOnOff); } static std::vector getLEDTypes(); diff --git a/wled00/button.cpp b/wled00/button.cpp index b5a4e9436..f02ed3d6d 100644 --- a/wled00/button.cpp +++ b/wled00/button.cpp @@ -267,7 +267,7 @@ void handleButton() if (btnPin[b]<0 || buttonType[b] == BTN_TYPE_NONE) continue; #endif - if (usermods.handleButton(b)) continue; // did usermod handle buttons + if (UsermodManager::handleButton(b)) continue; // did usermod handle buttons if (buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED) { // button is not a button but a potentiometer if (now - lastAnalogRead > ANALOG_BTN_READ_CYCLE) { diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index f99aa8cd5..3f6cfbacb 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -261,12 +261,12 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { JsonArray hw_btn_ins = btn_obj["ins"]; if (!hw_btn_ins.isNull()) { // deallocate existing button pins - for (unsigned b = 0; b < WLED_MAX_BUTTONS; b++) pinManager.deallocatePin(btnPin[b], PinOwner::Button); // does nothing if trying to deallocate a pin with PinOwner != Button + for (unsigned b = 0; b < WLED_MAX_BUTTONS; b++) PinManager::deallocatePin(btnPin[b], PinOwner::Button); // does nothing if trying to deallocate a pin with PinOwner != Button unsigned s = 0; for (JsonObject btn : hw_btn_ins) { CJSON(buttonType[s], btn["type"]); int8_t pin = btn["pin"][0] | -1; - if (pin > -1 && pinManager.allocatePin(pin, false, PinOwner::Button)) { + if (pin > -1 && PinManager::allocatePin(pin, false, PinOwner::Button)) { btnPin[s] = pin; #ifdef ARDUINO_ARCH_ESP32 // ESP32 only: check that analog button pin is a valid ADC gpio @@ -275,7 +275,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { // not an ADC analog pin DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), btnPin[s], s); btnPin[s] = -1; - pinManager.deallocatePin(pin,PinOwner::Button); + PinManager::deallocatePin(pin,PinOwner::Button); } else { analogReadResolution(12); // see #4040 } @@ -286,7 +286,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { // not a touch pin DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not a touch pin!\n"), btnPin[s], s); btnPin[s] = -1; - pinManager.deallocatePin(pin,PinOwner::Button); + PinManager::deallocatePin(pin,PinOwner::Button); } //if touch pin, enable the touch interrupt on ESP32 S2 & S3 #ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a function to check touch state but need to attach an interrupt to do so @@ -331,7 +331,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { if (fromFS) { // relies upon only being called once with fromFS == true, which is currently true. for (size_t s = 0; s < WLED_MAX_BUTTONS; s++) { - if (buttonType[s] == BTN_TYPE_NONE || btnPin[s] < 0 || !pinManager.allocatePin(btnPin[s], false, PinOwner::Button)) { + if (buttonType[s] == BTN_TYPE_NONE || btnPin[s] < 0 || !PinManager::allocatePin(btnPin[s], false, PinOwner::Button)) { btnPin[s] = -1; buttonType[s] = BTN_TYPE_NONE; } @@ -358,8 +358,8 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { #ifndef WLED_DISABLE_INFRARED int hw_ir_pin = hw["ir"]["pin"] | -2; // 4 if (hw_ir_pin > -2) { - pinManager.deallocatePin(irPin, PinOwner::IR); - if (pinManager.allocatePin(hw_ir_pin, false, PinOwner::IR)) { + PinManager::deallocatePin(irPin, PinOwner::IR); + if (PinManager::allocatePin(hw_ir_pin, false, PinOwner::IR)) { irPin = hw_ir_pin; } else { irPin = -1; @@ -374,8 +374,8 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { rlyOpenDrain = relay[F("odrain")] | rlyOpenDrain; int hw_relay_pin = relay["pin"] | -2; if (hw_relay_pin > -2) { - pinManager.deallocatePin(rlyPin, PinOwner::Relay); - if (pinManager.allocatePin(hw_relay_pin,true, PinOwner::Relay)) { + PinManager::deallocatePin(rlyPin, PinOwner::Relay); + if (PinManager::allocatePin(hw_relay_pin,true, PinOwner::Relay)) { rlyPin = hw_relay_pin; pinMode(rlyPin, rlyOpenDrain ? OUTPUT_OPEN_DRAIN : OUTPUT); } else { @@ -394,7 +394,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(i2c_sda, hw_if_i2c[0]); CJSON(i2c_scl, hw_if_i2c[1]); PinManagerPinType i2c[2] = { { i2c_sda, true }, { i2c_scl, true } }; - if (i2c_scl >= 0 && i2c_sda >= 0 && pinManager.allocateMultiplePins(i2c, 2, PinOwner::HW_I2C)) { + if (i2c_scl >= 0 && i2c_sda >= 0 && PinManager::allocateMultiplePins(i2c, 2, PinOwner::HW_I2C)) { #ifdef ESP32 if (!Wire.setPins(i2c_sda, i2c_scl)) { i2c_scl = i2c_sda = -1; } // this will fail if Wire is initialised (Wire.begin() called prior) else Wire.begin(); @@ -410,7 +410,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(spi_sclk, hw_if_spi[1]); CJSON(spi_miso, hw_if_spi[2]); PinManagerPinType spi[3] = { { spi_mosi, true }, { spi_miso, true }, { spi_sclk, true } }; - if (spi_mosi >= 0 && spi_sclk >= 0 && pinManager.allocateMultiplePins(spi, 3, PinOwner::HW_SPI)) { + if (spi_mosi >= 0 && spi_sclk >= 0 && PinManager::allocateMultiplePins(spi, 3, PinOwner::HW_SPI)) { #ifdef ESP32 SPI.begin(spi_sclk, spi_miso, spi_mosi); // SPI global uses VSPI on ESP32 and FSPI on C3, S3 #else @@ -664,7 +664,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { DEBUG_PRINTLN(F("Starting usermod config.")); JsonObject usermods_settings = doc["um"]; if (!usermods_settings.isNull()) { - needsSave = !usermods.readFromConfig(usermods_settings); + needsSave = !UsermodManager::readFromConfig(usermods_settings); } if (fromFS) return needsSave; @@ -700,7 +700,7 @@ void deserializeConfigFromFS() { // save default values to /cfg.json // call readFromConfig() with an empty object so that usermods can initialize to defaults prior to saving JsonObject empty = JsonObject(); - usermods.readFromConfig(empty); + UsermodManager::readFromConfig(empty); serializeConfig(); // init Ethernet (in case default type is set at compile time) #ifdef WLED_USE_ETHERNET @@ -1121,7 +1121,7 @@ void serializeConfig() { #endif JsonObject usermods_settings = root.createNestedObject("um"); - usermods.addToConfig(usermods_settings); + UsermodManager::addToConfig(usermods_settings); File f = WLED_FS.open(FPSTR(s_cfg_json), "w"); if (f) serializeJson(root, f); diff --git a/wled00/fcn_declare.h b/wled00/fcn_declare.h index a95064a2a..8903d1f27 100644 --- a/wled00/fcn_declare.h +++ b/wled00/fcn_declare.h @@ -318,34 +318,34 @@ class Usermod { class UsermodManager { private: - Usermod* ums[WLED_MAX_USERMODS]; - byte numMods = 0; + static Usermod* ums[WLED_MAX_USERMODS]; + static byte numMods; public: - void loop(); - void handleOverlayDraw(); - bool handleButton(uint8_t b); - bool getUMData(um_data_t **um_data, uint8_t mod_id = USERMOD_ID_RESERVED); // USERMOD_ID_RESERVED will poll all usermods - void setup(); - void connected(); - void appendConfigData(); - void addToJsonState(JsonObject& obj); - void addToJsonInfo(JsonObject& obj); - void readFromJsonState(JsonObject& obj); - void addToConfig(JsonObject& obj); - bool readFromConfig(JsonObject& obj); + static void loop(); + static void handleOverlayDraw(); + static bool handleButton(uint8_t b); + static bool getUMData(um_data_t **um_data, uint8_t mod_id = USERMOD_ID_RESERVED); // USERMOD_ID_RESERVED will poll all usermods + static void setup(); + static void connected(); + static void appendConfigData(); + static void addToJsonState(JsonObject& obj); + static void addToJsonInfo(JsonObject& obj); + static void readFromJsonState(JsonObject& obj); + static void addToConfig(JsonObject& obj); + static bool readFromConfig(JsonObject& obj); #ifndef WLED_DISABLE_MQTT - void onMqttConnect(bool sessionPresent); - bool onMqttMessage(char* topic, char* payload); + static void onMqttConnect(bool sessionPresent); + static bool onMqttMessage(char* topic, char* payload); #endif #ifndef WLED_DISABLE_ESPNOW - bool onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t len); + static bool onEspNowMessage(uint8_t* sender, uint8_t* payload, uint8_t len); #endif - void onUpdateBegin(bool); - void onStateChange(uint8_t); - bool add(Usermod* um); - Usermod* lookup(uint16_t mod_id); - byte getModCount() {return numMods;}; + static void onUpdateBegin(bool); + static void onStateChange(uint8_t); + static bool add(Usermod* um); + static Usermod* lookup(uint16_t mod_id); + static inline byte getModCount() {return numMods;}; }; //usermods_list.cpp diff --git a/wled00/json.cpp b/wled00/json.cpp index 596bd780e..0df7294c8 100644 --- a/wled00/json.cpp +++ b/wled00/json.cpp @@ -436,7 +436,7 @@ bool deserializeState(JsonObject root, byte callMode, byte presetId) } strip.resume(); - usermods.readFromJsonState(root); + UsermodManager::readFromJsonState(root); loadLedmap = root[F("ledmap")] | loadLedmap; @@ -592,7 +592,7 @@ void serializeState(JsonObject root, bool forPreset, bool includeBri, bool segme root[F("pl")] = currentPlaylist; root[F("ledmap")] = currentLedmap; - usermods.addToJsonState(root); + UsermodManager::addToJsonState(root); JsonObject nl = root.createNestedObject("nl"); nl["on"] = nightlightActive; @@ -784,7 +784,7 @@ void serializeInfo(JsonObject root) getTimeString(time); root[F("time")] = time; - usermods.addToJsonInfo(root); + UsermodManager::addToJsonInfo(root); uint16_t os = 0; #ifdef WLED_DEBUG diff --git a/wled00/led.cpp b/wled00/led.cpp index ba6ed2550..9de0495b4 100644 --- a/wled00/led.cpp +++ b/wled00/led.cpp @@ -131,7 +131,7 @@ void stateUpdated(byte callMode) { if (bri == nightlightTargetBri && callMode != CALL_MODE_NO_NOTIFY && nightlightMode != NL_MODE_SUN) nightlightActive = false; // notify usermods of state change - usermods.onStateChange(callMode); + UsermodManager::onStateChange(callMode); if (fadeTransition) { if (strip.getTransition() == 0) { diff --git a/wled00/mqtt.cpp b/wled00/mqtt.cpp index 833e6eb7d..6c523c3eb 100644 --- a/wled00/mqtt.cpp +++ b/wled00/mqtt.cpp @@ -45,7 +45,7 @@ static void onMqttConnect(bool sessionPresent) mqtt->subscribe(subuf, 0); } - usermods.onMqttConnect(sessionPresent); + UsermodManager::onMqttConnect(sessionPresent); DEBUG_PRINTLN(F("MQTT ready")); publishMqtt(); @@ -89,7 +89,7 @@ static void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProp topic += topicPrefixLen; } else { // Non-Wled Topic used here. Probably a usermod subscribed to this topic. - usermods.onMqttMessage(topic, payloadStr); + UsermodManager::onMqttMessage(topic, payloadStr); delete[] payloadStr; payloadStr = nullptr; return; @@ -115,7 +115,7 @@ static void onMqttMessage(char* topic, char* payload, AsyncMqttClientMessageProp } } else if (strlen(topic) != 0) { // non standard topic, check with usermods - usermods.onMqttMessage(topic, payloadStr); + UsermodManager::onMqttMessage(topic, payloadStr); } else { // topmost topic (just wled/MAC) parseMQTTBriPayload(payloadStr); diff --git a/wled00/overlay.cpp b/wled00/overlay.cpp index 239cff528..fcd0a40c2 100644 --- a/wled00/overlay.cpp +++ b/wled00/overlay.cpp @@ -88,7 +88,7 @@ void _overlayAnalogCountdown() } void handleOverlayDraw() { - usermods.handleOverlayDraw(); + UsermodManager::handleOverlayDraw(); if (analogClockSolidBlack) { const Segment* segments = strip.getSegments(); for (unsigned i = 0; i < strip.getSegmentsNum(); i++) { diff --git a/wled00/pin_manager.cpp b/wled00/pin_manager.cpp index be2a4f977..793b5440c 100644 --- a/wled00/pin_manager.cpp +++ b/wled00/pin_manager.cpp @@ -13,34 +13,16 @@ #endif #endif -#ifdef WLED_DEBUG -static void DebugPrintOwnerTag(PinOwner tag) -{ - uint32_t q = static_cast(tag); - if (q) { - DEBUG_PRINTF_P(PSTR("0x%02x (%d)"), q, q); - } else { - DEBUG_PRINT(F("(no owner)")); - } -} -#endif /// Actual allocation/deallocation routines -bool PinManagerClass::deallocatePin(byte gpio, PinOwner tag) +bool PinManager::deallocatePin(byte gpio, PinOwner tag) { if (gpio == 0xFF) return true; // explicitly allow clients to free -1 as a no-op if (!isPinOk(gpio, false)) return false; // but return false for any other invalid pin // if a non-zero ownerTag, only allow de-allocation if the owner's tag is provided if ((ownerTag[gpio] != PinOwner::None) && (ownerTag[gpio] != tag)) { - #ifdef WLED_DEBUG - DEBUG_PRINT(F("PIN DEALLOC: IO ")); - DEBUG_PRINT(gpio); - DEBUG_PRINT(F(" allocated by ")); - DebugPrintOwnerTag(ownerTag[gpio]); - DEBUG_PRINT(F(", but attempted de-allocation by ")); - DebugPrintOwnerTag(tag); - #endif + DEBUG_PRINTF_P(PSTR("PIN DEALLOC: FAIL GPIO %d allocated by 0x%02X, but attempted de-allocation by 0x%02X.\n"), gpio, static_cast(ownerTag[gpio]), static_cast(tag)); return false; } @@ -50,7 +32,7 @@ bool PinManagerClass::deallocatePin(byte gpio, PinOwner tag) } // support function for deallocating multiple pins -bool PinManagerClass::deallocateMultiplePins(const uint8_t *pinArray, byte arrayElementCount, PinOwner tag) +bool PinManager::deallocateMultiplePins(const uint8_t *pinArray, byte arrayElementCount, PinOwner tag) { bool shouldFail = false; DEBUG_PRINTLN(F("MULTIPIN DEALLOC")); @@ -66,14 +48,7 @@ bool PinManagerClass::deallocateMultiplePins(const uint8_t *pinArray, byte array // if the current pin is allocated by selected owner it is possible to release it continue; } - #ifdef WLED_DEBUG - DEBUG_PRINT(F("PIN DEALLOC: IO ")); - DEBUG_PRINT(gpio); - DEBUG_PRINT(F(" allocated by ")); - DebugPrintOwnerTag(ownerTag[gpio]); - DEBUG_PRINT(F(", but attempted de-allocation by ")); - DebugPrintOwnerTag(tag); - #endif + DEBUG_PRINTF_P(PSTR("PIN DEALLOC: FAIL GPIO %d allocated by 0x%02X, but attempted de-allocation by 0x%02X.\n"), gpio, static_cast(ownerTag[gpio]), static_cast(tag)); shouldFail = true; } if (shouldFail) { @@ -97,14 +72,14 @@ bool PinManagerClass::deallocateMultiplePins(const uint8_t *pinArray, byte array return true; } -bool PinManagerClass::deallocateMultiplePins(const managed_pin_type * mptArray, byte arrayElementCount, PinOwner tag) +bool PinManager::deallocateMultiplePins(const managed_pin_type * mptArray, byte arrayElementCount, PinOwner tag) { uint8_t pins[arrayElementCount]; for (int i=0; i(ownerTag[gpio])); shouldFail = true; } } @@ -158,64 +122,45 @@ bool PinManagerClass::allocateMultiplePins(const managed_pin_type * mptArray, by bitWrite(pinAlloc, gpio, true); ownerTag[gpio] = tag; - #ifdef WLED_DEBUG - DEBUG_PRINT(F("PIN ALLOC: Pin ")); - DEBUG_PRINT(gpio); - DEBUG_PRINT(F(" allocated by ")); - DebugPrintOwnerTag(tag); - DEBUG_PRINTLN(F("")); - #endif + DEBUG_PRINTF_P(PSTR("PIN ALLOC: Pin %d allocated by 0x%02X.\n"), gpio, static_cast(tag)); } + DEBUG_PRINTF_P(PSTR("PIN ALLOC: 0x%014llX.\n"), (unsigned long long)pinAlloc); return true; } -bool PinManagerClass::allocatePin(byte gpio, bool output, PinOwner tag) +bool PinManager::allocatePin(byte gpio, bool output, PinOwner tag) { // HW I2C & SPI pins have to be allocated using allocateMultiplePins variant since there is always SCL/SDA pair if (!isPinOk(gpio, output) || (gpio >= WLED_NUM_PINS) || tag==PinOwner::HW_I2C || tag==PinOwner::HW_SPI) { #ifdef WLED_DEBUG if (gpio < 255) { // 255 (-1) is the "not defined GPIO" if (!isPinOk(gpio, output)) { - DEBUG_PRINT(F("PIN ALLOC: FAIL for owner ")); - DebugPrintOwnerTag(tag); - DEBUG_PRINT(F(": GPIO ")); DEBUG_PRINT(gpio); + DEBUG_PRINTF_P(PSTR("PIN ALLOC: FAIL for owner 0x%02X: GPIO %d "), static_cast(tag), gpio); if (output) DEBUG_PRINTLN(F(" cannot be used for i/o on this MCU.")); else DEBUG_PRINTLN(F(" cannot be used as input on this MCU.")); } else { - DEBUG_PRINT(F("PIN ALLOC: FAIL: GPIO ")); DEBUG_PRINT(gpio); - DEBUG_PRINTLN(F(" - HW I2C & SPI pins have to be allocated using allocateMultiplePins()")); + DEBUG_PRINTF_P(PSTR("PIN ALLOC: FAIL GPIO %d - HW I2C & SPI pins have to be allocated using allocateMultiplePins.\n"), gpio); } } #endif return false; } if (isPinAllocated(gpio)) { - #ifdef WLED_DEBUG - DEBUG_PRINT(F("PIN ALLOC: Pin ")); - DEBUG_PRINT(gpio); - DEBUG_PRINT(F(" already allocated by ")); - DebugPrintOwnerTag(ownerTag[gpio]); - DEBUG_PRINTLN(F("")); - #endif + DEBUG_PRINTF_P(PSTR("PIN ALLOC: FAIL Pin %d already allocated by 0x%02X.\n"), gpio, static_cast(ownerTag[gpio])); return false; } bitWrite(pinAlloc, gpio, true); ownerTag[gpio] = tag; - #ifdef WLED_DEBUG - DEBUG_PRINT(F("PIN ALLOC: Pin ")); - DEBUG_PRINT(gpio); - DEBUG_PRINT(F(" successfully allocated by ")); - DebugPrintOwnerTag(tag); - DEBUG_PRINTLN(F("")); - #endif + DEBUG_PRINTF_P(PSTR("PIN ALLOC: Pin %d successfully allocated by 0x%02X.\n"), gpio, static_cast(ownerTag[gpio])); + DEBUG_PRINTF_P(PSTR("PIN ALLOC: 0x%014llX.\n"), (unsigned long long)pinAlloc); return true; } // if tag is set to PinOwner::None, checks for ANY owner of the pin. // if tag is set to any other value, checks if that tag is the current owner of the pin. -bool PinManagerClass::isPinAllocated(byte gpio, PinOwner tag) const +bool PinManager::isPinAllocated(byte gpio, PinOwner tag) { if (!isPinOk(gpio, false)) return true; if ((tag != PinOwner::None) && (ownerTag[gpio] != tag)) return false; @@ -239,7 +184,7 @@ bool PinManagerClass::isPinAllocated(byte gpio, PinOwner tag) const */ // Check if supplied GPIO is ok to use -bool PinManagerClass::isPinOk(byte gpio, bool output) const +bool PinManager::isPinOk(byte gpio, bool output) { if (gpio >= WLED_NUM_PINS) return false; // catch error case, to avoid array out-of-bounds access #ifdef ARDUINO_ARCH_ESP32 @@ -279,7 +224,7 @@ bool PinManagerClass::isPinOk(byte gpio, bool output) const return false; } -bool PinManagerClass::isReadOnlyPin(byte gpio) +bool PinManager::isReadOnlyPin(byte gpio) { #ifdef ARDUINO_ARCH_ESP32 if (gpio < WLED_NUM_PINS) return (digitalPinIsValid(gpio) && !digitalPinCanOutput(gpio)); @@ -287,14 +232,14 @@ bool PinManagerClass::isReadOnlyPin(byte gpio) return false; } -PinOwner PinManagerClass::getPinOwner(byte gpio) const +PinOwner PinManager::getPinOwner(byte gpio) { if (!isPinOk(gpio, false)) return PinOwner::None; return ownerTag[gpio]; } #ifdef ARDUINO_ARCH_ESP32 -byte PinManagerClass::allocateLedc(byte channels) +byte PinManager::allocateLedc(byte channels) { if (channels > WLED_MAX_ANALOG_CHANNELS || channels == 0) return 255; unsigned ca = 0; @@ -321,7 +266,7 @@ byte PinManagerClass::allocateLedc(byte channels) return 255; //not enough consecutive free LEDC channels } -void PinManagerClass::deallocateLedc(byte pos, byte channels) +void PinManager::deallocateLedc(byte pos, byte channels) { for (unsigned j = pos; j < pos + channels && j < WLED_MAX_ANALOG_CHANNELS; j++) { bitWrite(ledcAlloc, j, false); @@ -329,4 +274,12 @@ void PinManagerClass::deallocateLedc(byte pos, byte channels) } #endif -PinManagerClass pinManager = PinManagerClass(); +#ifdef ESP8266 +uint32_t PinManager::pinAlloc = 0UL; +#else +uint64_t PinManager::pinAlloc = 0ULL; +uint16_t PinManager::ledcAlloc = 0; +#endif +uint8_t PinManager::i2cAllocCount = 0; +uint8_t PinManager::spiAllocCount = 0; +PinOwner PinManager::ownerTag[WLED_NUM_PINS] = { PinOwner::None }; diff --git a/wled00/pin_manager.h b/wled00/pin_manager.h index a64900c89..73a4a3656 100644 --- a/wled00/pin_manager.h +++ b/wled00/pin_manager.h @@ -70,61 +70,54 @@ enum struct PinOwner : uint8_t { }; static_assert(0u == static_cast(PinOwner::None), "PinOwner::None must be zero, so default array initialization works as expected"); -class PinManagerClass { +class PinManager { private: - struct { #ifdef ESP8266 - #define WLED_NUM_PINS (GPIO_PIN_COUNT+1) // somehow they forgot GPIO 16 (0-16==17) - uint32_t pinAlloc : 24; // 24bit, 1 bit per pin, we use first 17bits + #define WLED_NUM_PINS (GPIO_PIN_COUNT+1) // somehow they forgot GPIO 16 (0-16==17) + static uint32_t pinAlloc; // 1 bit per pin, we use first 17bits #else - #define WLED_NUM_PINS (GPIO_PIN_COUNT) - uint64_t pinAlloc : 56; // 56 bits, 1 bit per pin, we use 50 bits on ESP32-S3 - uint16_t ledcAlloc : 16; // up to 16 LEDC channels (WLED_MAX_ANALOG_CHANNELS) + #define WLED_NUM_PINS (GPIO_PIN_COUNT) + static uint64_t pinAlloc; // 1 bit per pin, we use 50 bits on ESP32-S3 + static uint16_t ledcAlloc; // up to 16 LEDC channels (WLED_MAX_ANALOG_CHANNELS) #endif - uint8_t i2cAllocCount : 4; // allow multiple allocation of I2C bus pins but keep track of allocations - uint8_t spiAllocCount : 4; // allow multiple allocation of SPI bus pins but keep track of allocations - } __attribute__ ((packed)); - PinOwner ownerTag[WLED_NUM_PINS] = { PinOwner::None }; + static uint8_t i2cAllocCount; // allow multiple allocation of I2C bus pins but keep track of allocations + static uint8_t spiAllocCount; // allow multiple allocation of SPI bus pins but keep track of allocations + static PinOwner ownerTag[WLED_NUM_PINS]; public: - PinManagerClass() : pinAlloc(0ULL), i2cAllocCount(0), spiAllocCount(0) { + // De-allocates a single pin + static bool deallocatePin(byte gpio, PinOwner tag); + // De-allocates multiple pins but only if all can be deallocated (PinOwner has to be specified) + static bool deallocateMultiplePins(const uint8_t *pinArray, byte arrayElementCount, PinOwner tag); + static bool deallocateMultiplePins(const managed_pin_type *pinArray, byte arrayElementCount, PinOwner tag); + // Allocates a single pin, with an owner tag. + // De-allocation requires the same owner tag (or override) + static bool allocatePin(byte gpio, bool output, PinOwner tag); + // Allocates all the pins, or allocates none of the pins, with owner tag. + // Provided to simplify error condition handling in clients + // using more than one pin, such as I2C, SPI, rotary encoders, + // ethernet, etc.. + static bool allocateMultiplePins(const managed_pin_type * mptArray, byte arrayElementCount, PinOwner tag ); + + [[deprecated("Replaced by three-parameter allocatePin(gpio, output, ownerTag), for improved debugging")]] + static inline bool allocatePin(byte gpio, bool output = true) { return allocatePin(gpio, output, PinOwner::None); } + [[deprecated("Replaced by two-parameter deallocatePin(gpio, ownerTag), for improved debugging")]] + static inline void deallocatePin(byte gpio) { deallocatePin(gpio, PinOwner::None); } + + // will return true for reserved pins + static bool isPinAllocated(byte gpio, PinOwner tag = PinOwner::None); + // will return false for reserved pins + static bool isPinOk(byte gpio, bool output = true); + + static bool isReadOnlyPin(byte gpio); + + static PinOwner getPinOwner(byte gpio); + #ifdef ARDUINO_ARCH_ESP32 - ledcAlloc = 0; + static byte allocateLedc(byte channels); + static void deallocateLedc(byte pos, byte channels); #endif - } - // De-allocates a single pin - bool deallocatePin(byte gpio, PinOwner tag); - // De-allocates multiple pins but only if all can be deallocated (PinOwner has to be specified) - bool deallocateMultiplePins(const uint8_t *pinArray, byte arrayElementCount, PinOwner tag); - bool deallocateMultiplePins(const managed_pin_type *pinArray, byte arrayElementCount, PinOwner tag); - // Allocates a single pin, with an owner tag. - // De-allocation requires the same owner tag (or override) - bool allocatePin(byte gpio, bool output, PinOwner tag); - // Allocates all the pins, or allocates none of the pins, with owner tag. - // Provided to simplify error condition handling in clients - // using more than one pin, such as I2C, SPI, rotary encoders, - // ethernet, etc.. - bool allocateMultiplePins(const managed_pin_type * mptArray, byte arrayElementCount, PinOwner tag ); - - [[deprecated("Replaced by three-parameter allocatePin(gpio, output, ownerTag), for improved debugging")]] - inline bool allocatePin(byte gpio, bool output = true) { return allocatePin(gpio, output, PinOwner::None); } - [[deprecated("Replaced by two-parameter deallocatePin(gpio, ownerTag), for improved debugging")]] - inline void deallocatePin(byte gpio) { deallocatePin(gpio, PinOwner::None); } - - // will return true for reserved pins - bool isPinAllocated(byte gpio, PinOwner tag = PinOwner::None) const; - // will return false for reserved pins - bool isPinOk(byte gpio, bool output = true) const; - - static bool isReadOnlyPin(byte gpio); - - PinOwner getPinOwner(byte gpio) const; - - #ifdef ARDUINO_ARCH_ESP32 - byte allocateLedc(byte channels); - void deallocateLedc(byte pos, byte channels); - #endif }; -extern PinManagerClass pinManager; +//extern PinManager pinManager; #endif diff --git a/wled00/set.cpp b/wled00/set.cpp index 812bcc52f..96eb3ed13 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -104,18 +104,18 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) { int t = 0; - if (rlyPin>=0 && pinManager.isPinAllocated(rlyPin, PinOwner::Relay)) { - pinManager.deallocatePin(rlyPin, PinOwner::Relay); + if (rlyPin>=0 && PinManager::isPinAllocated(rlyPin, PinOwner::Relay)) { + PinManager::deallocatePin(rlyPin, PinOwner::Relay); } #ifndef WLED_DISABLE_INFRARED - if (irPin>=0 && pinManager.isPinAllocated(irPin, PinOwner::IR)) { + if (irPin>=0 && PinManager::isPinAllocated(irPin, PinOwner::IR)) { deInitIR(); - pinManager.deallocatePin(irPin, PinOwner::IR); + PinManager::deallocatePin(irPin, PinOwner::IR); } #endif for (unsigned s=0; s=0 && pinManager.isPinAllocated(btnPin[s], PinOwner::Button)) { - pinManager.deallocatePin(btnPin[s], PinOwner::Button); + if (btnPin[s]>=0 && PinManager::isPinAllocated(btnPin[s], PinOwner::Button)) { + PinManager::deallocatePin(btnPin[s], PinOwner::Button); #ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a function to check touch state, detach interrupt if (digitalPinToTouchChannel(btnPin[s]) >= 0) // if touch capable pin touchDetachInterrupt(btnPin[s]); // if not assigned previously, this will do nothing @@ -233,7 +233,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) // update other pins #ifndef WLED_DISABLE_INFRARED int hw_ir_pin = request->arg(F("IR")).toInt(); - if (pinManager.allocatePin(hw_ir_pin,false, PinOwner::IR)) { + if (PinManager::allocatePin(hw_ir_pin,false, PinOwner::IR)) { irPin = hw_ir_pin; } else { irPin = -1; @@ -244,7 +244,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) irApplyToAllSelected = !request->hasArg(F("MSO")); int hw_rly_pin = request->arg(F("RL")).toInt(); - if (pinManager.allocatePin(hw_rly_pin,true, PinOwner::Relay)) { + if (PinManager::allocatePin(hw_rly_pin,true, PinOwner::Relay)) { rlyPin = hw_rly_pin; } else { rlyPin = -1; @@ -259,7 +259,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) char bt[4] = "BT"; bt[2] = offset+i; bt[3] = 0; // button pin (use A,B,C,... if WLED_MAX_BUTTONS>10) char be[4] = "BE"; be[2] = offset+i; be[3] = 0; // button type (use A,B,C,... if WLED_MAX_BUTTONS>10) int hw_btn_pin = request->arg(bt).toInt(); - if (hw_btn_pin >= 0 && pinManager.allocatePin(hw_btn_pin,false,PinOwner::Button)) { + if (hw_btn_pin >= 0 && PinManager::allocatePin(hw_btn_pin,false,PinOwner::Button)) { btnPin[i] = hw_btn_pin; buttonType[i] = request->arg(be).toInt(); #ifdef ARDUINO_ARCH_ESP32 @@ -270,7 +270,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) // not an ADC analog pin DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for analog button #%d is not an analog pin!\n"), btnPin[i], i); btnPin[i] = -1; - pinManager.deallocatePin(hw_btn_pin,PinOwner::Button); + PinManager::deallocatePin(hw_btn_pin,PinOwner::Button); } else { analogReadResolution(12); // see #4040 } @@ -282,7 +282,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) // not a touch pin DEBUG_PRINTF_P(PSTR("PIN ALLOC error: GPIO%d for touch button #%d is not an touch pin!\n"), btnPin[i], i); btnPin[i] = -1; - pinManager.deallocatePin(hw_btn_pin,PinOwner::Button); + PinManager::deallocatePin(hw_btn_pin,PinOwner::Button); } #ifdef SOC_TOUCH_VERSION_2 // ESP32 S2 and S3 have a fucntion to check touch state but need to attach an interrupt to do so else @@ -631,10 +631,10 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) if (i2c_sda != hw_sda_pin || i2c_scl != hw_scl_pin) { // only if pins changed uint8_t old_i2c[2] = { static_cast(i2c_scl), static_cast(i2c_sda) }; - pinManager.deallocateMultiplePins(old_i2c, 2, PinOwner::HW_I2C); // just in case deallocation of old pins + PinManager::deallocateMultiplePins(old_i2c, 2, PinOwner::HW_I2C); // just in case deallocation of old pins PinManagerPinType i2c[2] = { { hw_sda_pin, true }, { hw_scl_pin, true } }; - if (hw_sda_pin >= 0 && hw_scl_pin >= 0 && pinManager.allocateMultiplePins(i2c, 2, PinOwner::HW_I2C)) { + if (hw_sda_pin >= 0 && hw_scl_pin >= 0 && PinManager::allocateMultiplePins(i2c, 2, PinOwner::HW_I2C)) { i2c_sda = hw_sda_pin; i2c_scl = hw_scl_pin; // no bus re-initialisation as usermods do not get any notification @@ -658,9 +658,9 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) if (spi_mosi != hw_mosi_pin || spi_miso != hw_miso_pin || spi_sclk != hw_sclk_pin) { // only if pins changed uint8_t old_spi[3] = { static_cast(spi_mosi), static_cast(spi_miso), static_cast(spi_sclk) }; - pinManager.deallocateMultiplePins(old_spi, 3, PinOwner::HW_SPI); // just in case deallocation of old pins + PinManager::deallocateMultiplePins(old_spi, 3, PinOwner::HW_SPI); // just in case deallocation of old pins PinManagerPinType spi[3] = { { hw_mosi_pin, true }, { hw_miso_pin, true }, { hw_sclk_pin, true } }; - if (hw_mosi_pin >= 0 && hw_sclk_pin >= 0 && pinManager.allocateMultiplePins(spi, 3, PinOwner::HW_SPI)) { + if (hw_mosi_pin >= 0 && hw_sclk_pin >= 0 && PinManager::allocateMultiplePins(spi, 3, PinOwner::HW_SPI)) { spi_mosi = hw_mosi_pin; spi_miso = hw_miso_pin; spi_sclk = hw_sclk_pin; @@ -750,8 +750,8 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) DEBUG_PRINTF_P(PSTR(" = %s\n"), value.c_str()); } } - usermods.readFromConfig(um); // force change of usermod parameters - DEBUG_PRINTLN(F("Done re-init usermods.")); + UsermodManager::readFromConfig(um); // force change of usermod parameters + DEBUG_PRINTLN(F("Done re-init UsermodManager::")); releaseJSONBufferLock(); } diff --git a/wled00/udp.cpp b/wled00/udp.cpp index 8cf733dff..09e1440ef 100644 --- a/wled00/udp.cpp +++ b/wled00/udp.cpp @@ -976,7 +976,7 @@ void espNowReceiveCB(uint8_t* address, uint8_t* data, uint8_t len, signed int rs #ifndef WLED_DISABLE_ESPNOW // usermods hook can override processing - if (usermods.onEspNowMessage(address, data, len)) return; + if (UsermodManager::onEspNowMessage(address, data, len)) return; #endif // handle WiZ Mote data diff --git a/wled00/um_manager.cpp b/wled00/um_manager.cpp index 2db29c3cd..d4ed8135f 100644 --- a/wled00/um_manager.cpp +++ b/wled00/um_manager.cpp @@ -68,3 +68,6 @@ bool UsermodManager::add(Usermod* um) ums[numMods++] = um; return true; } + +Usermod* UsermodManager::ums[WLED_MAX_USERMODS] = {nullptr}; +byte UsermodManager::numMods = 0; diff --git a/wled00/usermods_list.cpp b/wled00/usermods_list.cpp index 25d9ee9ab..36bd122a5 100644 --- a/wled00/usermods_list.cpp +++ b/wled00/usermods_list.cpp @@ -249,225 +249,225 @@ void registerUsermods() * || || || * \/ \/ \/ */ - //usermods.add(new MyExampleUsermod()); + //UsermodManager::add(new MyExampleUsermod()); #ifdef USERMOD_BATTERY - usermods.add(new UsermodBattery()); + UsermodManager::add(new UsermodBattery()); #endif #ifdef USERMOD_DALLASTEMPERATURE - usermods.add(new UsermodTemperature()); + UsermodManager::add(new UsermodTemperature()); #endif #ifdef USERMOD_SN_PHOTORESISTOR - usermods.add(new Usermod_SN_Photoresistor()); + UsermodManager::add(new Usermod_SN_Photoresistor()); #endif #ifdef USERMOD_PWM_FAN - usermods.add(new PWMFanUsermod()); + UsermodManager::add(new PWMFanUsermod()); #endif #ifdef USERMOD_BUZZER - usermods.add(new BuzzerUsermod()); + UsermodManager::add(new BuzzerUsermod()); #endif #ifdef USERMOD_BH1750 - usermods.add(new Usermod_BH1750()); + UsermodManager::add(new Usermod_BH1750()); #endif #ifdef USERMOD_BME280 - usermods.add(new UsermodBME280()); + UsermodManager::add(new UsermodBME280()); #endif #ifdef USERMOD_BME68X - usermods.add(new UsermodBME68X()); + UsermodManager::add(new UsermodBME68X()); #endif #ifdef USERMOD_SENSORSTOMQTT - usermods.add(new UserMod_SensorsToMQTT()); + UsermodManager::add(new UserMod_SensorsToMQTT()); #endif #ifdef USERMOD_PIRSWITCH - usermods.add(new PIRsensorSwitch()); + UsermodManager::add(new PIRsensorSwitch()); #endif #ifdef USERMOD_FOUR_LINE_DISPLAY - usermods.add(new FourLineDisplayUsermod()); + UsermodManager::add(new FourLineDisplayUsermod()); #endif #ifdef USERMOD_ROTARY_ENCODER_UI - usermods.add(new RotaryEncoderUIUsermod()); // can use USERMOD_FOUR_LINE_DISPLAY + UsermodManager::add(new RotaryEncoderUIUsermod()); // can use USERMOD_FOUR_LINE_DISPLAY #endif #ifdef USERMOD_AUTO_SAVE - usermods.add(new AutoSaveUsermod()); // can use USERMOD_FOUR_LINE_DISPLAY + UsermodManager::add(new AutoSaveUsermod()); // can use USERMOD_FOUR_LINE_DISPLAY #endif #ifdef USERMOD_DHT - usermods.add(new UsermodDHT()); + UsermodManager::add(new UsermodDHT()); #endif #ifdef USERMOD_VL53L0X_GESTURES - usermods.add(new UsermodVL53L0XGestures()); + UsermodManager::add(new UsermodVL53L0XGestures()); #endif #ifdef USERMOD_ANIMATED_STAIRCASE - usermods.add(new Animated_Staircase()); + UsermodManager::add(new Animated_Staircase()); #endif #ifdef USERMOD_MULTI_RELAY - usermods.add(new MultiRelay()); + UsermodManager::add(new MultiRelay()); #endif #ifdef USERMOD_RTC - usermods.add(new RTCUsermod()); + UsermodManager::add(new RTCUsermod()); #endif #ifdef USERMOD_ELEKSTUBE_IPS - usermods.add(new ElekstubeIPSUsermod()); + UsermodManager::add(new ElekstubeIPSUsermod()); #endif #ifdef USERMOD_ROTARY_ENCODER_BRIGHTNESS_COLOR - usermods.add(new RotaryEncoderBrightnessColor()); + UsermodManager::add(new RotaryEncoderBrightnessColor()); #endif #ifdef RGB_ROTARY_ENCODER - usermods.add(new RgbRotaryEncoderUsermod()); + UsermodManager::add(new RgbRotaryEncoderUsermod()); #endif #ifdef USERMOD_ST7789_DISPLAY - usermods.add(new St7789DisplayUsermod()); + UsermodManager::add(new St7789DisplayUsermod()); #endif #ifdef USERMOD_PIXELS_DICE_TRAY - usermods.add(new PixelsDiceTrayUsermod()); + UsermodManager::add(new PixelsDiceTrayUsermod()); #endif #ifdef USERMOD_SEVEN_SEGMENT - usermods.add(new SevenSegmentDisplay()); + UsermodManager::add(new SevenSegmentDisplay()); #endif #ifdef USERMOD_SSDR - usermods.add(new UsermodSSDR()); + UsermodManager::add(new UsermodSSDR()); #endif #ifdef USERMOD_CRONIXIE - usermods.add(new UsermodCronixie()); + UsermodManager::add(new UsermodCronixie()); #endif #ifdef QUINLED_AN_PENTA - usermods.add(new QuinLEDAnPentaUsermod()); + UsermodManager::add(new QuinLEDAnPentaUsermod()); #endif #ifdef USERMOD_WIZLIGHTS - usermods.add(new WizLightsUsermod()); + UsermodManager::add(new WizLightsUsermod()); #endif #ifdef USERMOD_WIREGUARD - usermods.add(new WireguardUsermod()); + UsermodManager::add(new WireguardUsermod()); #endif #ifdef USERMOD_WORDCLOCK - usermods.add(new WordClockUsermod()); + UsermodManager::add(new WordClockUsermod()); #endif #ifdef USERMOD_MY9291 - usermods.add(new MY9291Usermod()); + UsermodManager::add(new MY9291Usermod()); #endif #ifdef USERMOD_SI7021_MQTT_HA - usermods.add(new Si7021_MQTT_HA()); + UsermodManager::add(new Si7021_MQTT_HA()); #endif #ifdef USERMOD_SMARTNEST - usermods.add(new Smartnest()); + UsermodManager::add(new Smartnest()); #endif #ifdef USERMOD_AUDIOREACTIVE - usermods.add(new AudioReactive()); + UsermodManager::add(new AudioReactive()); #endif #ifdef USERMOD_ANALOG_CLOCK - usermods.add(new AnalogClockUsermod()); + UsermodManager::add(new AnalogClockUsermod()); #endif #ifdef USERMOD_PING_PONG_CLOCK - usermods.add(new PingPongClockUsermod()); + UsermodManager::add(new PingPongClockUsermod()); #endif #ifdef USERMOD_ADS1115 - usermods.add(new ADS1115Usermod()); + UsermodManager::add(new ADS1115Usermod()); #endif #ifdef USERMOD_KLIPPER_PERCENTAGE - usermods.add(new klipper_percentage()); + UsermodManager::add(new klipper_percentage()); #endif #ifdef USERMOD_BOBLIGHT - usermods.add(new BobLightUsermod()); + UsermodManager::add(new BobLightUsermod()); #endif #ifdef SD_ADAPTER - usermods.add(new UsermodSdCard()); + UsermodManager::add(new UsermodSdCard()); #endif #ifdef USERMOD_PWM_OUTPUTS - usermods.add(new PwmOutputsUsermod()); + UsermodManager::add(new PwmOutputsUsermod()); #endif #ifdef USERMOD_SHT - usermods.add(new ShtUsermod()); + UsermodManager::add(new ShtUsermod()); #endif #ifdef USERMOD_ANIMARTRIX - usermods.add(new AnimartrixUsermod("Animartrix", false)); + UsermodManager::add(new AnimartrixUsermod("Animartrix", false)); #endif #ifdef USERMOD_INTERNAL_TEMPERATURE - usermods.add(new InternalTemperatureUsermod()); + UsermodManager::add(new InternalTemperatureUsermod()); #endif #ifdef USERMOD_HTTP_PULL_LIGHT_CONTROL - usermods.add(new HttpPullLightControl()); + UsermodManager::add(new HttpPullLightControl()); #endif #ifdef USERMOD_MPU6050_IMU - static MPU6050Driver mpu6050; usermods.add(&mpu6050); + static MPU6050Driver mpu6050; UsermodManager::add(&mpu6050); #endif #ifdef USERMOD_GYRO_SURGE - static GyroSurge gyro_surge; usermods.add(&gyro_surge); + static GyroSurge gyro_surge; UsermodManager::add(&gyro_surge); #endif #ifdef USERMOD_LDR_DUSK_DAWN - usermods.add(new LDR_Dusk_Dawn_v2()); + UsermodManager::add(new LDR_Dusk_Dawn_v2()); #endif #ifdef USERMOD_STAIRCASE_WIPE - usermods.add(new StairwayWipeUsermod()); + UsermodManager::add(new StairwayWipeUsermod()); #endif #ifdef USERMOD_MAX17048 - usermods.add(new Usermod_MAX17048()); + UsermodManager::add(new Usermod_MAX17048()); #endif #ifdef USERMOD_TETRISAI - usermods.add(new TetrisAIUsermod()); + UsermodManager::add(new TetrisAIUsermod()); #endif #ifdef USERMOD_AHT10 - usermods.add(new UsermodAHT10()); + UsermodManager::add(new UsermodAHT10()); #endif #ifdef USERMOD_INA226 - usermods.add(new UsermodINA226()); + UsermodManager::add(new UsermodINA226()); #endif #ifdef USERMOD_LD2410 - usermods.add(new LD2410Usermod()); + UsermodManager::add(new LD2410Usermod()); #endif #ifdef USERMOD_POV_DISPLAY - usermods.add(new PovDisplayUsermod()); + UsermodManager::add(new PovDisplayUsermod()); #endif } diff --git a/wled00/wled.cpp b/wled00/wled.cpp index bc1cc7b73..39e0d250b 100644 --- a/wled00/wled.cpp +++ b/wled00/wled.cpp @@ -72,7 +72,7 @@ void WLED::loop() unsigned long usermodMillis = millis(); #endif userLoop(); - usermods.loop(); + UsermodManager::loop(); #ifdef WLED_DEBUG usermodMillis = millis() - usermodMillis; avgUsermodMillis += usermodMillis; @@ -410,10 +410,10 @@ void WLED::setup() #endif #if defined(WLED_DEBUG) && !defined(WLED_DEBUG_HOST) - pinManager.allocatePin(hardwareTX, true, PinOwner::DebugOut); // TX (GPIO1 on ESP32) reserved for debug output + PinManager::allocatePin(hardwareTX, true, PinOwner::DebugOut); // TX (GPIO1 on ESP32) reserved for debug output #endif #ifdef WLED_ENABLE_DMX //reserve GPIO2 as hardcoded DMX pin - pinManager.allocatePin(2, true, PinOwner::DMX); + PinManager::allocatePin(2, true, PinOwner::DMX); #endif DEBUG_PRINTLN(F("Registering usermods ...")); @@ -452,7 +452,7 @@ void WLED::setup() DEBUG_PRINTF_P(PSTR("heap %u\n"), ESP.getFreeHeap()); #if defined(STATUSLED) && STATUSLED>=0 - if (!pinManager.isPinAllocated(STATUSLED)) { + if (!PinManager::isPinAllocated(STATUSLED)) { // NOTE: Special case: The status LED should *NOT* be allocated. // See comments in handleStatusLed(). pinMode(STATUSLED, OUTPUT); @@ -465,7 +465,7 @@ void WLED::setup() DEBUG_PRINTLN(F("Usermods setup")); userSetup(); - usermods.setup(); + UsermodManager::setup(); DEBUG_PRINTF_P(PSTR("heap %u\n"), ESP.getFreeHeap()); if (strcmp(multiWiFi[0].clientSSID, DEFAULT_CLIENT_SSID) == 0) @@ -479,8 +479,8 @@ void WLED::setup() findWiFi(true); // start scanning for available WiFi-s // all GPIOs are allocated at this point - serialCanRX = !pinManager.isPinAllocated(hardwareRX); // Serial RX pin (GPIO 3 on ESP32 and ESP8266) - serialCanTX = !pinManager.isPinAllocated(hardwareTX) || pinManager.getPinOwner(hardwareTX) == PinOwner::DebugOut; // Serial TX pin (GPIO 1 on ESP32 and ESP8266) + serialCanRX = !PinManager::isPinAllocated(hardwareRX); // Serial RX pin (GPIO 3 on ESP32 and ESP8266) + serialCanTX = !PinManager::isPinAllocated(hardwareTX) || PinManager::getPinOwner(hardwareTX) == PinOwner::DebugOut; // Serial TX pin (GPIO 1 on ESP32 and ESP8266) #ifdef WLED_ENABLE_ADALIGHT //Serial RX (Adalight, Improv, Serial JSON) only possible if GPIO3 unused @@ -685,7 +685,7 @@ bool WLED::initEthernet() return false; } - if (!pinManager.allocateMultiplePins(pinsToAllocate, 10, PinOwner::Ethernet)) { + if (!PinManager::allocateMultiplePins(pinsToAllocate, 10, PinOwner::Ethernet)) { DEBUG_PRINTLN(F("initE: Failed to allocate ethernet pins")); return false; } @@ -719,7 +719,7 @@ bool WLED::initEthernet() DEBUG_PRINTLN(F("initC: ETH.begin() failed")); // de-allocate the allocated pins for (managed_pin_type mpt : pinsToAllocate) { - pinManager.deallocatePin(mpt.pin, PinOwner::Ethernet); + PinManager::deallocatePin(mpt.pin, PinOwner::Ethernet); } return false; } @@ -1010,7 +1010,7 @@ void WLED::handleConnection() } initInterfaces(); userConnected(); - usermods.connected(); + UsermodManager::connected(); lastMqttReconnectAttempt = 0; // force immediate update // shut down AP @@ -1033,7 +1033,7 @@ void WLED::handleStatusLED() uint32_t c = 0; #if STATUSLED>=0 - if (pinManager.isPinAllocated(STATUSLED)) { + if (PinManager::isPinAllocated(STATUSLED)) { return; //lower priority if something else uses the same pin } #endif diff --git a/wled00/wled_server.cpp b/wled00/wled_server.cpp index 9d4e4c85b..7d6fecd8b 100644 --- a/wled00/wled_server.cpp +++ b/wled00/wled_server.cpp @@ -396,7 +396,7 @@ void initServer() #if WLED_WATCHDOG_TIMEOUT > 0 WLED::instance().disableWatchdog(); #endif - usermods.onUpdateBegin(true); // notify usermods that update is about to begin (some may require task de-init) + UsermodManager::onUpdateBegin(true); // notify usermods that update is about to begin (some may require task de-init) lastEditTime = millis(); // make sure PIN does not lock during update strip.suspend(); #ifdef ESP8266 @@ -412,7 +412,7 @@ void initServer() } else { DEBUG_PRINTLN(F("Update Failed")); strip.resume(); - usermods.onUpdateBegin(false); // notify usermods that update has failed (some may require task init) + UsermodManager::onUpdateBegin(false); // notify usermods that update has failed (some may require task init) #if WLED_WATCHDOG_TIMEOUT > 0 WLED::instance().enableWatchdog(); #endif diff --git a/wled00/xml.cpp b/wled00/xml.cpp index 71d66d002..a9195a309 100644 --- a/wled00/xml.cpp +++ b/wled00/xml.cpp @@ -135,7 +135,7 @@ void appendGPIOinfo() { if (requestJSONBufferLock(6)) { // if we can't allocate JSON buffer ignore usermod pins JsonObject mods = pDoc->createNestedObject(F("um")); - usermods.addToConfig(mods); + UsermodManager::addToConfig(mods); if (!mods.isNull()) fillUMPins(mods); releaseJSONBufferLock(); } @@ -144,7 +144,7 @@ void appendGPIOinfo() { // add reserved (unusable) pins oappend(SET_F("d.rsvd=[")); for (unsigned i = 0; i < WLED_NUM_PINS; i++) { - if (!pinManager.isPinOk(i, false)) { // include readonly pins + if (!PinManager::isPinOk(i, false)) { // include readonly pins oappendi(i); oappend(","); } } @@ -181,7 +181,7 @@ void appendGPIOinfo() { oappend(SET_F("d.ro_gpio=[")); bool firstPin = true; for (unsigned i = 0; i < WLED_NUM_PINS; i++) { - if (pinManager.isReadOnlyPin(i)) { + if (PinManager::isReadOnlyPin(i)) { // No comma before the first pin if (!firstPin) oappend(SET_F(",")); oappendi(i); @@ -370,7 +370,7 @@ void getSettingsJS(byte subPage, char* dest) int nPins = bus->getPins(pins); for (int i = 0; i < nPins; i++) { lp[1] = offset+i; - if (pinManager.isPinOk(pins[i]) || bus->isVirtual()) sappend('v',lp,pins[i]); + if (PinManager::isPinOk(pins[i]) || bus->isVirtual()) sappend('v',lp,pins[i]); } sappend('v',lc,bus->getLength()); sappend('v',lt,bus->getType()); @@ -694,7 +694,7 @@ void getSettingsJS(byte subPage, char* dest) { appendGPIOinfo(); oappend(SET_F("numM=")); - oappendi(usermods.getModCount()); + oappendi(UsermodManager::getModCount()); oappend(";"); sappend('v',SET_F("SDA"),i2c_sda); sappend('v',SET_F("SCL"),i2c_scl); @@ -706,7 +706,7 @@ void getSettingsJS(byte subPage, char* dest) oappend(SET_F("addInfo('MOSI','")); oappendi(HW_PIN_DATASPI); oappend(SET_F("');")); oappend(SET_F("addInfo('MISO','")); oappendi(HW_PIN_MISOSPI); oappend(SET_F("');")); oappend(SET_F("addInfo('SCLK','")); oappendi(HW_PIN_CLOCKSPI); oappend(SET_F("');")); - usermods.appendConfigData(); + UsermodManager::appendConfigData(); } if (subPage == SUBPAGE_UPDATE) // update From 9cb3531e2d3cd12dbdfc0669dfdbd1f5a58e75b6 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Sat, 21 Sep 2024 22:24:36 +0200 Subject: [PATCH 141/142] Remove erroneous file Fix constant dependancy --- usermods/audioreactive/audio_reactive.old.h | 2071 ------------------- wled00/FX_fcn.cpp | 3 +- 2 files changed, 1 insertion(+), 2073 deletions(-) delete mode 100644 usermods/audioreactive/audio_reactive.old.h diff --git a/usermods/audioreactive/audio_reactive.old.h b/usermods/audioreactive/audio_reactive.old.h deleted file mode 100644 index 4f2e04c08..000000000 --- a/usermods/audioreactive/audio_reactive.old.h +++ /dev/null @@ -1,2071 +0,0 @@ -#pragma once - -#include "wled.h" - -#ifdef ARDUINO_ARCH_ESP32 - -#include -#include - -#ifdef WLED_ENABLE_DMX - #error This audio reactive usermod is not compatible with DMX Out. -#endif - -#endif - -#if defined(ARDUINO_ARCH_ESP32) && (defined(WLED_DEBUG) || defined(SR_DEBUG)) -#include -#endif - -/* - * Usermods allow you to add own functionality to WLED more easily - * See: https://github.com/Aircoookie/WLED/wiki/Add-own-functionality - * - * This is an audioreactive v2 usermod. - * .... - */ - -#if !defined(FFTTASK_PRIORITY) -#define FFTTASK_PRIORITY 1 // standard: looptask prio -//#define FFTTASK_PRIORITY 2 // above looptask, below asyc_tcp -//#define FFTTASK_PRIORITY 4 // above asyc_tcp -#endif - -// Comment/Uncomment to toggle usb serial debugging -// #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) DEBUGOUT.print(x) - #define DEBUGSR_PRINTLN(x) DEBUGOUT.println(x) - #define DEBUGSR_PRINTF(x...) DEBUGOUT.printf(x) -#else - #define DEBUGSR_PRINT(x) - #define DEBUGSR_PRINTLN(x) - #define DEBUGSR_PRINTF(x...) -#endif - -#if defined(MIC_LOGGER) || defined(FFT_SAMPLING_LOG) - #define PLOT_PRINT(x) DEBUGOUT.print(x) - #define PLOT_PRINTLN(x) DEBUGOUT.println(x) - #define PLOT_PRINTF(x...) DEBUGOUT.printf(x) -#else - #define PLOT_PRINT(x) - #define PLOT_PRINTLN(x) - #define PLOT_PRINTF(x...) -#endif - -#define MAX_PALETTES 3 - -static volatile bool disableSoundProcessing = false; // if true, sound processing (FFT, filters, AGC) will be suspended. "volatile" as its shared between tasks. -static uint8_t audioSyncEnabled = 0; // bit field: bit 0 - send, bit 1 - receive (config value) -static bool udpSyncConnected = false; // UDP connection status -> true if connected to multicast group - -#define NUM_GEQ_CHANNELS 16 // number of frequency channels. Don't change !! - -// audioreactive variables -#ifdef ARDUINO_ARCH_ESP32 -static float micDataReal = 0.0f; // MicIn data with full 24bit resolution - lowest 8bit after decimal point -static float multAgc = 1.0f; // sample * multAgc = sampleAgc. Our AGC multiplier -static float sampleAvg = 0.0f; // Smoothed Average sample - sampleAvg < 1 means "quiet" (simple noise gate) -static float sampleAgc = 0.0f; // Smoothed AGC sample -static uint8_t soundAgc = 0; // Automagic gain control: 0 - none, 1 - normal, 2 - vivid, 3 - lazy (config value) -#endif -//static float volumeSmth = 0.0f; // either sampleAvg or sampleAgc depending on soundAgc; smoothed sample -static float FFT_MajorPeak = 1.0f; // FFT: strongest (peak) frequency -static float FFT_Magnitude = 0.0f; // FFT: volume (magnitude) of peak frequency -static bool samplePeak = false; // Boolean flag for peak - used in effects. Responding routine may reset this flag. Auto-reset after WS2812FX::getMinShowDelay() -static bool udpSamplePeak = false; // Boolean flag for peak. Set at the same time as samplePeak, but reset by transmitAudioData -static unsigned long timeOfPeak = 0; // time of last sample peak detection. -static uint8_t fftResult[NUM_GEQ_CHANNELS]= {0};// Our calculated freq. channel result table to be used by effects - -// 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 - -// user settable parameters for limitSoundDynamics() -#ifdef UM_AUDIOREACTIVE_DYNAMICS_LIMITER_OFF -static bool limiterOn = false; // bool: enable / disable dynamics limiter -#else -static bool limiterOn = true; -#endif -static uint16_t attackTime = 80; // int: attack time in milliseconds. Default 0.08sec -static uint16_t decayTime = 1400; // int: decay time in milliseconds. Default 1.40sec - -// peak detection -#ifdef ARDUINO_ARCH_ESP32 -static void detectSamplePeak(void); // peak detection function (needs scaled FFT results in vReal[]) - no used for 8266 receive-only mode -#endif -static void autoResetPeak(void); // peak auto-reset function -static uint8_t maxVol = 31; // (was 10) Reasonable value for constant volume for 'peak detector', as it won't always trigger (deprecated) -static uint8_t binNum = 8; // Used to select the bin for FFT based beat detection (deprecated) - -#ifdef ARDUINO_ARCH_ESP32 - -// use audio source class (ESP32 specific) -#include "audio_source.h" -constexpr i2s_port_t I2S_PORT = I2S_NUM_0; // I2S port to use (do not change !) -constexpr int BLOCK_SIZE = 128; // I2S buffer size (samples) - -// globals -static uint8_t inputLevel = 128; // UI slider value -#ifndef SR_SQUELCH - uint8_t soundSquelch = 10; // squelch value for volume reactive routines (config value) -#else - uint8_t soundSquelch = SR_SQUELCH; // squelch value for volume reactive routines (config value) -#endif -#ifndef SR_GAIN - uint8_t sampleGain = 60; // sample gain (config value) -#else - uint8_t sampleGain = SR_GAIN; // sample gain (config value) -#endif -// user settable options for FFTResult scaling -static uint8_t FFTScalingMode = 3; // 0 none; 1 optimized logarithmic; 2 optimized linear; 3 optimized square root - -// -// AGC presets -// Note: in C++, "const" implies "static" - no need to explicitly declare everything as "static const" -// -#define AGC_NUM_PRESETS 3 // AGC presets: normal, vivid, lazy -const double agcSampleDecay[AGC_NUM_PRESETS] = { 0.9994f, 0.9985f, 0.9997f}; // decay factor for sampleMax, in case the current sample is below sampleMax -const float agcZoneLow[AGC_NUM_PRESETS] = { 32, 28, 36}; // low volume emergency zone -const float agcZoneHigh[AGC_NUM_PRESETS] = { 240, 240, 248}; // high volume emergency zone -const float agcZoneStop[AGC_NUM_PRESETS] = { 336, 448, 304}; // disable AGC integrator if we get above this level -const float agcTarget0[AGC_NUM_PRESETS] = { 112, 144, 164}; // first AGC setPoint -> between 40% and 65% -const float agcTarget0Up[AGC_NUM_PRESETS] = { 88, 64, 116}; // setpoint switching value (a poor man's bang-bang) -const float agcTarget1[AGC_NUM_PRESETS] = { 220, 224, 216}; // second AGC setPoint -> around 85% -const double agcFollowFast[AGC_NUM_PRESETS] = { 1/192.f, 1/128.f, 1/256.f}; // quickly follow setpoint - ~0.15 sec -const double agcFollowSlow[AGC_NUM_PRESETS] = {1/6144.f,1/4096.f,1/8192.f}; // slowly follow setpoint - ~2-15 secs -const double agcControlKp[AGC_NUM_PRESETS] = { 0.6f, 1.5f, 0.65f}; // AGC - PI control, proportional gain parameter -const double agcControlKi[AGC_NUM_PRESETS] = { 1.7f, 1.85f, 1.2f}; // AGC - PI control, integral gain parameter -const float agcSampleSmooth[AGC_NUM_PRESETS] = { 1/12.f, 1/6.f, 1/16.f}; // smoothing factor for sampleAgc (use rawSampleAgc if you want the non-smoothed value) -// AGC presets end - -static AudioSource *audioSource = nullptr; -static bool useBandPassFilter = false; // if true, enables a bandpass filter 80Hz-16Khz to remove noise. Applies before FFT. - -//////////////////// -// Begin FFT Code // -//////////////////// - -// some prototypes, to ensure consistent interfaces -static float mapf(float x, float in_min, float in_max, float out_min, float out_max); // map function for float -static float fftAddAvg(int from, int to); // average of several FFT result bins -static void FFTcode(void * parameter); // audio processing task: read samples, run FFT, fill GEQ channels from FFT results -static void runMicFilter(uint16_t numSamples, float *sampleBuffer); // pre-filtering of raw samples (band-pass) -static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels); // post-processing and post-amp of GEQ channels - -static TaskHandle_t FFT_Task = nullptr; - -// Table of multiplication factors so that we can even out the frequency response. -static float fftResultPink[NUM_GEQ_CHANNELS] = { 1.70f, 1.71f, 1.73f, 1.78f, 1.68f, 1.56f, 1.55f, 1.63f, 1.79f, 1.62f, 1.80f, 2.06f, 2.47f, 3.35f, 6.83f, 9.55f }; - -// globals and FFT Output variables shared with animations -#if defined(WLED_DEBUG) || defined(SR_DEBUG) -static uint64_t fftTime = 0; -static uint64_t sampleTime = 0; -#endif - -// FFT Task variables (filtering and post-processing) -static float fftCalc[NUM_GEQ_CHANNELS] = {0.0f}; // Try and normalize fftBin values to a max of 4096, so that 4096/16 = 256. -static float fftAvg[NUM_GEQ_CHANNELS] = {0.0f}; // Calculated frequency channel results, with smoothing (used if dynamics limiter is ON) -#ifdef SR_DEBUG -static float fftResultMax[NUM_GEQ_CHANNELS] = {0.0f}; // A table used for testing to determine how our post-processing is working. -#endif - -// audio source parameters and constant -#ifdef ARDUINO_ARCH_ESP32C3 -constexpr SRate_t SAMPLE_RATE = 16000; // 16kHz - use if FFTtask takes more than 20ms. Physical sample time -> 32ms -#define FFT_MIN_CYCLE 30 // Use with 16Khz sampling -#else -constexpr SRate_t SAMPLE_RATE = 22050; // Base sample rate in Hz - 22Khz is a standard rate. Physical sample time -> 23ms -//constexpr SRate_t SAMPLE_RATE = 16000; // 16kHz - use if FFTtask takes more than 20ms. Physical sample time -> 32ms -//constexpr SRate_t SAMPLE_RATE = 20480; // Base sample rate in Hz - 20Khz is experimental. Physical sample time -> 25ms -//constexpr SRate_t SAMPLE_RATE = 10240; // Base sample rate in Hz - previous default. Physical sample time -> 50ms -#define FFT_MIN_CYCLE 21 // minimum time before FFT task is repeated. Use with 22Khz sampling -//#define FFT_MIN_CYCLE 30 // Use with 16Khz sampling -//#define FFT_MIN_CYCLE 23 // minimum time before FFT task is repeated. Use with 20Khz sampling -//#define FFT_MIN_CYCLE 46 // minimum time before FFT task is repeated. Use with 10Khz sampling -#endif - -// FFT Constants -constexpr uint16_t samplesFFT = 512; // Samples in an FFT batch - This value MUST ALWAYS be a power of 2 -constexpr uint16_t samplesFFT_2 = 256; // meaningfull part of FFT results - only the "lower half" contains useful information. -// the following are observed values, supported by a bit of "educated guessing" -//#define FFT_DOWNSCALE 0.65f // 20kHz - downscaling factor for FFT results - "Flat-Top" window @20Khz, old freq channels -#define FFT_DOWNSCALE 0.46f // downscaling factor for FFT results - for "Flat-Top" window @22Khz, new freq channels -#define LOG_256 5.54517744f // log(256) - -// These are the input and output vectors. Input vectors receive computed results from FFT. -static float vReal[samplesFFT] = {0.0f}; // FFT sample inputs / freq output - these are our raw result bins -static float vImag[samplesFFT] = {0.0f}; // imaginary parts - -// Create FFT object -// lib_deps += https://github.com/kosme/arduinoFFT#develop @ 2.0.1 -// these options actually cause slow-downs on all esp32 processors, don't use them. -// #define FFT_SPEED_OVER_PRECISION // enables use of reciprocals (1/x etc) - not faster on ESP32 -// #define FFT_SQRT_APPROXIMATION // enables "quake3" style inverse sqrt - slower on ESP32 -// Below options are forcing ArduinoFFT to use sqrtf() instead of sqrt() -#define sqrt(x) sqrtf(x) // little hack that reduces FFT time by 10-50% on ESP32 -#define sqrt_internal sqrtf // see https://github.com/kosme/arduinoFFT/pull/83 - -#include - -/* Create FFT object with weighing factor storage */ -static ArduinoFFT FFT = ArduinoFFT( vReal, vImag, samplesFFT, SAMPLE_RATE, true); - -// Helper functions - -// float version of map() -static float mapf(float x, float in_min, float in_max, float out_min, float out_max){ - return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; -} - -// compute average of several FFT result bins -static float fftAddAvg(int from, int to) { - float result = 0.0f; - for (int i = from; i <= to; i++) { - result += vReal[i]; - } - return result / float(to - from + 1); -} - -// -// FFT main task -// -void FFTcode(void * parameter) -{ - DEBUGSR_PRINT("FFT started on core: "); DEBUGSR_PRINTLN(xPortGetCoreID()); - - // see https://www.freertos.org/vtaskdelayuntil.html - const TickType_t xFrequency = FFT_MIN_CYCLE * portTICK_PERIOD_MS; - - TickType_t xLastWakeTime = xTaskGetTickCount(); - 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. - - // Don't run FFT computing code if we're in Receive mode or in realtime mode - if (disableSoundProcessing || (audioSyncEnabled & 0x02)) { - vTaskDelayUntil( &xLastWakeTime, xFrequency); // release CPU, and let I2S fill its buffers - continue; - } - -#if defined(WLED_DEBUG) || defined(SR_DEBUG) - uint64_t start = esp_timer_get_time(); - bool haveDoneFFT = false; // indicates if second measurement (FFT time) is valid -#endif - - // get a fresh batch of samples from I2S - if (audioSource) audioSource->getSamples(vReal, samplesFFT); - -#if defined(WLED_DEBUG) || defined(SR_DEBUG) - if (start < esp_timer_get_time()) { // filter out overflows - uint64_t sampleTimeInMillis = (esp_timer_get_time() - start +5ULL) / 10ULL; // "+5" to ensure proper rounding - sampleTime = (sampleTimeInMillis*3 + sampleTime*7)/10; // smooth - } - start = esp_timer_get_time(); // start measuring FFT time -#endif - - xLastWakeTime = xTaskGetTickCount(); // update "last unblocked time" for vTaskDelay - - // band pass filter - can reduce noise floor by a factor of 50 - // downside: frequencies below 100Hz will be ignored - if (useBandPassFilter) runMicFilter(samplesFFT, vReal); - - // find highest sample in the batch - float maxSample = 0.0f; // max sample from 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 (fabsf((float)vReal[i]) > maxSample) maxSample = fabsf((float)vReal[i]); - } - // 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; - -#ifdef SR_DEBUG - if (true) { // this allows measure FFT runtimes, as it disables the "only when needed" optimization -#else - if (sampleAvg > 0.25f) { // noise gate open means that FFT results will be used. Don't run FFT if results are not needed. -#endif - - // run FFT (takes 3-5ms on ESP32, ~12ms on ESP32-S2) - FFT.dcRemoval(); // remove DC offset - FFT.windowing( FFTWindow::Flat_top, FFTDirection::Forward); // Weigh data using "Flat Top" function - better amplitude accuracy - //FFT.windowing(FFTWindow::Blackman_Harris, FFTDirection::Forward); // Weigh data using "Blackman- Harris" window - sharp peaks due to excellent sideband rejection - FFT.compute( FFTDirection::Forward ); // Compute FFT - FFT.complexToMagnitude(); // Compute magnitudes - vReal[0] = 0.0f; // The remaining DC offset on the signal produces a strong spike on position 0 that should be eliminated to avoid issues. - - FFT.majorPeak(&FFT_MajorPeak, &FFT_Magnitude); // let the effects know which freq was most dominant - FFT_MajorPeak = constrain(FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects - -#if defined(WLED_DEBUG) || defined(SR_DEBUG) - haveDoneFFT = true; -#endif - - } else { // noise gate closed - only clear results as FFT was skipped. MIC samples are still valid when we do this. - memset(vReal, 0, sizeof(vReal)); - FFT_MajorPeak = 1.0f; - FFT_Magnitude = 0.001f; - } - - for (int i = 0; i < samplesFFT; i++) { - float t = fabsf(vReal[i]); // just to be sure - values in fft bins should be positive any way - vReal[i] = t / 16.0f; // Reduce magnitude. Want end result to be scaled linear and ~4096 max. - } // for() - - // mapping of FFT result bins to frequency channels - if (fabsf(sampleAvg) > 0.5f) { // noise gate open -#if 0 - /* 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, 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 determine the bins. - * End frequency = Start frequency * multiplier ^ 16 - * Multiplier = (End frequency/ Start frequency) ^ 1/16 - * Multiplier = 1.320367784 - */ // Range - fftCalc[ 0] = fftAddAvg(2,4); // 60 - 100 - fftCalc[ 1] = fftAddAvg(4,5); // 80 - 120 - fftCalc[ 2] = fftAddAvg(5,7); // 100 - 160 - fftCalc[ 3] = fftAddAvg(7,9); // 140 - 200 - fftCalc[ 4] = fftAddAvg(9,12); // 180 - 260 - fftCalc[ 5] = fftAddAvg(12,16); // 240 - 340 - fftCalc[ 6] = fftAddAvg(16,21); // 320 - 440 - fftCalc[ 7] = fftAddAvg(21,29); // 420 - 600 - fftCalc[ 8] = fftAddAvg(29,37); // 580 - 760 - fftCalc[ 9] = fftAddAvg(37,48); // 740 - 980 - fftCalc[10] = fftAddAvg(48,64); // 960 - 1300 - fftCalc[11] = fftAddAvg(64,84); // 1280 - 1700 - fftCalc[12] = fftAddAvg(84,111); // 1680 - 2240 - fftCalc[13] = fftAddAvg(111,147); // 2220 - 2960 - fftCalc[14] = fftAddAvg(147,194); // 2940 - 3900 - fftCalc[15] = fftAddAvg(194,250); // 3880 - 5000 // avoid the last 5 bins, which are usually inaccurate -#else - /* new mapping, optimized for 22050 Hz by softhack007 */ - // bins frequency range - if (useBandPassFilter) { - // skip frequencies below 100hz - fftCalc[ 0] = 0.8f * fftAddAvg(3,4); - fftCalc[ 1] = 0.9f * fftAddAvg(4,5); - fftCalc[ 2] = fftAddAvg(5,6); - fftCalc[ 3] = fftAddAvg(6,7); - // don't use the last bins from 206 to 255. - fftCalc[15] = fftAddAvg(165,205) * 0.75f; // 40 7106 - 8828 high -- with some damping - } else { - fftCalc[ 0] = fftAddAvg(1,2); // 1 43 - 86 sub-bass - fftCalc[ 1] = fftAddAvg(2,3); // 1 86 - 129 bass - fftCalc[ 2] = fftAddAvg(3,5); // 2 129 - 216 bass - fftCalc[ 3] = fftAddAvg(5,7); // 2 216 - 301 bass + midrange - // don't use the last bins from 216 to 255. They are usually contaminated by aliasing (aka noise) - fftCalc[15] = fftAddAvg(165,215) * 0.70f; // 50 7106 - 9259 high -- with some damping - } - fftCalc[ 4] = fftAddAvg(7,10); // 3 301 - 430 midrange - fftCalc[ 5] = fftAddAvg(10,13); // 3 430 - 560 midrange - fftCalc[ 6] = fftAddAvg(13,19); // 5 560 - 818 midrange - fftCalc[ 7] = fftAddAvg(19,26); // 7 818 - 1120 midrange -- 1Khz should always be the center ! - fftCalc[ 8] = fftAddAvg(26,33); // 7 1120 - 1421 midrange - fftCalc[ 9] = fftAddAvg(33,44); // 9 1421 - 1895 midrange - fftCalc[10] = fftAddAvg(44,56); // 12 1895 - 2412 midrange + high mid - fftCalc[11] = fftAddAvg(56,70); // 14 2412 - 3015 high mid - fftCalc[12] = fftAddAvg(70,86); // 16 3015 - 3704 high mid - fftCalc[13] = fftAddAvg(86,104); // 18 3704 - 4479 high mid - fftCalc[14] = fftAddAvg(104,165) * 0.88f; // 61 4479 - 7106 high mid + high -- with slight damping -#endif - } else { // noise gate closed - just decay old values - for (int i=0; i < NUM_GEQ_CHANNELS; i++) { - fftCalc[i] *= 0.85f; // decay to zero - if (fftCalc[i] < 4.0f) fftCalc[i] = 0.0f; - } - } - - // post-processing of frequency channels (pink noise adjustment, AGC, smoothing, scaling) - postProcessFFTResults((fabsf(sampleAvg) > 0.25f)? true : false , NUM_GEQ_CHANNELS); - -#if defined(WLED_DEBUG) || defined(SR_DEBUG) - if (haveDoneFFT && (start < esp_timer_get_time())) { // filter out overflows - uint64_t fftTimeInMillis = ((esp_timer_get_time() - start) +5ULL) / 10ULL; // "+5" to ensure proper rounding - fftTime = (fftTimeInMillis*3 + fftTime*7)/10; // smooth - } -#endif - // run peak detection - autoResetPeak(); - detectSamplePeak(); - - #if !defined(I2S_GRAB_ADC1_COMPLETELY) - if ((audioSource == nullptr) || (audioSource->getType() != AudioSource::Type_I2SAdc)) // the "delay trick" does not help for analog ADC - #endif - vTaskDelayUntil( &xLastWakeTime, xFrequency); // release CPU, and let I2S fill its buffers - - } // for(;;)ever -} // FFTcode() task end - - -/////////////////////////// -// Pre / Postprocessing // -/////////////////////////// - -static void runMicFilter(uint16_t numSamples, float *sampleBuffer) // pre-filtering of raw samples (band-pass) -{ - // low frequency cutoff parameter - see https://dsp.stackexchange.com/questions/40462/exponential-moving-average-cut-off-frequency - //constexpr float alpha = 0.04f; // 150Hz - //constexpr float alpha = 0.03f; // 110Hz - constexpr float alpha = 0.0225f; // 80hz - //constexpr float alpha = 0.01693f;// 60hz - // high frequency cutoff parameter - //constexpr float beta1 = 0.75f; // 11Khz - //constexpr float beta1 = 0.82f; // 15Khz - //constexpr float beta1 = 0.8285f; // 18Khz - constexpr float beta1 = 0.85f; // 20Khz - - constexpr float beta2 = (1.0f - beta1) / 2.0f; - static float last_vals[2] = { 0.0f }; // FIR high freq cutoff filter - static float lowfilt = 0.0f; // IIR low frequency cutoff filter - - for (int i=0; i < numSamples; i++) { - // FIR lowpass, to remove high frequency noise - float highFilteredSample; - if (i < (numSamples-1)) highFilteredSample = beta1*sampleBuffer[i] + beta2*last_vals[0] + beta2*sampleBuffer[i+1]; // smooth out spikes - else highFilteredSample = beta1*sampleBuffer[i] + beta2*last_vals[0] + beta2*last_vals[1]; // special handling for last sample in array - last_vals[1] = last_vals[0]; - last_vals[0] = sampleBuffer[i]; - sampleBuffer[i] = highFilteredSample; - // IIR highpass, to remove low frequency noise - lowfilt += alpha * (sampleBuffer[i] - lowfilt); - sampleBuffer[i] = sampleBuffer[i] - lowfilt; - } -} - -static void postProcessFFTResults(bool noiseGateOpen, int numberOfChannels) // post-processing and post-amp of GEQ channels -{ - for (int i=0; i < numberOfChannels; i++) { - - if (noiseGateOpen) { // noise gate open - // Adjustment for frequency curves. - fftCalc[i] *= fftResultPink[i]; - if (FFTScalingMode > 0) fftCalc[i] *= FFT_DOWNSCALE; // adjustment related to FFT windowing function - // Manual linear adjustment of gain using sampleGain adjustment for different input types. - fftCalc[i] *= soundAgc ? multAgc : ((float)sampleGain/40.0f * (float)inputLevel/128.0f + 1.0f/16.0f); //apply gain, with inputLevel adjustment - if(fftCalc[i] < 0) fftCalc[i] = 0.0f; - } - - // smooth results - rise fast, fall slower - if (fftCalc[i] > fftAvg[i]) fftAvg[i] = fftCalc[i]*0.75f + 0.25f*fftAvg[i]; // rise fast; will need approx 2 cycles (50ms) for converging against fftCalc[i] - else { // fall slow - if (decayTime < 1000) fftAvg[i] = fftCalc[i]*0.22f + 0.78f*fftAvg[i]; // approx 5 cycles (225ms) for falling to zero - else if (decayTime < 2000) fftAvg[i] = fftCalc[i]*0.17f + 0.83f*fftAvg[i]; // default - approx 9 cycles (225ms) for falling to zero - else if (decayTime < 3000) fftAvg[i] = fftCalc[i]*0.14f + 0.86f*fftAvg[i]; // approx 14 cycles (350ms) for falling to zero - else fftAvg[i] = fftCalc[i]*0.1f + 0.9f*fftAvg[i]; // approx 20 cycles (500ms) for falling to zero - } - // constrain internal vars - just to be sure - fftCalc[i] = constrain(fftCalc[i], 0.0f, 1023.0f); - fftAvg[i] = constrain(fftAvg[i], 0.0f, 1023.0f); - - float currentResult; - if(limiterOn == true) - currentResult = fftAvg[i]; - else - currentResult = fftCalc[i]; - - switch (FFTScalingMode) { - case 1: - // Logarithmic scaling - currentResult *= 0.42f; // 42 is the answer ;-) - currentResult -= 8.0f; // this skips the lowest row, giving some room for peaks - if (currentResult > 1.0f) currentResult = logf(currentResult); // log to base "e", which is the fastest log() function - else currentResult = 0.0f; // special handling, because log(1) = 0; log(0) = undefined - currentResult *= 0.85f + (float(i)/18.0f); // extra up-scaling for high frequencies - currentResult = mapf(currentResult, 0.0f, LOG_256, 0.0f, 255.0f); // map [log(1) ... log(255)] to [0 ... 255] - break; - case 2: - // Linear scaling - currentResult *= 0.30f; // needs a bit more damping, get stay below 255 - currentResult -= 4.0f; // giving a bit more room for peaks (WLEDMM uses -2) - if (currentResult < 1.0f) currentResult = 0.0f; - currentResult *= 0.85f + (float(i)/1.8f); // extra up-scaling for high frequencies - break; - case 3: - // square root scaling - currentResult *= 0.38f; - currentResult -= 6.0f; - if (currentResult > 1.0f) currentResult = sqrtf(currentResult); - else currentResult = 0.0f; // special handling, because sqrt(0) = undefined - currentResult *= 0.85f + (float(i)/4.5f); // extra up-scaling for high frequencies - currentResult = mapf(currentResult, 0.0f, 16.0f, 0.0f, 255.0f); // map [sqrt(1) ... sqrt(256)] to [0 ... 255] - break; - - case 0: - default: - // no scaling - leave freq bins as-is - currentResult -= 4; // just a bit more room for peaks (WLEDMM uses -2) - break; - } - - // Now, let's dump it all into fftResult. Need to do this, otherwise other routines might grab fftResult values prematurely. - if (soundAgc > 0) { // apply extra "GEQ Gain" if set by user - float post_gain = (float)inputLevel/128.0f; - if (post_gain < 1.0f) post_gain = ((post_gain -1.0f) * 0.8f) +1.0f; - currentResult *= post_gain; - } - fftResult[i] = constrain((int)currentResult, 0, 255); - } -} -//////////////////// -// Peak detection // -//////////////////// - -// peak detection is called from FFT task when vReal[] contains valid FFT results -static void detectSamplePeak(void) { - bool havePeak = false; - // softhack007: this code continuously triggers while amplitude in the selected bin is above a certain threshold. So it does not detect peaks - it detects high activity in a frequency bin. - // Poor man's beat detection by seeing if sample > Average + some value. - // This goes through ALL of the 255 bins - but ignores stupid settings - // 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. - if ((sampleAvg > 1) && (maxVol > 0) && (binNum > 4) && (vReal[binNum] > maxVol) && ((millis() - timeOfPeak) > 100)) { - havePeak = true; - } - - if (havePeak) { - samplePeak = true; - timeOfPeak = millis(); - udpSamplePeak = true; - } -} - -#endif - -static void autoResetPeak(void) { - uint16_t MinShowDelay = MAX(50, WS2812FX::getMinShowDelay()); // Fixes private class variable compiler error. Unsure if this is the correct way of fixing the root problem. -THATDONFC - if (millis() - timeOfPeak > MinShowDelay) { // Auto-reset of samplePeak after a complete frame has passed. - samplePeak = false; - if (audioSyncEnabled == 0) udpSamplePeak = false; // this is normally reset by transmitAudioData - } -} - - -//////////////////// -// usermod class // -//////////////////// - -//class name. Use something descriptive and leave the ": public Usermod" part :) -class AudioReactive : public Usermod { - - private: -#ifdef ARDUINO_ARCH_ESP32 - - #ifndef AUDIOPIN - int8_t audioPin = -1; - #else - int8_t audioPin = AUDIOPIN; - #endif - #ifndef SR_DMTYPE // I2S mic type - uint8_t dmType = 1; // 0=none/disabled/analog; 1=generic I2S - #define SR_DMTYPE 1 // default type = I2S - #else - uint8_t dmType = SR_DMTYPE; - #endif - #ifndef I2S_SDPIN // aka DOUT - int8_t i2ssdPin = 32; - #else - int8_t i2ssdPin = I2S_SDPIN; - #endif - #ifndef I2S_WSPIN // aka LRCL - int8_t i2swsPin = 15; - #else - int8_t i2swsPin = I2S_WSPIN; - #endif - #ifndef I2S_CKPIN // aka BCLK - int8_t i2sckPin = 14; /*PDM: set to I2S_PIN_NO_CHANGE*/ - #else - int8_t i2sckPin = I2S_CKPIN; - #endif - #ifndef MCLK_PIN - int8_t mclkPin = I2S_PIN_NO_CHANGE; /* ESP32: only -1, 0, 1, 3 allowed*/ - #else - int8_t mclkPin = MCLK_PIN; - #endif -#endif - - // new "V2" audiosync struct - 44 Bytes - struct __attribute__ ((packed)) audioSyncPacket { // "packed" ensures that there are no additional gaps - char header[6]; // 06 Bytes offset 0 - uint8_t reserved1[2]; // 02 Bytes, offset 6 - gap required by the compiler - not used yet - float sampleRaw; // 04 Bytes offset 8 - either "sampleRaw" or "rawSampleAgc" depending on soundAgc setting - float sampleSmth; // 04 Bytes offset 12 - either "sampleAvg" or "sampleAgc" depending on soundAgc setting - uint8_t samplePeak; // 01 Bytes offset 16 - 0 no peak; >=1 peak detected. In future, this will also provide peak Magnitude - uint8_t reserved2; // 01 Bytes offset 17 - for future extensions - not used yet - uint8_t fftResult[16]; // 16 Bytes offset 18 - uint16_t reserved3; // 02 Bytes, offset 34 - gap required by the compiler - not used yet - float FFT_Magnitude; // 04 Bytes offset 36 - float FFT_MajorPeak; // 04 Bytes offset 40 - }; - - // old "V1" audiosync struct - 83 Bytes payload, 88 bytes total (with padding added by compiler) - for backwards compatibility - struct audioSyncPacket_v1 { - char header[6]; // 06 Bytes - uint8_t myVals[32]; // 32 Bytes - int sampleAgc; // 04 Bytes - int sampleRaw; // 04 Bytes - float sampleAvg; // 04 Bytes - bool samplePeak; // 01 Bytes - uint8_t fftResult[16]; // 16 Bytes - double FFT_Magnitude; // 08 Bytes - double FFT_MajorPeak; // 08 Bytes - }; - - constexpr static unsigned UDPSOUND_MAX_PACKET = MAX(sizeof(audioSyncPacket), sizeof(audioSyncPacket_v1)); - - // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer) - #ifdef UM_AUDIOREACTIVE_ENABLE - bool enabled = true; - #else - bool enabled = false; - #endif - - bool initDone = false; - bool addPalettes = false; - int8_t palettes = 0; - - // variables for UDP sound sync - WiFiUDP fftUdp; // UDP object for sound sync (from WiFi UDP, not Async UDP!) - unsigned long lastTime = 0; // last time of running UDP Microphone Sync - const uint16_t delayMs = 10; // I don't want to sample too often and overload WLED - uint16_t audioSyncPort= 11988;// default port for UDP sound sync - - bool updateIsRunning = false; // true during OTA. - -#ifdef ARDUINO_ARCH_ESP32 - // used for AGC - int last_soundAgc = -1; // used to detect AGC mode change (for resetting AGC internal error buffers) - float control_integrated = 0.0f; // persistent across calls to agcAvg(); "integrator control" = accumulated error - // variables used by getSample() and agcAvg() - int16_t micIn = 0; // Current sample starts with negative values and large values, which is why it's 16 bit signed - float sampleMax = 0.0f; // Max sample over a few seconds. Needed for AGC controller. - float micLev = 0.0f; // Used to convert returned value to have '0' as minimum. A leveller - float expAdjF = 0.0f; // Used for exponential filter. - float sampleReal = 0.0f; // "sampleRaw" as float, to provide bits that are lost otherwise (before amplification by sampleGain or inputLevel). Needed for AGC. - int16_t sampleRaw = 0; // Current sample. Must only be updated ONCE!!! (amplified mic value by sampleGain and inputLevel) - int16_t rawSampleAgc = 0; // not smoothed AGC sample -#endif - - // variables used in effects - float volumeSmth = 0.0f; // either sampleAvg or sampleAgc depending on soundAgc; smoothed sample - int16_t volumeRaw = 0; // either sampleRaw or rawSampleAgc depending on soundAgc - float my_magnitude =0.0f; // FFT_Magnitude, scaled by multAgc - - // used to feed "Info" Page - unsigned long last_UDPTime = 0; // time of last valid UDP sound sync datapacket - int receivedFormat = 0; // last received UDP sound sync format - 0=none, 1=v1 (0.13.x), 2=v2 (0.14.x) - float maxSample5sec = 0.0f; // max sample (after AGC) in last 5 seconds - unsigned long sampleMaxTimer = 0; // last time maxSample5sec was reset - #define CYCLE_SAMPLEMAX 3500 // time window for merasuring - - // strings to reduce flash memory usage (used more than twice) - static const char _name[]; - static const char _enabled[]; - static const char _config[]; - static const char _dynamics[]; - static const char _frequency[]; - static const char _inputLvl[]; -#if defined(ARDUINO_ARCH_ESP32) && !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) - static const char _analogmic[]; -#endif - static const char _digitalmic[]; - static const char _addPalettes[]; - static const char UDP_SYNC_HEADER[]; - static const char UDP_SYNC_HEADER_v1[]; - - // private methods - void removeAudioPalettes(void); - void createAudioPalettes(void); - CRGB getCRGBForBand(int x, int pal); - void fillAudioPalettes(void); - - //////////////////// - // Debug support // - //////////////////// - void logAudio() - { - if (disableSoundProcessing && (!udpSyncConnected || ((audioSyncEnabled & 0x02) == 0))) return; // no audio availeable - #ifdef MIC_LOGGER - // Debugging functions for audio input and sound processing. Comment out the values you want to see - PLOT_PRINT("micReal:"); PLOT_PRINT(micDataReal); PLOT_PRINT("\t"); - PLOT_PRINT("volumeSmth:"); PLOT_PRINT(volumeSmth); PLOT_PRINT("\t"); - //PLOT_PRINT("volumeRaw:"); PLOT_PRINT(volumeRaw); PLOT_PRINT("\t"); - PLOT_PRINT("DC_Level:"); PLOT_PRINT(micLev); PLOT_PRINT("\t"); - //PLOT_PRINT("sampleAgc:"); PLOT_PRINT(sampleAgc); PLOT_PRINT("\t"); - //PLOT_PRINT("sampleAvg:"); PLOT_PRINT(sampleAvg); PLOT_PRINT("\t"); - //PLOT_PRINT("sampleReal:"); PLOT_PRINT(sampleReal); PLOT_PRINT("\t"); - #ifdef ARDUINO_ARCH_ESP32 - //PLOT_PRINT("micIn:"); PLOT_PRINT(micIn); PLOT_PRINT("\t"); - //PLOT_PRINT("sample:"); PLOT_PRINT(sample); PLOT_PRINT("\t"); - //PLOT_PRINT("sampleMax:"); PLOT_PRINT(sampleMax); PLOT_PRINT("\t"); - //PLOT_PRINT("samplePeak:"); PLOT_PRINT((samplePeak!=0) ? 128:0); PLOT_PRINT("\t"); - //PLOT_PRINT("multAgc:"); PLOT_PRINT(multAgc, 4); PLOT_PRINT("\t"); - #endif - PLOT_PRINTLN(); - #endif - - #ifdef FFT_SAMPLING_LOG - #if 0 - for(int i=0; i maxVal) maxVal = fftResult[i]; - if(fftResult[i] < minVal) minVal = fftResult[i]; - } - for(int i = 0; i < NUM_GEQ_CHANNELS; i++) { - PLOT_PRINT(i); PLOT_PRINT(":"); - PLOT_PRINTF("%04ld ", map(fftResult[i], 0, (scaleValuesFromCurrentMaxVal ? maxVal : defaultScalingFromHighValue), (mapValuesToPlotterSpace*i*scalingToHighValue)+0, (mapValuesToPlotterSpace*i*scalingToHighValue)+scalingToHighValue-1)); - } - if(printMaxVal) { - PLOT_PRINTF("maxVal:%04d ", maxVal + (mapValuesToPlotterSpace ? 16*256 : 0)); - } - if(printMinVal) { - PLOT_PRINTF("%04d:minVal ", minVal); // printed with value first, then label, so negative values can be seen in Serial Monitor but don't throw off y axis in Serial Plotter - } - if(mapValuesToPlotterSpace) - PLOT_PRINTF("max:%04d ", (printMaxVal ? 17 : 16)*256); // print line above the maximum value we expect to see on the plotter to avoid autoscaling y axis - else { - PLOT_PRINTF("max:%04d ", 256); - } - PLOT_PRINTLN(); - #endif // FFT_SAMPLING_LOG - } // logAudio() - - -#ifdef ARDUINO_ARCH_ESP32 - ////////////////////// - // Audio Processing // - ////////////////////// - - /* - * A "PI controller" 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 zone (<10% or >90%) - very fast adjustment - */ - 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 - - float lastMultAgc = multAgc; // last multiplier used - float multAgcTemp = multAgc; // new multiplier - float tmpAgc = sampleReal * multAgc; // what-if amplified signal - - float control_error; // "control error" input for PI control - - if (last_soundAgc != soundAgc) control_integrated = 0.0f; // new preset - reset integrator - - // For PI controller, we need to have a constant "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 ((fabsf(sampleReal) < 2.0f) || (sampleMax < 1.0f)) { - // MIC signal is "squelched" - deliver silence - tmpAgc = 0; - // we need to "spin down" the intgrated error buffer - if (fabs(control_integrated) < 0.01f) control_integrated = 0.0f; - else control_integrated *= 0.91f; - } 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.0f) multAgcTemp = 32.0f; - if (multAgcTemp < 1.0f/64.0f) multAgcTemp = 1.0f/64.0f; - - // compute error terms - control_error = multAgcTemp - lastMultAgc; - - if (((multAgcTemp > 0.085f) && (multAgcTemp < 6.5f)) //integrator anti-windup by clamping - && (multAgc*sampleMax < agcZoneStop[AGC_preset])) //integrator ceiling (>140% of max) - control_integrated += control_error * 0.002f * 0.25f; // 2ms = integration time; 0.25 for damping - else - control_integrated *= 0.9f; // 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 energy 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; - } - - // limit amplification again - PI controller sometimes "overshoots" - //multAgcTemp = constrain(multAgcTemp, 0.015625f, 32.0f); // 1/64 < multAgcTemp < 32 - if (multAgcTemp > 32.0f) multAgcTemp = 32.0f; - if (multAgcTemp < 1.0f/64.0f) multAgcTemp = 1.0f/64.0f; - } - - // NOW finally amplify the signal - tmpAgc = sampleReal * multAgcTemp; // apply gain to signal - if (fabsf(sampleReal) < 2.0f) tmpAgc = 0.0f; // apply squelch threshold - //tmpAgc = constrain(tmpAgc, 0, 255); - if (tmpAgc > 255) tmpAgc = 255.0f; // limit to 8bit - if (tmpAgc < 1) tmpAgc = 0.0f; // just to be sure - - // update global vars ONCE - multAgc, sampleAGC, rawSampleAgc - multAgc = multAgcTemp; - rawSampleAgc = 0.8f * tmpAgc + 0.2f * (float)rawSampleAgc; - // update smoothed AGC sample - if (fabsf(tmpAgc) < 1.0f) - sampleAgc = 0.5f * tmpAgc + 0.5f * sampleAgc; // fast path to zero - else - sampleAgc += agcSampleSmooth[AGC_preset] * (tmpAgc - sampleAgc); // smooth path - - sampleAgc = fabsf(sampleAgc); // // make sure we have a positive value - last_soundAgc = soundAgc; - } // agcAvg() - - // post-processing and filtering of MIC sample (micDataReal) from FFTcode() - void getSample() - { - float sampleAdj; // Gain adjusted sample value - float tmpSample; // An interim sample variable used for calculations. - const float weighting = 0.2f; // Exponential filter weighting. Will be adjustable in a future release. - 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; - #else - #ifdef ARDUINO_ARCH_ESP32 - micIn = int(micDataReal); // micDataSm = ((micData * 3) + micData)/4; - #else - // this is the minimal code for reading analog mic input on 8266. - // warning!! Absolutely experimental code. Audio on 8266 is still not working. Expects a million follow-on problems. - static unsigned long lastAnalogTime = 0; - static float lastAnalogValue = 0.0f; - if (millis() - lastAnalogTime > 20) { - micDataReal = analogRead(A0); // read one sample with 10bit resolution. This is a dirty hack, supporting volumereactive effects only. - lastAnalogTime = millis(); - lastAnalogValue = micDataReal; - yield(); - } else micDataReal = lastAnalogValue; - micIn = int(micDataReal); - #endif - #endif - - micLev += (micDataReal-micLev) / 12288.0f; - if (micIn < micLev) micLev = ((micLev * 31.0f) + micDataReal) / 32.0f; // 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 = fabsf(micDataReal - micLev); - expAdjF = (weighting * micInNoDC + (1.0f-weighting) * expAdjF); - expAdjF = fabsf(expAdjF); // Now (!) take the absolute value - - expAdjF = (expAdjF <= soundSquelch) ? 0.0f : expAdjF; // simple noise gate - if ((soundSquelch == 0) && (expAdjF < 0.25f)) expAdjF = 0.0f; // do something meaningfull when "squelch = 0" - - tmpSample = expAdjF; - micIn = abs(micIn); // And get the absolute value of each sample - - sampleAdj = tmpSample * sampleGain * inputLevel / 5120.0f /* /40 /128 */ + tmpSample / 16.0f; // Adjust the gain. with inputLevel adjustment - sampleReal = tmpSample; - - sampleAdj = fmax(fmin(sampleAdj, 255.0f), 0.0f); // Question: why are we limiting the value to 8 bits ??? - sampleRaw = (int16_t)sampleAdj; // ONLY update sample ONCE!!!! - - // keep "peak" sample, but decay value if current sample is below peak - if ((sampleMax < sampleReal) && (sampleReal > 0.5f)) { - sampleMax += 0.5f * (sampleReal - sampleMax); // new peak - with some filtering - // another simple way to detect samplePeak - cannot detect beats, but reacts on peak volume - if (((binNum < 12) || ((maxVol < 1))) && (millis() - timeOfPeak > 80) && (sampleAvg > 1)) { - samplePeak = true; - timeOfPeak = millis(); - udpSamplePeak = true; - } - } else { - if ((multAgc*sampleMax > agcZoneStop[AGC_preset]) && (soundAgc > 0)) - sampleMax += 0.5f * (sampleReal - sampleMax); // over AGC Zone - get back quickly - else - sampleMax *= agcSampleDecay[AGC_preset]; // signal to zero --> 5-8sec - } - if (sampleMax < 0.5f) sampleMax = 0.0f; - - sampleAvg = ((sampleAvg * 15.0f) + sampleAdj) / 16.0f; // Smooth it out over the last 16 samples. - sampleAvg = fabsf(sampleAvg); // make sure we have a positive value - } // getSample() - -#endif - - /* Limits the dynamics of volumeSmth (= sampleAvg or sampleAgc). - * does not affect FFTResult[] or volumeRaw ( = sample or rawSampleAgc) - */ - // effects: Gravimeter, Gravcenter, Gravcentric, Noisefire, Plasmoid, Freqpixels, Freqwave, Gravfreq, (2D Swirl, 2D Waverly) - void limitSampleDynamics(void) { - const float bigChange = 196.0f; // just a representative number - a large, expected sample value - static unsigned long last_time = 0; - static float last_volumeSmth = 0.0f; - - if (limiterOn == false) return; - - long delta_time = millis() - last_time; - delta_time = constrain(delta_time , 1, 1000); // below 1ms -> 1ms; above 1sec -> sily lil hick-up - float deltaSample = volumeSmth - last_volumeSmth; - - if (attackTime > 0) { // user has defined attack time > 0 - float maxAttack = bigChange * float(delta_time) / float(attackTime); - if (deltaSample > maxAttack) deltaSample = maxAttack; - } - if (decayTime > 0) { // user has defined decay time > 0 - float maxDecay = - bigChange * float(delta_time) / float(decayTime); - if (deltaSample < maxDecay) deltaSample = maxDecay; - } - - volumeSmth = last_volumeSmth + deltaSample; - - last_volumeSmth = volumeSmth; - last_time = millis(); - } - - - ////////////////////// - // UDP Sound Sync // - ////////////////////// - - // try to establish UDP sound sync connection - void connectUDPSoundSync(void) { - // This function tries to establish a UDP sync connection if needed - // necessary as we also want to transmit in "AP Mode", but the standard "connected()" callback only reacts on STA connection - static unsigned long last_connection_attempt = 0; - - if ((audioSyncPort <= 0) || ((audioSyncEnabled & 0x03) == 0)) return; // Sound Sync not enabled - if (udpSyncConnected) return; // already connected - if (!(apActive || interfacesInited)) return; // neither AP nor other connections availeable - if (millis() - last_connection_attempt < 15000) return; // only try once in 15 seconds - if (updateIsRunning) return; - - // if we arrive here, we need a UDP connection but don't have one - last_connection_attempt = millis(); - connected(); // try to start UDP - } - -#ifdef ARDUINO_ARCH_ESP32 - void transmitAudioData() - { - //DEBUGSR_PRINTLN("Transmitting UDP Mic Packet"); - - audioSyncPacket transmitData; - memset(reinterpret_cast(&transmitData), 0, sizeof(transmitData)); // make sure that the packet - including "invisible" padding bytes added by the compiler - is fully initialized - - strncpy_P(transmitData.header, PSTR(UDP_SYNC_HEADER), 6); - // transmit samples that were not modified by limitSampleDynamics() - transmitData.sampleRaw = (soundAgc) ? rawSampleAgc: sampleRaw; - transmitData.sampleSmth = (soundAgc) ? sampleAgc : sampleAvg; - transmitData.samplePeak = udpSamplePeak ? 1:0; - udpSamplePeak = false; // Reset udpSamplePeak after we've transmitted it - - for (int i = 0; i < NUM_GEQ_CHANNELS; i++) { - transmitData.fftResult[i] = (uint8_t)constrain(fftResult[i], 0, 254); - } - - transmitData.FFT_Magnitude = my_magnitude; - transmitData.FFT_MajorPeak = FFT_MajorPeak; - -#ifndef WLED_DISABLE_ESPNOW - if (useESPNowSync && statusESPNow == ESP_NOW_STATE_ON) { - EspNowPartialPacket buffer = {{'W','L','E','D'}, 0, 1, {0}}; - //DEBUGSR_PRINTLN(F("ESP-NOW Sending audio packet.")); - size_t packetSize = sizeof(EspNowPartialPacket) - sizeof(EspNowPartialPacket::data) + sizeof(transmitData); - memcpy(buffer.data, &transmitData, sizeof(transmitData)); - quickEspNow.send(ESPNOW_BROADCAST_ADDRESS, reinterpret_cast(&buffer), packetSize); - } -#endif - - if (udpSyncConnected && fftUdp.beginMulticastPacket() != 0) { // beginMulticastPacket returns 0 in case of error - fftUdp.write(reinterpret_cast(&transmitData), sizeof(transmitData)); - fftUdp.endPacket(); - } - return; - } // transmitAudioData() - -#endif - - static inline bool isValidUdpSyncVersion(const char *header) { - return strncmp_P(header, UDP_SYNC_HEADER, 6) == 0; - } - static inline bool isValidUdpSyncVersion_v1(const char *header) { - return strncmp_P(header, UDP_SYNC_HEADER_v1, 6) == 0; - } - - void decodeAudioData(int packetSize, uint8_t *fftBuff) { - audioSyncPacket receivedPacket; - memset(&receivedPacket, 0, sizeof(receivedPacket)); // start clean - memcpy(&receivedPacket, fftBuff, min((unsigned)packetSize, (unsigned)sizeof(receivedPacket))); // don't violate alignment - thanks @willmmiles# - - // update samples for effects - volumeSmth = fmaxf(receivedPacket.sampleSmth, 0.0f); - volumeRaw = fmaxf(receivedPacket.sampleRaw, 0.0f); -#ifdef ARDUINO_ARCH_ESP32 - // update internal samples - sampleRaw = volumeRaw; - sampleAvg = volumeSmth; - rawSampleAgc = volumeRaw; - sampleAgc = volumeSmth; - multAgc = 1.0f; -#endif - // Only change samplePeak IF it's currently false. - // If it's true already, then the animation still needs to respond. - autoResetPeak(); - if (!samplePeak) { - samplePeak = receivedPacket.samplePeak > 0; - if (samplePeak) timeOfPeak = millis(); - } - //These values are only computed by ESP32 - for (int i = 0; i < NUM_GEQ_CHANNELS; i++) fftResult[i] = receivedPacket.fftResult[i]; - my_magnitude = fmaxf(receivedPacket.FFT_Magnitude, 0.0f); - FFT_Magnitude = my_magnitude; - FFT_MajorPeak = constrain(receivedPacket.FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects - } - - void decodeAudioData_v1(int packetSize, uint8_t *fftBuff) { - audioSyncPacket_v1 *receivedPacket = reinterpret_cast(fftBuff); - // update samples for effects - volumeSmth = fmaxf(receivedPacket->sampleAgc, 0.0f); - volumeRaw = volumeSmth; // V1 format does not have "raw" AGC sample -#ifdef ARDUINO_ARCH_ESP32 - // update internal samples - sampleRaw = fmaxf(receivedPacket->sampleRaw, 0.0f); - sampleAvg = fmaxf(receivedPacket->sampleAvg, 0.0f);; - sampleAgc = volumeSmth; - rawSampleAgc = volumeRaw; - multAgc = 1.0f; -#endif - // Only change samplePeak IF it's currently false. - // If it's true already, then the animation still needs to respond. - autoResetPeak(); - if (!samplePeak) { - samplePeak = receivedPacket->samplePeak > 0; - if (samplePeak) timeOfPeak = millis(); - } - //These values are only available on the ESP32 - for (int i = 0; i < NUM_GEQ_CHANNELS; i++) fftResult[i] = receivedPacket->fftResult[i]; - my_magnitude = fmaxf(receivedPacket->FFT_Magnitude, 0.0f); - FFT_Magnitude = my_magnitude; - FFT_MajorPeak = constrain(receivedPacket->FFT_MajorPeak, 1.0f, 11025.0f); // restrict value to range expected by effects - } - - bool receiveAudioData() // check & process new data. return TRUE in case that new audio data was received. - { - if (!udpSyncConnected) return false; - bool haveFreshData = false; - - size_t packetSize = fftUdp.parsePacket(); -#ifdef ARDUINO_ARCH_ESP32 - if ((packetSize > 0) && ((packetSize < 5) || (packetSize > UDPSOUND_MAX_PACKET))) fftUdp.flush(); // discard invalid packets (too small or too big) - only works on esp32 -#endif - if ((packetSize > 5) && (packetSize <= UDPSOUND_MAX_PACKET)) { - //DEBUGSR_PRINTLN("Received UDP Sync Packet"); - uint8_t fftBuff[UDPSOUND_MAX_PACKET+1] = { 0 }; // fixed-size buffer for receiving (stack), to avoid heap fragmentation caused by variable sized arrays - fftUdp.read(fftBuff, packetSize); - - // VERIFY THAT THIS IS A COMPATIBLE PACKET - if (packetSize == sizeof(audioSyncPacket) && (isValidUdpSyncVersion((const char *)fftBuff))) { - decodeAudioData(packetSize, fftBuff); - //DEBUGSR_PRINTLN("Finished parsing UDP Sync Packet v2"); - haveFreshData = true; - receivedFormat = 2; - } else { - if (packetSize == sizeof(audioSyncPacket_v1) && (isValidUdpSyncVersion_v1((const char *)fftBuff))) { - decodeAudioData_v1(packetSize, fftBuff); - //DEBUGSR_PRINTLN("Finished parsing UDP Sync Packet v1"); - haveFreshData = true; - receivedFormat = 1; - } else receivedFormat = 0; // unknown format - } - } - return haveFreshData; - } - - - ////////////////////// - // usermod functions// - ////////////////////// - - public: - //Functions called by WLED or other usermods - - /* - * setup() is called once at boot. WiFi is not yet connected at this point. - * You can use it to initialize variables, sensors or similar. - * It is called *AFTER* readFromConfig() - */ - void setup() override - { - disableSoundProcessing = true; // just to be sure - if (!initDone) { - // 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 = 8; - 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) - um_data->u_type[0] = UMT_FLOAT; - um_data->u_data[1] = &volumeRaw; // used (New) - um_data->u_type[1] = UMT_UINT16; - um_data->u_data[2] = fftResult; //*used (Blurz, DJ Light, Noisemove, GEQ_base, 2D Funky Plank, Akemi) - um_data->u_type[2] = UMT_BYTE_ARR; - um_data->u_data[3] = &samplePeak; //*used (Puddlepeak, Ripplepeak, Waterfall) - um_data->u_type[3] = UMT_BYTE; - um_data->u_data[4] = &FFT_MajorPeak; //*used (Ripplepeak, Freqmap, Freqmatrix, Freqpixels, Freqwave, Gravfreq, Rocktaves, Waterfall) - um_data->u_type[4] = UMT_FLOAT; - um_data->u_data[5] = &my_magnitude; // used (New) - um_data->u_type[5] = UMT_FLOAT; - um_data->u_data[6] = &maxVol; // assigned in effect function from UI element!!! (Puddlepeak, Ripplepeak, Waterfall) - um_data->u_type[6] = UMT_BYTE; - um_data->u_data[7] = &binNum; // assigned in effect function from UI element!!! (Puddlepeak, Ripplepeak, Waterfall) - um_data->u_type[7] = UMT_BYTE; - } - - -#ifdef ARDUINO_ARCH_ESP32 - - // Reset I2S peripheral for good measure - i2s_driver_uninstall(I2S_NUM_0); // E (696) I2S: i2s_driver_uninstall(2006): I2S port 0 has not installed - #if !defined(CONFIG_IDF_TARGET_ESP32C3) - delay(100); - periph_module_reset(PERIPH_I2S0_MODULE); // not possible on -C3 - #endif - delay(100); // Give that poor microphone some time to setup. - - useBandPassFilter = false; - - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) - if ((i2sckPin == I2S_PIN_NO_CHANGE) && (i2ssdPin >= 0) && (i2swsPin >= 0) && ((dmType == 1) || (dmType == 4)) ) dmType = 5; // dummy user support: SCK == -1 --means--> PDM microphone - #endif - - switch (dmType) { - #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) - // stub cases for not-yet-supported I2S modes on other ESP32 chips - case 0: //ADC analog - #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) - case 5: //PDM Microphone - #endif - #endif - case 1: - DEBUGSR_PRINT(F("AR: Generic I2S Microphone - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT)); - audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE); - delay(100); - if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin); - break; - case 2: - DEBUGSR_PRINTLN(F("AR: ES7243 Microphone (right channel only).")); - audioSource = new ES7243(SAMPLE_RATE, BLOCK_SIZE); - delay(100); - if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); - break; - case 3: - DEBUGSR_PRINT(F("AR: SPH0645 Microphone - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT)); - audioSource = new SPH0654(SAMPLE_RATE, BLOCK_SIZE); - delay(100); - audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin); - break; - case 4: - DEBUGSR_PRINT(F("AR: Generic I2S Microphone with Master Clock - ")); DEBUGSR_PRINTLN(F(I2S_MIC_CHANNEL_TEXT)); - audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f/24.0f); - delay(100); - if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); - break; - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) - case 5: - DEBUGSR_PRINT(F("AR: I2S PDM Microphone - ")); DEBUGSR_PRINTLN(F(I2S_PDM_MIC_CHANNEL_TEXT)); - audioSource = new I2SSource(SAMPLE_RATE, BLOCK_SIZE, 1.0f/4.0f); - useBandPassFilter = true; // this reduces the noise floor on SPM1423 from 5% Vpp (~380) down to 0.05% Vpp (~5) - delay(100); - if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin); - break; - #endif - case 6: - DEBUGSR_PRINTLN(F("AR: ES8388 Source")); - audioSource = new ES8388Source(SAMPLE_RATE, BLOCK_SIZE); - delay(100); - if (audioSource) audioSource->initialize(i2swsPin, i2ssdPin, i2sckPin, mclkPin); - break; - - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) - // ADC over I2S is only possible on "classic" ESP32 - case 0: - default: - DEBUGSR_PRINTLN(F("AR: Analog Microphone (left channel only).")); - audioSource = new I2SAdcSource(SAMPLE_RATE, BLOCK_SIZE); - delay(100); - useBandPassFilter = true; // PDM bandpass filter seems to help for bad quality analog - if (audioSource) audioSource->initialize(audioPin); - break; - #endif - } - delay(250); // give microphone enough time to initialise - - if (!audioSource) enabled = false; // audio failed to initialise -#endif - if (enabled) onUpdateBegin(false); // create FFT task, and initialize network - if (enabled) disableSoundProcessing = false; // all good - enable audio processing -#ifdef ARDUINO_ARCH_ESP32 - if (FFT_Task == nullptr) enabled = false; // FFT task creation failed - if ((!audioSource) || (!audioSource->isInitialized())) { - // audio source failed to initialize. Still stay "enabled", as there might be input arriving via UDP Sound Sync - #ifdef WLED_DEBUG - DEBUG_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings.")); - #else - DEBUGSR_PRINTLN(F("AR: Failed to initialize sound input driver. Please check input PIN settings.")); - #endif - disableSoundProcessing = true; - } -#endif - if (enabled) connectUDPSoundSync(); - if (enabled && addPalettes) createAudioPalettes(); - initDone = true; - } - - - /* - * connected() is called every time the WiFi is (re)connected - * Use it to initialize network interfaces - */ - void connected() override - { - if (udpSyncConnected) { // clean-up: if open, close old UDP sync connection - udpSyncConnected = false; - fftUdp.stop(); - } - - if (audioSyncPort > 0 && (audioSyncEnabled & 0x03)) { - #ifdef ARDUINO_ARCH_ESP32 - udpSyncConnected = fftUdp.beginMulticast(IPAddress(239, 0, 0, 1), audioSyncPort); - #else - udpSyncConnected = fftUdp.beginMulticast(WiFi.localIP(), IPAddress(239, 0, 0, 1), audioSyncPort); - #endif - } - } - - - /* - * 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() override - { - static unsigned long lastUMRun = millis(); - - if (!enabled) { - disableSoundProcessing = true; // keep processing suspended (FFT task) - lastUMRun = millis(); // update time keeping - return; - } - // We cannot wait indefinitely before processing audio data - if (WS2812FX::isUpdating() && (millis() - lastUMRun < 2)) return; // be nice, but not too nice - - // suspend local sound processing when "real time mode" is active (E131, UDP, ADALIGHT, ARTNET) - if ( (realtimeOverride == REALTIME_OVERRIDE_NONE) // please add other overrides here if needed - &&( (realtimeMode == REALTIME_MODE_GENERIC) - ||(realtimeMode == REALTIME_MODE_E131) - ||(realtimeMode == REALTIME_MODE_UDP) - ||(realtimeMode == REALTIME_MODE_ADALIGHT) - ||(realtimeMode == REALTIME_MODE_ARTNET) ) ) // please add other modes here if needed - { - #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG) - if ((disableSoundProcessing == false) && (audioSyncEnabled == 0)) { // we just switched to "disabled" - DEBUG_PRINTLN(F("[AR userLoop] realtime mode active - audio processing suspended.")); - DEBUG_PRINTF_P(PSTR(" RealtimeMode = %d; RealtimeOverride = %d\n"), int(realtimeMode), int(realtimeOverride)); - } - #endif - disableSoundProcessing = true; - } else { - #if defined(ARDUINO_ARCH_ESP32) && defined(WLED_DEBUG) - if ((disableSoundProcessing == true) && (audioSyncEnabled == 0) && audioSource->isInitialized()) { // we just switched to "enabled" - DEBUG_PRINTLN(F("[AR userLoop] realtime mode ended - audio processing resumed.")); - DEBUG_PRINTF_P(PSTR(" RealtimeMode = %d; RealtimeOverride = %d\n"), int(realtimeMode), int(realtimeOverride)); - } - #endif - if ((disableSoundProcessing == true) && (audioSyncEnabled == 0)) lastUMRun = millis(); // just left "realtime mode" - update timekeeping - disableSoundProcessing = false; - } - - if (audioSyncEnabled & 0x02) disableSoundProcessing = true; // make sure everything is disabled IF in audio Receive mode - if (audioSyncEnabled & 0x01) disableSoundProcessing = false; // keep running audio IF we're in audio Transmit mode -#ifdef ARDUINO_ARCH_ESP32 - if (!audioSource->isInitialized()) disableSoundProcessing = true; // no audio source - - - // Only run the sampling code IF we're not in Receive mode or realtime mode - if (!(audioSyncEnabled & 0x02) && !disableSoundProcessing) { - if (soundAgc > AGC_NUM_PRESETS) soundAgc = 0; // make sure that AGC preset is valid (to avoid array bounds violation) - - unsigned long t_now = millis(); // remember current time - int userloopDelay = int(t_now - lastUMRun); - if (lastUMRun == 0) userloopDelay=0; // startup - don't have valid data from last run. - - #ifdef WLED_DEBUG - // complain when audio userloop has been delayed for long time. Currently we need userloop running between 500 and 1500 times per second. - // softhack007 disabled temporarily - avoid serial console spam with MANY leds and low FPS - //if ((userloopDelay > 65) && !disableSoundProcessing && (audioSyncEnabled == 0)) { - // DEBUG_PRINTF_P(PSTR("[AR userLoop] hiccup detected -> was inactive for last %d millis!\n"), userloopDelay); - //} - #endif - - // run filters, and repeat in case of loop delays (hick-up compensation) - if (userloopDelay <2) userloopDelay = 0; // minor glitch, no problem - if (userloopDelay >200) userloopDelay = 200; // limit number of filter re-runs - do { - getSample(); // run microphone sampling filters - agcAvg(t_now - userloopDelay); // Calculated the PI adjusted value as sampleAvg - userloopDelay -= 2; // advance "simulated time" by 2ms - } while (userloopDelay > 0); - lastUMRun = t_now; // update time keeping - - // update samples for effects (raw, smooth) - volumeSmth = (soundAgc) ? sampleAgc : sampleAvg; - volumeRaw = (soundAgc) ? rawSampleAgc: sampleRaw; - // update FFTMagnitude, taking into account AGC amplification - my_magnitude = FFT_Magnitude; // / 16.0f, 8.0f, 4.0f done in effects - if (soundAgc) my_magnitude *= multAgc; - if (volumeSmth < 1 ) my_magnitude = 0.001f; // noise gate closed - mute - - limitSampleDynamics(); - } // if (!disableSoundProcessing) -#endif - - autoResetPeak(); // auto-reset sample peak after strip minShowDelay - if (!udpSyncConnected) udpSamplePeak = false; // reset UDP samplePeak while UDP is unconnected - - connectUDPSoundSync(); // ensure we have a connection - if needed - - // UDP Microphone Sync - receive mode - if ((audioSyncEnabled & 0x02) && udpSyncConnected) { - // Only run the audio listener code if we're in Receive mode - static float syncVolumeSmth = 0; - bool have_new_sample = false; - if (millis() - lastTime > delayMs) { - have_new_sample = receiveAudioData(); - if (have_new_sample) last_UDPTime = millis(); -#ifdef ARDUINO_ARCH_ESP32 - else fftUdp.flush(); // Flush udp input buffers if we haven't read it - avoids hickups in receive mode. Does not work on 8266. -#endif - lastTime = millis(); - } - if (have_new_sample) syncVolumeSmth = volumeSmth; // remember received sample - else volumeSmth = syncVolumeSmth; // restore originally received sample for next run of dynamics limiter - limitSampleDynamics(); // run dynamics limiter on received volumeSmth, to hide jumps and hickups - } - - #if defined(MIC_LOGGER) || defined(MIC_SAMPLING_LOG) || defined(FFT_SAMPLING_LOG) - static unsigned long lastMicLoggerTime = 0; - if (millis()-lastMicLoggerTime > 20) { - lastMicLoggerTime = millis(); - logAudio(); - } - #endif - - // Info Page: keep max sample from last 5 seconds -#ifdef ARDUINO_ARCH_ESP32 - if ((millis() - sampleMaxTimer) > CYCLE_SAMPLEMAX) { - sampleMaxTimer = millis(); - maxSample5sec = (0.15f * maxSample5sec) + 0.85f *((soundAgc) ? sampleAgc : sampleAvg); // reset, and start with some smoothing - if (sampleAvg < 1) maxSample5sec = 0; // noise gate - } else { - if ((sampleAvg >= 1)) maxSample5sec = fmaxf(maxSample5sec, (soundAgc) ? rawSampleAgc : sampleRaw); // follow maximum volume - } -#else // similar functionality for 8266 receive only - use VolumeSmth instead of raw sample data - if ((millis() - sampleMaxTimer) > CYCLE_SAMPLEMAX) { - sampleMaxTimer = millis(); - maxSample5sec = (0.15 * maxSample5sec) + 0.85 * volumeSmth; // reset, and start with some smoothing - if (volumeSmth < 1.0f) maxSample5sec = 0; // noise gate - if (maxSample5sec < 0.0f) maxSample5sec = 0; // avoid negative values - } else { - if (volumeSmth >= 1.0f) maxSample5sec = fmaxf(maxSample5sec, volumeRaw); // follow maximum volume - } -#endif - -#ifdef ARDUINO_ARCH_ESP32 - //UDP Microphone Sync - transmit mode - if ((audioSyncEnabled & 0x01) && (millis() - lastTime > 20)) { - // Only run the transmit code IF we're in Transmit mode - transmitAudioData(); - lastTime = millis(); - } -#endif - - fillAudioPalettes(); - } - - - bool getUMData(um_data_t **data) override - { - if (!data || !enabled) return false; // no pointer provided by caller or not enabled -> exit - *data = um_data; - return true; - } - -#ifdef ARDUINO_ARCH_ESP32 - void onUpdateBegin(bool init) override - { -#ifdef WLED_DEBUG - fftTime = sampleTime = 0; -#endif - // gracefully suspend FFT task (if running) - disableSoundProcessing = true; - - // reset sound data - micDataReal = 0.0f; - volumeRaw = 0; volumeSmth = 0.0f; - sampleAgc = 0.0f; sampleAvg = 0.0f; - sampleRaw = 0; rawSampleAgc = 0.0f; - my_magnitude = 0.0f; FFT_Magnitude = 0.0f; FFT_MajorPeak = 1.0f; - multAgc = 1.0f; - // reset FFT data - memset(fftCalc, 0, sizeof(fftCalc)); - memset(fftAvg, 0, sizeof(fftAvg)); - memset(fftResult, 0, sizeof(fftResult)); - for(int i=(init?0:1); i don't process audio - updateIsRunning = init; - } -#endif - -#ifdef ARDUINO_ARCH_ESP32 - /** - * handleButton() can be used to override default button behaviour. Returning true - * will prevent button working in a default way. - */ - bool handleButton(uint8_t b) override { - yield(); - // crude way of determining if audio input is analog - // better would be for AudioSource to implement getType() - if (enabled - && dmType == 0 && audioPin>=0 - && (buttonType[b] == BTN_TYPE_ANALOG || buttonType[b] == BTN_TYPE_ANALOG_INVERTED) - ) { - return true; - } - return false; - } - -#endif - //////////////////////////// - // Settings and Info Page // - //////////////////////////// - - /* - * 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) override - { -#ifdef ARDUINO_ARCH_ESP32 - char myStringBuffer[16]; // buffer for snprintf() - not used yet on 8266 -#endif - JsonObject user = root["u"]; - if (user.isNull()) user = root.createNestedObject("u"); - - JsonArray infoArr = user.createNestedArray(FPSTR(_name)); - - String uiDomString = F(""); - infoArr.add(uiDomString); - - if (enabled) { -#ifdef ARDUINO_ARCH_ESP32 - // Input Level Slider - if (disableSoundProcessing == false) { // only show slider when audio processing is running - if (soundAgc > 0) { - infoArr = user.createNestedArray(F("GEQ Input Level")); // if AGC is on, this slider only affects fftResult[] frequencies - } else { - infoArr = user.createNestedArray(F("Audio Input Level")); - } - uiDomString = F("
"); // - infoArr.add(uiDomString); - } -#endif - // The following can be used for troubleshooting user errors and is so not enclosed in #ifdef WLED_DEBUG - - // current Audio input - infoArr = user.createNestedArray(F("Audio Source")); - if (audioSyncEnabled & 0x02) { - // UDP sound sync - receive mode - infoArr.add(F("UDP sound sync")); - if (udpSyncConnected) { - if (millis() - last_UDPTime < 2500) - infoArr.add(F(" - receiving")); - else - infoArr.add(F(" - idle")); - } else { - infoArr.add(F(" - no connection")); - } -#ifndef ARDUINO_ARCH_ESP32 // substitute for 8266 - } else { - infoArr.add(F("sound sync Off")); - } -#else // ESP32 only - } else { - // Analog or I2S digital input - if (audioSource && (audioSource->isInitialized())) { - // audio source successfully configured - if (audioSource->getType() == AudioSource::Type_I2SAdc) { - infoArr.add(F("ADC analog")); - } else { - infoArr.add(F("I2S digital")); - } - // input level or "silence" - if (maxSample5sec > 1.0f) { - float my_usage = 100.0f * (maxSample5sec / 255.0f); - snprintf_P(myStringBuffer, 15, PSTR(" - peak %3d%%"), int(my_usage)); - infoArr.add(myStringBuffer); - } else { - infoArr.add(F(" - quiet")); - } - } else { - // error during audio source setup - infoArr.add(F("not initialized")); - infoArr.add(F(" - check pin settings")); - } - } - - // Sound processing (FFT and input filters) - infoArr = user.createNestedArray(F("Sound Processing")); - if (audioSource && (disableSoundProcessing == false)) { - infoArr.add(F("running")); - } else { - infoArr.add(F("suspended")); - } - - // AGC or manual Gain - if ((soundAgc==0) && (disableSoundProcessing == false) && !(audioSyncEnabled & 0x02)) { - infoArr = user.createNestedArray(F("Manual Gain")); - float myGain = ((float)sampleGain/40.0f * (float)inputLevel/128.0f) + 1.0f/16.0f; // non-AGC gain from presets - infoArr.add(roundf(myGain*100.0f) / 100.0f); - infoArr.add("x"); - } - if (soundAgc && (disableSoundProcessing == false) && !(audioSyncEnabled & 0x02)) { - infoArr = user.createNestedArray(F("AGC Gain")); - infoArr.add(roundf(multAgc*100.0f) / 100.0f); - infoArr.add("x"); - } -#endif - // UDP Sound Sync status - infoArr = user.createNestedArray(F("UDP Sound Sync")); - if (audioSyncEnabled) { - if (audioSyncEnabled & 0x01) { - infoArr.add(F("send mode")); - if ((udpSyncConnected) && (millis() - lastTime < 2500)) infoArr.add(F(" v2")); - } else if (audioSyncEnabled & 0x02) { - infoArr.add(F("receive mode")); - } - } else - infoArr.add("off"); - if (audioSyncEnabled && !udpSyncConnected) infoArr.add(" (unconnected)"); - if (audioSyncEnabled && udpSyncConnected && (millis() - last_UDPTime < 2500)) { - if (receivedFormat == 1) infoArr.add(F(" v1")); - if (receivedFormat == 2) infoArr.add(F(" v2")); - } - - #if defined(WLED_DEBUG) || defined(SR_DEBUG) - #ifdef ARDUINO_ARCH_ESP32 - infoArr = user.createNestedArray(F("Sampling time")); - infoArr.add(float(sampleTime)/100.0f); - infoArr.add(" ms"); - - infoArr = user.createNestedArray(F("FFT time")); - infoArr.add(float(fftTime)/100.0f); - if ((fftTime/100) >= FFT_MIN_CYCLE) // FFT time over budget -> I2S buffer will overflow - infoArr.add("! ms"); - else if ((fftTime/80 + sampleTime/80) >= FFT_MIN_CYCLE) // FFT time >75% of budget -> risk of instability - infoArr.add(" ms!"); - else - infoArr.add(" ms"); - - DEBUGSR_PRINTF("AR Sampling time: %5.2f ms\n", float(sampleTime)/100.0f); - DEBUGSR_PRINTF("AR FFT time : %5.2f ms\n", float(fftTime)/100.0f); - #endif - #endif - } - } - - - /* - * 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) override - { - if (!initDone) return; // prevent crash on boot applyPreset() - JsonObject usermod = root[FPSTR(_name)]; - if (usermod.isNull()) { - usermod = root.createNestedObject(FPSTR(_name)); - } - usermod["on"] = enabled; - } - - - /* - * 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) override - { - if (!initDone) return; // prevent crash on boot applyPreset() - bool prevEnabled = enabled; - JsonObject usermod = root[FPSTR(_name)]; - if (!usermod.isNull()) { - if (usermod[FPSTR(_enabled)].is()) { - enabled = usermod[FPSTR(_enabled)].as(); - if (prevEnabled != enabled) onUpdateBegin(!enabled); - if (addPalettes) { - // add/remove custom/audioreactive palettes - if (prevEnabled && !enabled) removeAudioPalettes(); - if (!prevEnabled && enabled) createAudioPalettes(); - } - } -#ifdef ARDUINO_ARCH_ESP32 - if (usermod[FPSTR(_inputLvl)].is()) { - inputLevel = min(255,max(0,usermod[FPSTR(_inputLvl)].as())); - } -#endif - } - if (root.containsKey(F("rmcpal")) && root[F("rmcpal")].as()) { - // handle removal of custom palettes from JSON call so we don't break things - removeAudioPalettes(); - } - } - - void onStateChange(uint8_t callMode) override { - if (initDone && enabled && addPalettes && palettes==0 && WS2812FX::customPalettes.size()<10) { - // if palettes were removed during JSON call re-add them - createAudioPalettes(); - } - } - - /* - * 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) override - { - JsonObject top = root.createNestedObject(FPSTR(_name)); - top[FPSTR(_enabled)] = enabled; - top[FPSTR(_addPalettes)] = addPalettes; - -#ifdef ARDUINO_ARCH_ESP32 - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) - JsonObject amic = top.createNestedObject(FPSTR(_analogmic)); - amic["pin"] = audioPin; - #endif - - JsonObject dmic = top.createNestedObject(FPSTR(_digitalmic)); - dmic["type"] = dmType; - JsonArray pinArray = dmic.createNestedArray("pin"); - pinArray.add(i2ssdPin); - pinArray.add(i2swsPin); - pinArray.add(i2sckPin); - pinArray.add(mclkPin); - - JsonObject cfg = top.createNestedObject(FPSTR(_config)); - cfg[F("squelch")] = soundSquelch; - cfg[F("gain")] = sampleGain; - cfg[F("AGC")] = soundAgc; - - JsonObject freqScale = top.createNestedObject(FPSTR(_frequency)); - freqScale[F("scale")] = FFTScalingMode; -#endif - - JsonObject dynLim = top.createNestedObject(FPSTR(_dynamics)); - dynLim[F("limiter")] = limiterOn; - dynLim[F("rise")] = attackTime; - dynLim[F("fall")] = decayTime; - - JsonObject sync = top.createNestedObject("sync"); - sync["port"] = audioSyncPort; - sync["mode"] = audioSyncEnabled; - } - - - /* - * 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) override - { - JsonObject top = root[FPSTR(_name)]; - bool configComplete = !top.isNull(); - bool oldEnabled = enabled; - bool oldAddPalettes = addPalettes; - - configComplete &= getJsonValue(top[FPSTR(_enabled)], enabled); - configComplete &= getJsonValue(top[FPSTR(_addPalettes)], addPalettes); - -#ifdef ARDUINO_ARCH_ESP32 - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) - configComplete &= getJsonValue(top[FPSTR(_analogmic)]["pin"], audioPin); - #else - audioPin = -1; // MCU does not support analog mic - #endif - - configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["type"], dmType); - #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) || defined(CONFIG_IDF_TARGET_ESP32S3) - if (dmType == 0) dmType = SR_DMTYPE; // MCU does not support analog - #if defined(CONFIG_IDF_TARGET_ESP32S2) || defined(CONFIG_IDF_TARGET_ESP32C3) - if (dmType == 5) dmType = SR_DMTYPE; // MCU does not support PDM - #endif - #endif - - configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][0], i2ssdPin); - configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][1], i2swsPin); - configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][2], i2sckPin); - configComplete &= getJsonValue(top[FPSTR(_digitalmic)]["pin"][3], mclkPin); - - configComplete &= getJsonValue(top[FPSTR(_config)][F("squelch")], soundSquelch); - configComplete &= getJsonValue(top[FPSTR(_config)][F("gain")], sampleGain); - configComplete &= getJsonValue(top[FPSTR(_config)][F("AGC")], soundAgc); - - configComplete &= getJsonValue(top[FPSTR(_frequency)][F("scale")], FFTScalingMode); - - configComplete &= getJsonValue(top[FPSTR(_dynamics)][F("limiter")], limiterOn); - configComplete &= getJsonValue(top[FPSTR(_dynamics)][F("rise")], attackTime); - configComplete &= getJsonValue(top[FPSTR(_dynamics)][F("fall")], decayTime); -#endif - configComplete &= getJsonValue(top["sync"]["port"], audioSyncPort); - configComplete &= getJsonValue(top["sync"]["mode"], audioSyncEnabled); - - if (initDone) { - // add/remove custom/audioreactive palettes - if ((oldAddPalettes && !addPalettes) || (oldAddPalettes && !enabled)) removeAudioPalettes(); - if ((addPalettes && !oldAddPalettes && enabled) || (addPalettes && !oldEnabled && enabled)) createAudioPalettes(); - } // else setup() will create palettes - return configComplete; - } - - - void appendConfigData() override - { -#ifdef ARDUINO_ARCH_ESP32 - oappend(SET_F("dd=addDropdown('AudioReactive','digitalmic:type');")); - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) - oappend(SET_F("addOption(dd,'Generic Analog',0);")); - #endif - oappend(SET_F("addOption(dd,'Generic I2S',1);")); - oappend(SET_F("addOption(dd,'ES7243',2);")); - oappend(SET_F("addOption(dd,'SPH0654',3);")); - oappend(SET_F("addOption(dd,'Generic I2S with Mclk',4);")); - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) - oappend(SET_F("addOption(dd,'Generic I2S PDM',5);")); - #endif - oappend(SET_F("addOption(dd,'ES8388',6);")); - - oappend(SET_F("dd=addDropdown('AudioReactive','config:AGC');")); - oappend(SET_F("addOption(dd,'Off',0);")); - oappend(SET_F("addOption(dd,'Normal',1);")); - oappend(SET_F("addOption(dd,'Vivid',2);")); - oappend(SET_F("addOption(dd,'Lazy',3);")); - - oappend(SET_F("dd=addDropdown('AudioReactive','dynamics:limiter');")); - oappend(SET_F("addOption(dd,'Off',0);")); - oappend(SET_F("addOption(dd,'On',1);")); - oappend(SET_F("addInfo('AudioReactive:dynamics:limiter',0,' On ');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('AudioReactive:dynamics:rise',1,'ms (♪ effects only)');")); - oappend(SET_F("addInfo('AudioReactive:dynamics:fall',1,'ms (♪ effects only)');")); - - oappend(SET_F("dd=addDropdown('AudioReactive','frequency:scale');")); - oappend(SET_F("addOption(dd,'None',0);")); - oappend(SET_F("addOption(dd,'Linear (Amplitude)',2);")); - oappend(SET_F("addOption(dd,'Square Root (Energy)',3);")); - oappend(SET_F("addOption(dd,'Logarithmic (Loudness)',1);")); -#endif - - oappend(SET_F("dd=addDropdown('AudioReactive','sync:mode');")); - oappend(SET_F("addOption(dd,'Off',0);")); -#ifdef ARDUINO_ARCH_ESP32 - oappend(SET_F("addOption(dd,'Send',1);")); -#endif - oappend(SET_F("addOption(dd,'Receive',2);")); -#ifdef ARDUINO_ARCH_ESP32 - oappend(SET_F("addInfo('AudioReactive:digitalmic:type',1,'requires reboot!');")); // 0 is field type, 1 is actual field - oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',0,'sd/data/dout','I2S SD');")); - oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',1,'ws/clk/lrck','I2S WS');")); - oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',2,'sck/bclk','I2S SCK');")); - #if !defined(CONFIG_IDF_TARGET_ESP32S2) && !defined(CONFIG_IDF_TARGET_ESP32C3) && !defined(CONFIG_IDF_TARGET_ESP32S3) - oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',3,'only use -1, 0, 1 or 3','I2S MCLK');")); - #else - oappend(SET_F("addInfo('AudioReactive:digitalmic:pin[]',3,'master clock','I2S MCLK');")); - #endif -#endif - } - - - /* - * 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() override - //{ - //WS2812FX::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!). - * This could be used in the future for the system to determine whether your usermod is installed. - */ - uint16_t getId() override - { - return USERMOD_ID_AUDIOREACTIVE; - } -}; - -void AudioReactive::removeAudioPalettes(void) { - DEBUG_PRINTLN(F("Removing audio palettes.")); - while (palettes>0) { - WS2812FX::customPalettes.pop_back(); - DEBUG_PRINTLN(palettes); - palettes--; - } - DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(WS2812FX::customPalettes.size()); -} - -void AudioReactive::createAudioPalettes(void) { - DEBUG_PRINT(F("Total # of palettes: ")); DEBUG_PRINTLN(WS2812FX::customPalettes.size()); - if (palettes) return; - DEBUG_PRINTLN(F("Adding audio palettes.")); - for (int i=0; i= palettes) lastCustPalette -= palettes; - for (int pal=0; palgetStart() >= segStopIdx) continue; if (bus->getStart() + bus->getLength() <= segStartIdx) continue; - //uint8_t type = bus->getType(); if (bus->hasRGB() || (strip.cctFromRgb && bus->hasCCT())) capabilities |= SEG_CAPABILITY_RGB; if (!strip.cctFromRgb && bus->hasCCT()) capabilities |= SEG_CAPABILITY_CCT; if (strip.correctWB && (bus->hasRGB() || bus->hasCCT())) capabilities |= SEG_CAPABILITY_CCT; //white balance correction (CCT slider) @@ -1563,7 +1562,7 @@ uint16_t WS2812FX::getLengthPhysical() const { unsigned len = 0; for (size_t b = 0; b < BusManager::getNumBusses(); b++) { Bus *bus = BusManager::getBus(b); - if (bus->getType() >= TYPE_NET_DDP_RGB) continue; //exclude non-physical network busses + if (bus->isVirtual()) continue; //exclude non-physical network busses len += bus->getLength(); } return len; From bd7cd32f911d85963ec5aba22bdf3420a8770866 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Sun, 22 Sep 2024 13:56:14 +0200 Subject: [PATCH 142/142] Add mandatory refresh capability to remove type dependency. --- wled00/bus_manager.cpp | 2 +- wled00/bus_manager.h | 2 ++ wled00/data/settings_leds.htm | 9 +++++---- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 3766975f1..5b948b9c4 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -830,7 +830,7 @@ static String LEDTypesToJson(const std::vector& types) { String json; for (const auto &type : types) { // capabilities follows similar pattern as JSON API - int capabilities = Bus::hasRGB(type.id) | Bus::hasWhite(type.id)<<1 | Bus::hasCCT(type.id)<<2 | Bus::is16bit(type.id)<<4; + int capabilities = Bus::hasRGB(type.id) | Bus::hasWhite(type.id)<<1 | Bus::hasCCT(type.id)<<2 | Bus::is16bit(type.id)<<4 | Bus::mustRefresh(type.id)<<5; char str[256]; sprintf_P(str, PSTR("{i:%d,c:%d,t:\"%s\",n:\"%s\"},"), type.id, capabilities, type.type, type.name); json += str; diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 40fe61f40..e96b9de71 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -104,6 +104,7 @@ class Bus { inline bool isPWM() const { return isPWM(_type); } inline bool isVirtual() const { return isVirtual(_type); } inline bool is16bit() const { return is16bit(_type); } + inline bool mustRefresh() const { return mustRefresh(_type); } inline void setReversed(bool reversed) { _reversed = reversed; } inline void setStart(uint16_t start) { _start = start; } inline void setAutoWhiteMode(uint8_t m) { if (m < 5) _autoWhiteMode = m; } @@ -142,6 +143,7 @@ class Bus { static constexpr bool isPWM(uint8_t type) { return (type >= TYPE_ANALOG_MIN && type <= TYPE_ANALOG_MAX); } static constexpr bool isVirtual(uint8_t type) { return (type >= TYPE_VIRTUAL_MIN && type <= TYPE_VIRTUAL_MAX); } static constexpr bool is16bit(uint8_t type) { return type == TYPE_UCS8903 || type == TYPE_UCS8904 || type == TYPE_SM16825; } + static constexpr bool mustRefresh(uint8_t type) { return type == TYPE_TM1814; } static constexpr int numPWMPins(uint8_t type) { return (type - 40); } static inline int16_t getCCT() { return _cct; } diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index 54ba9d8ba..dd0e8ee8b 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -22,6 +22,7 @@ function hasW(t) { return !!(gT(t).c & 0x02); } // has white channel function hasCCT(t) { return !!(gT(t).c & 0x04); } // is white CCT enabled function is16b(t) { return !!(gT(t).c & 0x10); } // is digital 16 bit type + function mustR(t) { return !!(gT(t).c & 0x20); } // Off refresh is mandatory function numPins(t){ return Math.max(gT(t).t.length, 1); } // type length determines number of GPIO pins function S() { getLoc(); @@ -255,7 +256,7 @@ d.Sf["LA"+n].min = (isVir(t) || isAna(t)) ? 0 : 1; d.Sf["MA"+n].min = (isVir(t) || isAna(t)) ? 0 : 250; } - gId("rf"+n).onclick = (t == 31) ? (()=>{return false}) : (()=>{}); // prevent change for TM1814 + gId("rf"+n).onclick = mustR(t) ? (()=>{return false}) : (()=>{}); // prevent change change of "Refresh" checkmark when mandatory gRGBW |= hasW(t); // RGBW checkbox gId("co"+n).style.display = (isVir(t) || isAna(t)) ? "none":"inline"; // hide color order for PWM gId("dig"+n+"w").style.display = (isDig(t) && hasW(t)) ? "inline":"none"; // show swap channels dropdown @@ -457,9 +458,9 @@ mA/LED: