From 17fdb4f5c90a7f5b6fb5c21f8e4378f170e385f4 Mon Sep 17 00:00:00 2001 From: Robin Meis Date: Thu, 1 Aug 2024 19:49:47 +0200 Subject: [PATCH 001/111] Allow lower values for touch threshold --- wled00/cfg.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index f9a94e228..369ddada0 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -296,7 +296,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { #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) + touchAttachInterrupt(btnPin[s], 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 e9b7320d1ce0b21170741c1c7fc3143bf0d5b824 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sun, 14 Jul 2024 11:38:29 -0400 Subject: [PATCH 002/111] Add preliminary web stress test script --- tools/stress_test.sh | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 tools/stress_test.sh diff --git a/tools/stress_test.sh b/tools/stress_test.sh new file mode 100644 index 000000000..c107f789a --- /dev/null +++ b/tools/stress_test.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# Some web server stress tests +# +# Perform a large number of parallel requests, stress testing the web server +# TODO: some kind of performance metrics + + +TARGET=$1 + +JSON_TARGETS=('json/state' 'json/info' 'json/si', 'json/palettes' 'json/fxdata' 'settings/s.js?p=2') +FILE_TARGETS=('' 'iro.js' 'rangetouch.js' 'settings' 'settings/wifi') +CURL_ARGS="--compressed --parallel --parallel-immediate --parallel-max 2" + +# TODO: argument parsing + +# Test static file targets +TARGETS=(${JSON_TARGETS[@]}) +#TARGETS=(${FILE_TARGETS[@]}) + +# Expand target URLS to full arguments for curl +FULL_OPTIONS=$(printf "http://${TARGET}/%s -o /dev/null " "${TARGETS[@]}") + +#echo ${FULL_OPTIONS} +time curl ${CURL_ARGS} ${FULL_OPTIONS} From 91efcb910bc3d648044b655241ec094b19cb175e Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sun, 14 Jul 2024 11:38:29 -0400 Subject: [PATCH 003/111] stress_test: Add a larger JSON target --- tools/stress_test.sh | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tools/stress_test.sh b/tools/stress_test.sh index c107f789a..c3bdfd14d 100644 --- a/tools/stress_test.sh +++ b/tools/stress_test.sh @@ -7,15 +7,24 @@ TARGET=$1 +CURL_ARGS="--compressed --parallel --parallel-immediate --parallel-max 50" + JSON_TARGETS=('json/state' 'json/info' 'json/si', 'json/palettes' 'json/fxdata' 'settings/s.js?p=2') FILE_TARGETS=('' 'iro.js' 'rangetouch.js' 'settings' 'settings/wifi') -CURL_ARGS="--compressed --parallel --parallel-immediate --parallel-max 2" + +# Replicate one target many times +function replicate() { + printf "${1}?%d " {1..8} +} +read -a JSON_LARGE_TARGETS <<< $(replicate "json/si") +read -a JSON_LARGER_TARGETS <<< $(replicate "json/fxdata") # TODO: argument parsing # Test static file targets -TARGETS=(${JSON_TARGETS[@]}) +#TARGETS=(${JSON_TARGETS[@]}) #TARGETS=(${FILE_TARGETS[@]}) +TARGETS=(${JSON_LARGER_TARGETS[@]}) # Expand target URLS to full arguments for curl FULL_OPTIONS=$(printf "http://${TARGET}/%s -o /dev/null " "${TARGETS[@]}") From abcd2a2d01146c84ceb5ffd485d4d0cd80d41543 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sun, 14 Jul 2024 11:38:29 -0400 Subject: [PATCH 004/111] tools/stress_test: Allow command line target spec --- tools/stress_test.sh | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/tools/stress_test.sh b/tools/stress_test.sh index c3bdfd14d..31a0469f9 100644 --- a/tools/stress_test.sh +++ b/tools/stress_test.sh @@ -4,14 +4,16 @@ # Perform a large number of parallel requests, stress testing the web server # TODO: some kind of performance metrics - -TARGET=$1 +# Accepts two command line arguments: +# - first argument - mandatory - IP or hostname of target server +# - second argument - targert type +HOST=$1 +declare -n TARGET_STR="${2:-JSON_LARGER}_TARGETS" CURL_ARGS="--compressed --parallel --parallel-immediate --parallel-max 50" JSON_TARGETS=('json/state' 'json/info' 'json/si', 'json/palettes' 'json/fxdata' 'settings/s.js?p=2') FILE_TARGETS=('' 'iro.js' 'rangetouch.js' 'settings' 'settings/wifi') - # Replicate one target many times function replicate() { printf "${1}?%d " {1..8} @@ -19,15 +21,10 @@ function replicate() { read -a JSON_LARGE_TARGETS <<< $(replicate "json/si") read -a JSON_LARGER_TARGETS <<< $(replicate "json/fxdata") -# TODO: argument parsing - -# Test static file targets -#TARGETS=(${JSON_TARGETS[@]}) -#TARGETS=(${FILE_TARGETS[@]}) -TARGETS=(${JSON_LARGER_TARGETS[@]}) - # Expand target URLS to full arguments for curl -FULL_OPTIONS=$(printf "http://${TARGET}/%s -o /dev/null " "${TARGETS[@]}") +TARGETS=(${TARGET_STR[@]}) +#echo "${TARGETS[@]}" +FULL_TGT_OPTIONS=$(printf "http://${HOST}/%s -o /dev/null " "${TARGETS[@]}") +#echo ${FULL_TGT_OPTIONS} -#echo ${FULL_OPTIONS} -time curl ${CURL_ARGS} ${FULL_OPTIONS} +time curl ${CURL_ARGS} ${FULL_TGT_OPTIONS} From 0191af412bf1fa1f827646665d9bee33530f34c6 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sun, 14 Jul 2024 11:38:29 -0400 Subject: [PATCH 005/111] stress_test: Add small JSON target Good for measuring quick response performance --- tools/stress_test.sh | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/stress_test.sh b/tools/stress_test.sh index 31a0469f9..4ea672405 100644 --- a/tools/stress_test.sh +++ b/tools/stress_test.sh @@ -10,7 +10,9 @@ HOST=$1 declare -n TARGET_STR="${2:-JSON_LARGER}_TARGETS" -CURL_ARGS="--compressed --parallel --parallel-immediate --parallel-max 50" +PARALLEL_MAX=${PARALLEL_MAX:-50} + +CURL_ARGS="--compressed --parallel --parallel-immediate --parallel-max ${PARALLEL_MAX}" JSON_TARGETS=('json/state' 'json/info' 'json/si', 'json/palettes' 'json/fxdata' 'settings/s.js?p=2') FILE_TARGETS=('' 'iro.js' 'rangetouch.js' 'settings' 'settings/wifi') @@ -18,6 +20,7 @@ FILE_TARGETS=('' 'iro.js' 'rangetouch.js' 'settings' 'settings/wifi') function replicate() { printf "${1}?%d " {1..8} } +read -a JSON_SMALL_TARGETS <<< $(replicate "json/info") read -a JSON_LARGE_TARGETS <<< $(replicate "json/si") read -a JSON_LARGER_TARGETS <<< $(replicate "json/fxdata") From 541d3f286afab91b21c1468f82471883f45315ac Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sun, 14 Jul 2024 11:38:29 -0400 Subject: [PATCH 006/111] stress_test: Make it worse Read 40 copies! --- tools/stress_test.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/stress_test.sh b/tools/stress_test.sh index 4ea672405..0a11c4332 100644 --- a/tools/stress_test.sh +++ b/tools/stress_test.sh @@ -18,8 +18,9 @@ JSON_TARGETS=('json/state' 'json/info' 'json/si', 'json/palettes' 'json/fxdata' FILE_TARGETS=('' 'iro.js' 'rangetouch.js' 'settings' 'settings/wifi') # Replicate one target many times function replicate() { - printf "${1}?%d " {1..8} + printf "${1}?%d " {1..40} } +read -a JSON_TINY_TARGETS <<< $(replicate "json/nodes") read -a JSON_SMALL_TARGETS <<< $(replicate "json/info") read -a JSON_LARGE_TARGETS <<< $(replicate "json/si") read -a JSON_LARGER_TARGETS <<< $(replicate "json/fxdata") From 5a6ebd665787b66e94719dacf2e82d07f2c0ce6e Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sun, 14 Jul 2024 11:38:29 -0400 Subject: [PATCH 007/111] stress_test: Allow replication argument --- tools/stress_test.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tools/stress_test.sh b/tools/stress_test.sh index 0a11c4332..fe25f3b4e 100644 --- a/tools/stress_test.sh +++ b/tools/stress_test.sh @@ -4,11 +4,13 @@ # Perform a large number of parallel requests, stress testing the web server # TODO: some kind of performance metrics -# Accepts two command line arguments: +# Accepts three command line arguments: # - first argument - mandatory - IP or hostname of target server -# - second argument - targert type +# - second argument - target type (optional) +# - third argument - xfer count (for replicated targets) (optional) HOST=$1 declare -n TARGET_STR="${2:-JSON_LARGER}_TARGETS" +REPLICATE_COUNT=$(("${3:-10}")) PARALLEL_MAX=${PARALLEL_MAX:-50} @@ -18,7 +20,7 @@ JSON_TARGETS=('json/state' 'json/info' 'json/si', 'json/palettes' 'json/fxdata' FILE_TARGETS=('' 'iro.js' 'rangetouch.js' 'settings' 'settings/wifi') # Replicate one target many times function replicate() { - printf "${1}?%d " {1..40} + printf "${1}?%d " $(seq 1 ${REPLICATE_COUNT}) } read -a JSON_TINY_TARGETS <<< $(replicate "json/nodes") read -a JSON_SMALL_TARGETS <<< $(replicate "json/info") From 509cbdf476c258aa2eb5900310ea899f886ce025 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sun, 14 Jul 2024 11:38:29 -0400 Subject: [PATCH 008/111] Add UDP test code Courtesy of @_tvk_ on Discord --- tools/udp_test.py | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 tools/udp_test.py diff --git a/tools/udp_test.py b/tools/udp_test.py new file mode 100644 index 000000000..c4c9129cf --- /dev/null +++ b/tools/udp_test.py @@ -0,0 +1,46 @@ +import numpy as np +import socket + +class WledRealtimeClient: + def __init__(self, wled_controller_ip, num_pixels, udp_port=21324, max_pixels_per_packet=126): + self.wled_controller_ip = wled_controller_ip + self.num_pixels = num_pixels + self.udp_port = udp_port + self.max_pixels_per_packet = max_pixels_per_packet + self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self._prev_pixels = np.full((3, self.num_pixels), 253, dtype=np.uint8) + self.pixels = np.full((3, self.num_pixels), 1, dtype=np.uint8) + + def update(self): + # Truncate values and cast to integer + self.pixels = np.clip(self.pixels, 0, 255).astype(np.uint8) + p = np.copy(self.pixels) + + idx = np.where(~np.all(p == self._prev_pixels, axis=0))[0] + num_pixels = len(idx) + n_packets = (num_pixels + self.max_pixels_per_packet - 1) // self.max_pixels_per_packet + idx_split = np.array_split(idx, n_packets) + + header = bytes([1, 2]) # WARLS protocol header + for packet_indices in idx_split: + data = bytearray(header) + for i in packet_indices: + data.extend([i, *p[:, i]]) # Index and RGB values + self._sock.sendto(bytes(data), (self.wled_controller_ip, self.udp_port)) + + self._prev_pixels = np.copy(p) + + + +################################## LED blink test ################################## +if __name__ == "__main__": + WLED_CONTROLLER_IP = "192.168.1.153" + NUM_PIXELS = 255 # Amount of LEDs on your strip + import time + wled = WledRealtimeClient(WLED_CONTROLLER_IP, NUM_PIXELS) + print('Starting LED blink test') + while True: + for i in range(NUM_PIXELS): + wled.pixels[1, i] = 255 if wled.pixels[1, i] == 0 else 0 + wled.update() + time.sleep(.01) From 5582bbac6068b03d6ec68c94c6e7035a6a35f72c Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sun, 14 Jul 2024 11:38:29 -0400 Subject: [PATCH 009/111] stress_test: Add code for logging responses --- tools/stress_test.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/stress_test.sh b/tools/stress_test.sh index fe25f3b4e..d7c344c58 100644 --- a/tools/stress_test.sh +++ b/tools/stress_test.sh @@ -15,6 +15,7 @@ REPLICATE_COUNT=$(("${3:-10}")) PARALLEL_MAX=${PARALLEL_MAX:-50} CURL_ARGS="--compressed --parallel --parallel-immediate --parallel-max ${PARALLEL_MAX}" +CURL_PRINT_RESPONSE_ARGS="-w %{http_code}\n" JSON_TARGETS=('json/state' 'json/info' 'json/si', 'json/palettes' 'json/fxdata' 'settings/s.js?p=2') FILE_TARGETS=('' 'iro.js' 'rangetouch.js' 'settings' 'settings/wifi') From 6f01896d0443d3cb4029fc4b8c7167bd66d2c3b4 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sun, 4 Aug 2024 14:02:05 -0400 Subject: [PATCH 010/111] requestJSONBufferLock: Fix locking implementation On ESP8266, it isn't permissible to call delay() in system context; ensure this is legal before waiting. On ESP32, use an operating system mutex to ensure consistent variable state in a multicore environment, and manage the wait without needing to loop. --- wled00/util.cpp | 27 +++++++++++++++++++++++---- wled00/wled.h | 1 + 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/wled00/util.cpp b/wled00/util.cpp index 6bc02234b..d2302a085 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -213,15 +213,31 @@ bool requestJSONBufferLock(uint8_t module) DEBUG_PRINTLN(F("ERROR: JSON buffer not allocated!")); return false; } - unsigned long now = millis(); - - while (jsonBufferLock && millis()-now < 250) delay(1); // wait for fraction for buffer lock +#if defined(ARDUINO_ARCH_ESP32) + // Use a recursive mutex type in case our task is the one holding the JSON buffer. + // This can happen during large JSON web transactions. In this case, we continue immediately + // and then will return out below if the lock is still held. + if (xSemaphoreTakeRecursive(jsonBufferLockMutex, 250) == pdFALSE) return false; // timed out waiting +#elif defined(ARDUINO_ARCH_ESP8266) + // If we're in system context, delay() won't return control to the user context, so there's + // no point in waiting. + if (can_yield()) { + unsigned long now = millis(); + while (jsonBufferLock && (millis()-now < 250)) delay(1); // wait for fraction for buffer lock + } +#else + #error Unsupported task framework - fix requestJSONBufferLock +#endif + // If the lock is still held - by us, or by another task if (jsonBufferLock) { DEBUG_PRINT(F("ERROR: Locking JSON buffer failed! (still locked by ")); DEBUG_PRINT(jsonBufferLock); DEBUG_PRINTLN(")"); - return false; // waiting time-outed +#ifdef ARDUINO_ARCH_ESP32 + xSemaphoreGiveRecursive(jsonBufferLockMutex); +#endif + return false; } jsonBufferLock = module ? module : 255; @@ -239,6 +255,9 @@ void releaseJSONBufferLock() DEBUG_PRINT(jsonBufferLock); DEBUG_PRINTLN(")"); jsonBufferLock = 0; +#ifdef ARDUINO_ARCH_ESP32 + xSemaphoreGiveRecursive(jsonBufferLockMutex); +#endif } diff --git a/wled00/wled.h b/wled00/wled.h index f761b970d..d6915e7fb 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -810,6 +810,7 @@ WLED_GLOBAL int8_t spi_sclk _INIT(SPISCLKPIN); // global ArduinoJson buffer #if defined(ARDUINO_ARCH_ESP32) WLED_GLOBAL JsonDocument *pDoc _INIT(nullptr); +WLED_GLOBAL SemaphoreHandle_t jsonBufferLockMutex _INIT(xSemaphoreCreateRecursiveMutex()); #else WLED_GLOBAL StaticJsonDocument gDoc; WLED_GLOBAL JsonDocument *pDoc _INIT(&gDoc); From e701b5b5ebddb62e565b74ea2c45da7bc8ef8cc2 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sun, 4 Aug 2024 14:02:05 -0400 Subject: [PATCH 011/111] util: Print locking module when JSON lock fails For debugging, also log who was trying to lock when it was contended. --- wled00/util.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/wled00/util.cpp b/wled00/util.cpp index d2302a085..5d4428be4 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -231,7 +231,9 @@ bool requestJSONBufferLock(uint8_t module) #endif // If the lock is still held - by us, or by another task if (jsonBufferLock) { - DEBUG_PRINT(F("ERROR: Locking JSON buffer failed! (still locked by ")); + DEBUG_PRINT(F("ERROR: Locking JSON buffer (")); + DEBUG_PRINT(module); + DEBUG_PRINT(F(") failed! (still locked by ")); DEBUG_PRINT(jsonBufferLock); DEBUG_PRINTLN(")"); #ifdef ARDUINO_ARCH_ESP32 From 113dbbdf94f9f5b7bc0d007bb4cd7fe7af9b5c2d Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sun, 4 Aug 2024 15:08:46 -0400 Subject: [PATCH 012/111] Use DEBUG_PRINTF_P for jsonBufferLock Tiny code space usage reduction. --- wled00/util.cpp | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/wled00/util.cpp b/wled00/util.cpp index 5d4428be4..86ba5b486 100644 --- a/wled00/util.cpp +++ b/wled00/util.cpp @@ -231,11 +231,7 @@ bool requestJSONBufferLock(uint8_t module) #endif // If the lock is still held - by us, or by another task if (jsonBufferLock) { - DEBUG_PRINT(F("ERROR: Locking JSON buffer (")); - DEBUG_PRINT(module); - DEBUG_PRINT(F(") failed! (still locked by ")); - DEBUG_PRINT(jsonBufferLock); - DEBUG_PRINTLN(")"); + DEBUG_PRINTF_P(PSTR("ERROR: Locking JSON buffer (%d) failed! (still locked by %d)\n"), module, jsonBufferLock); #ifdef ARDUINO_ARCH_ESP32 xSemaphoreGiveRecursive(jsonBufferLockMutex); #endif @@ -243,9 +239,7 @@ bool requestJSONBufferLock(uint8_t module) } jsonBufferLock = module ? module : 255; - DEBUG_PRINT(F("JSON buffer locked. (")); - DEBUG_PRINT(jsonBufferLock); - DEBUG_PRINTLN(")"); + DEBUG_PRINTF_P(PSTR("JSON buffer locked. (%d)\n"), jsonBufferLock); pDoc->clear(); return true; } @@ -253,9 +247,7 @@ bool requestJSONBufferLock(uint8_t module) void releaseJSONBufferLock() { - DEBUG_PRINT(F("JSON buffer released. (")); - DEBUG_PRINT(jsonBufferLock); - DEBUG_PRINTLN(")"); + DEBUG_PRINTF_P(PSTR("JSON buffer released. (%d)\n"), jsonBufferLock); jsonBufferLock = 0; #ifdef ARDUINO_ARCH_ESP32 xSemaphoreGiveRecursive(jsonBufferLockMutex); From 1d27aa2f18fb11c1eb811b703a920408524d8248 Mon Sep 17 00:00:00 2001 From: Will Miles Date: Sun, 14 Jul 2024 11:38:29 -0400 Subject: [PATCH 013/111] Move .ino to .cpp This fixes platformio continually recompiling this file. --- wled00/{wled00.ino => wled00.cpp} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename wled00/{wled00.ino => wled00.cpp} (100%) diff --git a/wled00/wled00.ino b/wled00/wled00.cpp similarity index 100% rename from wled00/wled00.ino rename to wled00/wled00.cpp From e82f38e277b6ad3d7b943b8868c3f88cb7c53382 Mon Sep 17 00:00:00 2001 From: Blaz Kristan Date: Mon, 5 Aug 2024 16:42:21 +0200 Subject: [PATCH 014/111] Tuning --- wled00/FX.h | 38 +++++++++--------------------- wled00/FX_2Dfcn.cpp | 6 ++--- wled00/FX_fcn.cpp | 46 ++++++++++++++++++------------------- wled00/colors.cpp | 41 +++++++++++++++------------------ wled00/data/settings_um.htm | 21 +++++++++-------- wled00/set.cpp | 9 ++++---- wled00/util.cpp | 2 +- 7 files changed, 73 insertions(+), 90 deletions(-) diff --git a/wled00/FX.h b/wled00/FX.h index e4ebd3016..57de5df44 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -563,16 +563,16 @@ typedef struct Segment { // transition functions void startTransition(uint16_t dur); // transition has to start before actual segment values change - void stopTransition(void); // ends transition mode by destroying transition structure - void handleTransition(void); + void stopTransition(void); // ends transition mode by destroying transition structure (does nothing if not in transition) + inline void handleTransition(void) { if (progress() == 0xFFFFU) stopTransition(); } #ifndef WLED_DISABLE_MODE_BLEND void swapSegenv(tmpsegd_t &tmpSegD); // copies segment data into specifed buffer, if buffer is not a transition buffer, segment data is overwritten from transition buffer void restoreSegenv(tmpsegd_t &tmpSegD); // restores segment data from buffer, if buffer is not transition buffer, changed values are copied to transition buffer #endif - uint16_t progress(void); // transition progression between 0-65535 - uint8_t currentBri(bool useCct = false); // current segment brightness/CCT (blended while in transition) - uint8_t currentMode(void); // currently active effect/mode (while in transition) - uint32_t currentColor(uint8_t slot); // currently active segment color (blended while in transition) + uint16_t progress(void) const; // transition progression between 0-65535 + uint8_t currentBri(bool useCct = false) const; // current segment brightness/CCT (blended while in transition) + uint8_t currentMode(void) const; // currently active effect/mode (while in transition) + uint32_t currentColor(uint8_t slot) const; // currently active segment color (blended while in transition) CRGBPalette16 &loadPalette(CRGBPalette16 &tgt, uint8_t pal); void setCurrentPalette(void); @@ -587,7 +587,7 @@ typedef struct Segment { inline void setPixelColor(float i, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0, bool aa = true) { setPixelColor(i, RGBW32(r,g,b,w), aa); } inline void setPixelColor(float i, CRGB c, bool aa = true) { setPixelColor(i, RGBW32(c.r,c.g,c.b,0), aa); } #endif - uint32_t getPixelColor(int i); + uint32_t getPixelColor(int i) const; // 1D support functions (some implement 2D as well) void blur(uint8_t, bool smear = false); void fill(uint32_t c); @@ -599,8 +599,8 @@ typedef struct Segment { inline void addPixelColor(int n, byte r, byte g, byte b, byte w = 0, bool fast = false) { addPixelColor(n, RGBW32(r,g,b,w), fast); } inline void addPixelColor(int n, CRGB c, bool fast = false) { addPixelColor(n, RGBW32(c.r,c.g,c.b,0), fast); } inline void fadePixelColor(uint16_t n, uint8_t fade) { setPixelColor(n, color_fade(getPixelColor(n), fade, true)); } - uint32_t color_from_palette(uint16_t, bool mapping, bool wrap, uint8_t mcol, uint8_t pbri = 255); - uint32_t color_wheel(uint8_t pos); + 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 matrix uint16_t virtualWidth(void) const; // segment width in virtual pixels (accounts for groupping and spacing) @@ -618,7 +618,7 @@ typedef struct Segment { inline void setPixelColorXY(float x, float y, byte r, byte g, byte b, byte w = 0, bool aa = true) { setPixelColorXY(x, y, RGBW32(r,g,b,w), aa); } inline void setPixelColorXY(float x, float y, CRGB c, bool aa = true) { setPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), aa); } #endif - uint32_t getPixelColorXY(int x, int y); + uint32_t getPixelColorXY(int x, int y) const; // 2D support functions inline void blendPixelColorXY(uint16_t x, uint16_t y, uint32_t color, uint8_t blend) { setPixelColorXY(x, y, color_blend(getPixelColorXY(x,y), color, blend)); } inline void blendPixelColorXY(uint16_t x, uint16_t y, CRGB c, uint8_t blend) { blendPixelColorXY(x, y, RGBW32(c.r,c.g,c.b,0), blend); } @@ -729,15 +729,7 @@ class WS2812FX { // 96 bytes customMappingSize(0), _lastShow(0), _segment_index(0), - _mainSegment(0), - _queuedChangesSegId(255), - _qStart(0), - _qStop(0), - _qStartY(0), - _qStopY(0), - _qGrouping(0), - _qSpacing(0), - _qOffset(0) + _mainSegment(0) { WS2812FX::instance = this; _mode.reserve(_modeCount); // allocate memory to prevent initial fragmentation (does not increase size()) @@ -945,14 +937,6 @@ class WS2812FX { // 96 bytes uint8_t _segment_index; uint8_t _mainSegment; - uint8_t _queuedChangesSegId; - uint16_t _qStart, _qStop, _qStartY, _qStopY; - uint8_t _qGrouping, _qSpacing; - uint16_t _qOffset; -/* - void - setUpSegmentFromQueuedChanges(void); -*/ }; extern const char JSON_mode_names[]; diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index b262c157d..c135c3dbc 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -180,7 +180,7 @@ void IRAM_ATTR Segment::setPixelColorXY(int x, int y, uint32_t col) if (reverse ) x = virtualWidth() - x - 1; if (reverse_y) y = virtualHeight() - y - 1; - if (transpose) { unsigned t = x; x = y; y = t; } // swap X & Y if segment transposed + if (transpose) { std::swap(x,y); } // swap X & Y if segment transposed x *= groupLength(); // expand to physical pixels y *= groupLength(); // expand to physical pixels @@ -261,12 +261,12 @@ void Segment::setPixelColorXY(float x, float y, uint32_t col, bool aa) #endif // returns RGBW values of pixel -uint32_t IRAM_ATTR Segment::getPixelColorXY(int x, int y) { +uint32_t IRAM_ATTR Segment::getPixelColorXY(int x, int y) const { if (!isActive()) return 0; // not active if (x >= virtualWidth() || y >= virtualHeight() || x<0 || y<0) return 0; // if pixel would fall out of virtual segment just exit if (reverse ) x = virtualWidth() - x - 1; if (reverse_y) y = virtualHeight() - y - 1; - if (transpose) { unsigned t = x; x = y; y = t; } // swap X & Y if segment transposed + if (transpose) { std::swap(x,y); } // swap X & Y if segment transposed x *= groupLength(); // expand to physical pixels y *= groupLength(); // expand to physical pixels if (x >= width() || y >= height()) return 0; diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 48570b30d..758df62fa 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -327,13 +327,8 @@ void Segment::stopTransition() { } } -void Segment::handleTransition() { - unsigned _progress = progress(); - if (_progress == 0xFFFFU) stopTransition(); -} - // transition progression between 0-65535 -uint16_t IRAM_ATTR Segment::progress() { +uint16_t IRAM_ATTR Segment::progress() const { if (isInTransition()) { unsigned diff = millis() - _t->_start; if (_t->_dur > 0 && diff < _t->_dur) return diff * 0xFFFFU / _t->_dur; @@ -412,7 +407,7 @@ void Segment::restoreSegenv(tmpsegd_t &tmpSeg) { } #endif -uint8_t IRAM_ATTR Segment::currentBri(bool useCct) { +uint8_t IRAM_ATTR Segment::currentBri(bool useCct) const { unsigned prog = progress(); if (prog < 0xFFFFU) { unsigned curBri = (useCct ? cct : (on ? opacity : 0)) * prog; @@ -422,7 +417,7 @@ uint8_t IRAM_ATTR Segment::currentBri(bool useCct) { return (useCct ? cct : (on ? opacity : 0)); } -uint8_t IRAM_ATTR Segment::currentMode() { +uint8_t IRAM_ATTR Segment::currentMode() const { #ifndef WLED_DISABLE_MODE_BLEND unsigned prog = progress(); if (modeBlending && prog < 0xFFFFU) return _t->_modeT; @@ -430,7 +425,7 @@ uint8_t IRAM_ATTR Segment::currentMode() { return mode; } -uint32_t IRAM_ATTR Segment::currentColor(uint8_t slot) { +uint32_t IRAM_ATTR Segment::currentColor(uint8_t slot) const { if (slot >= NUM_COLORS) slot = 0; #ifndef WLED_DISABLE_MODE_BLEND return isInTransition() ? color_blend(_t->_segT._colorT[slot], colors[slot], progress(), true) : colors[slot]; @@ -685,9 +680,11 @@ uint16_t IRAM_ATTR Segment::virtualLength() const { vLen = vH; break; case M12_pCorner: - case M12_pArc: vLen = max(vW,vH); // get the longest dimension break; + case M12_pArc: + vLen = sqrt16(vH*vH + vW*vW); // use diagonal + break; case M12_sPinwheel: vLen = getPinwheelLength(vW, vH); break; @@ -730,12 +727,14 @@ void IRAM_ATTR Segment::setPixelColor(int i, uint32_t col) if (i==0) setPixelColorXY(0, 0, col); else { - float step = HALF_PI / (2.85f*i); - for (float rad = 0.0f; rad <= HALF_PI+step/2; rad += step) { - // may want to try float version as well (with or without antialiasing) - int x = roundf(sin_t(rad) * i); - int y = roundf(cos_t(rad) * i); + float r = i; + float step = HALF_PI / (2.8284f * r + 4); // we only need (PI/4)/(r/sqrt(2)+1) steps + for (float rad = 0.0f; rad <= (HALF_PI/2)+step/2; rad += step) { + int x = roundf(sin_t(rad) * r); + int y = roundf(cos_t(rad) * r); + // exploit symmetry setPixelColorXY(x, y, col); + setPixelColorXY(y, x, col); } // Bresenham’s Algorithm (may not fill every pixel) //int d = 3 - (2*i); @@ -893,7 +892,7 @@ void Segment::setPixelColor(float i, uint32_t col, bool aa) } #endif -uint32_t IRAM_ATTR Segment::getPixelColor(int i) +uint32_t IRAM_ATTR Segment::getPixelColor(int i) const { if (!isActive()) return 0; // not active #ifndef WLED_DISABLE_2D @@ -1059,7 +1058,7 @@ void Segment::fade_out(uint8_t rate) { const int rows = virtualHeight(); // will be 1 for 1D rate = (255-rate) >> 1; - float mappedRate = float(rate) +1.1f; + float mappedRate = 1.0f / (float(rate) + 1.1f); uint32_t color = colors[1]; // SEGCOLOR(1); // target color int w2 = W(color); @@ -1069,15 +1068,16 @@ void Segment::fade_out(uint8_t rate) { for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) { color = is2D() ? getPixelColorXY(x, y) : getPixelColor(x); + if (color == colors[1]) continue; // already at target color int w1 = W(color); int r1 = R(color); int g1 = G(color); int b1 = B(color); - int wdelta = (w2 - w1) / mappedRate; - int rdelta = (r2 - r1) / mappedRate; - int gdelta = (g2 - g1) / mappedRate; - int bdelta = (b2 - b1) / mappedRate; + int wdelta = (w2 - w1) * mappedRate; + int rdelta = (r2 - r1) * mappedRate; + int gdelta = (g2 - g1) * mappedRate; + int bdelta = (b2 - b1) * mappedRate; // if fade isn't complete, make sure delta is at least 1 (fixes rounding issues) wdelta += (w2 == w1) ? 0 : (w2 > w1) ? 1 : -1; @@ -1149,7 +1149,7 @@ void Segment::blur(uint8_t blur_amount, bool smear) { * The colours are a transition r -> g -> b -> back to r * Inspired by the Adafruit examples. */ -uint32_t Segment::color_wheel(uint8_t pos) { +uint32_t Segment::color_wheel(uint8_t pos) const { if (palette) return color_from_palette(pos, false, true, 0); // perhaps "strip.paletteBlend < 2" should be better instead of "true" uint8_t w = W(currentColor(0)); pos = 255 - pos; @@ -1173,7 +1173,7 @@ uint32_t Segment::color_wheel(uint8_t pos) { * @param pbri Value to scale the brightness of the returned color by. Default is 255. (no scaling) * @returns Single color from palette */ -uint32_t Segment::color_from_palette(uint16_t i, bool mapping, bool wrap, uint8_t mcol, uint8_t pbri) { +uint32_t Segment::color_from_palette(uint16_t i, bool mapping, bool wrap, uint8_t mcol, uint8_t pbri) const { uint32_t color = gamma32(currentColor(mcol)); // default palette or no RGB support on segment diff --git a/wled00/colors.cpp b/wled00/colors.cpp index 82dde47bb..ebea7ea05 100644 --- a/wled00/colors.cpp +++ b/wled00/colors.cpp @@ -8,10 +8,10 @@ * color blend function */ uint32_t color_blend(uint32_t color1, uint32_t color2, uint16_t blend, bool b16) { - if(blend == 0) return color1; + if (blend == 0) return color1; unsigned blendmax = b16 ? 0xFFFF : 0xFF; - if(blend == blendmax) return color2; - uint8_t shift = b16 ? 16 : 8; + if (blend == blendmax) return color2; + unsigned shift = b16 ? 16 : 8; uint32_t w1 = W(color1); uint32_t r1 = R(color1); @@ -73,22 +73,19 @@ uint32_t color_fade(uint32_t c1, uint8_t amount, bool video) uint32_t g = G(c1); uint32_t b = B(c1); uint32_t w = W(c1); - if (video) { - uint32_t scale = amount; // 32bit for faster calculation - scaledcolor = (((r * scale) >> 8) << 16) + ((r && scale) ? 1 : 0); - scaledcolor |= (((g * scale) >> 8) << 8) + ((g && scale) ? 1 : 0); - scaledcolor |= ((b * scale) >> 8) + ((b && scale) ? 1 : 0); + uint32_t scale = amount + !video; // 32bit for faster calculation + if (video) { + scaledcolor = (((r * scale) >> 8) << 16) + ((r && scale) ? 1 : 0); + scaledcolor |= (((g * scale) >> 8) << 8) + ((g && scale) ? 1 : 0); + scaledcolor |= ((b * scale) >> 8) + ((b && scale) ? 1 : 0); scaledcolor |= (((w * scale) >> 8) << 24) + ((w && scale) ? 1 : 0); - return scaledcolor; - } - else { - uint32_t scale = 1 + amount; - scaledcolor = ((r * scale) >> 8) << 16; + } else { + scaledcolor = ((r * scale) >> 8) << 16; scaledcolor |= ((g * scale) >> 8) << 8; - scaledcolor |= (b * scale) >> 8; + scaledcolor |= (b * scale) >> 8; scaledcolor |= ((w * scale) >> 8) << 24; - return scaledcolor; } + return scaledcolor; } void setRandomColor(byte* rgb) @@ -140,25 +137,25 @@ CRGBPalette16 generateHarmonicRandomPalette(CRGBPalette16 &basepalette) case 1: // triadic harmonics[0] = basehue + 113 + random8(15); harmonics[1] = basehue + 233 + random8(15); - harmonics[2] = basehue -7 + random8(15); + harmonics[2] = basehue - 7 + random8(15); break; case 2: // split-complementary harmonics[0] = basehue + 145 + random8(10); harmonics[1] = basehue + 205 + random8(10); - harmonics[2] = basehue - 5 + random8(10); + harmonics[2] = basehue - 5 + random8(10); break; case 3: // square - harmonics[0] = basehue + 85 + random8(10); + harmonics[0] = basehue + 85 + random8(10); harmonics[1] = basehue + 175 + random8(10); harmonics[2] = basehue + 265 + random8(10); break; case 4: // tetradic - harmonics[0] = basehue + 80 + random8(20); + harmonics[0] = basehue + 80 + random8(20); harmonics[1] = basehue + 170 + random8(20); - harmonics[2] = basehue + random8(30)-15; + harmonics[2] = basehue - 15 + random8(30); break; } @@ -384,13 +381,13 @@ bool colorFromHexString(byte* rgb, const char* in) { return true; } -float minf (float v, float w) +static inline float minf(float v, float w) { if (w > v) return v; return w; } -float maxf (float v, float w) +static inline float maxf(float v, float w) { if (w > v) return w; return v; diff --git a/wled00/data/settings_um.htm b/wled00/data/settings_um.htm index a50fc8269..686c881d9 100644 --- a/wled00/data/settings_um.htm +++ b/wled00/data/settings_um.htm @@ -34,7 +34,7 @@ if (d.um_p[0]==-1) d.um_p.shift(); // remove filler d.Sf.SDA.max = d.Sf.SCL.max = d.Sf.MOSI.max = d.Sf.SCLK.max = d.Sf.MISO.max = d.max_gpio; //for (let i of d.getElementsByTagName("input")) if (i.type === "number" && i.name.replace("[]","").substr(-3) === "pin") i.max = d.max_gpio; - pinDropdowns(); // convert INPUT to SELECT for pins + pinDD(); // convert INPUT to SELECT for pins }); // error event scE.addEventListener("error", (ev) => { @@ -165,25 +165,25 @@ urows += `
`; } } - 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 015/111] 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 016/111] 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 017/111] 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 018/111] 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 019/111] 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 020/111] 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 021/111] 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 022/111] 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 023/111] 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 024/111] 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 025/111] 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 026/111] 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 027/111] 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 028/111] 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 029/111] #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 030/111] 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 031/111] 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 032/111] 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 033/111] 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 034/111] 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 035/111] 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 036/111] 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 037/111] 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 038/111] 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 039/111] 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 040/111] 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 041/111] 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 107/111] 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 108/111] 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 109/111] 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 110/111] 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 111/111] 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: