diff --git a/CHANGELOG.md b/CHANGELOG.md index ac52339f0..4564e5786 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ ### Builds after release 0.12.0 +#### Build 2203060 + +- Dynamic hiding of unused color controls in UI (PR #2567) +- Removed native Cronixie support and added Cronixie usermod +- Fixed disabled timed preset expanding calendar +- Fixed Color Order setting shown for analog busses +- Fixed incorrect operator (#2566) + +#### Build 2203011 + +- IR rewrite (PR #2561), supports CCT +- Added locate button to Time settings +- CSS fixes and adjustments +- Consistent Tab indentation in index JS and CSS +- Added initial contribution style guideline + #### Build 2202222 - Version bump to 0.13.0-b7 "Toki" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..560a70973 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,78 @@ +## Thank you for making WLED better! + +Here are a few suggestions to make it easier for you to contribute! + +### Code style + +When in doubt, it is easiest to replicate the code style you find in the files you want to edit :) +Below are the guidelines we use in the WLED repository. + +#### Indentation + +We use tabs for Indentation in Web files (.html/.css/.js) and spaces (2 per indentation level) for all other files. +You are all set if you have enabled `Editor: Detect Indentation` in VS Code. + +#### Blocks + +Whether the opening bracket of e.g. an `if` block is in the same line as the condition or in a separate line is up to your discretion. If there is only one statement, leaving out block braches is acceptable. + +Good: +```cpp +if (a == b) { + doStuff(a); +} +``` + +```cpp +if (a == b) +{ + doStuff(a); +} +``` + +```cpp +if (a == b) doStuff(a); +``` + +There should always be a space between a keyword and its condition and between the condition and brace. +Within the condition, no space should be between the paranthesis and variables. +Spaces between variables and operators are up to the authors discretion. +There should be no space between function names and their argument parenthesis. + +Good: +```cpp +if (a == b) { + doStuff(a); +} +``` + +Not good: +```cpp +if( a==b ){ + doStuff ( a); +} +``` + +#### Comments + +Comments should have a space between the delimiting characters (e.g. `//`) and the comment text. +Note: This is a recent change, the majority of the codebase still has comments without spaces. + +Good: +``` +// This is a comment. + +/* This is a CSS inline comment */ + +/* + * This is a comment + * wrapping over multiple lines, + * used in WLED for file headers and function explanations + */ + + +``` + +There is no set character limit for a comment within a line, +though as a rule of thumb you should wrap your comment if it exceeds the width of your editor window. +Inline comments are OK if they describe that line only and are not exceedingly wide. \ No newline at end of file diff --git a/platformio.ini b/platformio.ini index ed8d15388..47d6e9b52 100644 --- a/platformio.ini +++ b/platformio.ini @@ -337,10 +337,10 @@ build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_Ethernet -D lib_deps = ${esp32.lib_deps} board_build.partitions = ${esp32.default_partitions} -# ESP32 ETH build that fits in old 1M app space (disables Blynk, Cronixie, and Hue sync) +# ESP32 ETH build that fits in old 1M app space (disables Blynk and Hue sync) [env:esp32_eth_ota1mapp] extends = env:esp32_eth -build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_Ethernet_OTA -D RLYPIN=-1 -D WLED_USE_ETHERNET -D BTNPIN=-1 -D WLED_DISABLE_BLYNK -D WLED_DISABLE_CRONIXIE -D WLED_DISABLE_HUESYNC +build_flags = ${common.build_flags_esp32} -D WLED_RELEASE_NAME=ESP32_Ethernet_OTA -D RLYPIN=-1 -D WLED_USE_ETHERNET -D BTNPIN=-1 -D WLED_DISABLE_BLYNK -D WLED_DISABLE_HUESYNC [env:esp32s2_saola] board = esp32-s2-saola-1 @@ -536,7 +536,7 @@ build_flags = ${common.build_flags_esp32} -D WLED_DISABLE_BROWNOUT_DET -D WLED_D -D LEDPIN=12 -D RLYPIN=27 -D BTNPIN=34 - -D WLED_DISABLE_INFRARED + -D WLED_DISABLE_BLYNK -D DEFAULT_LED_COUNT=6 # Display config -D ST7789_DRIVER diff --git a/platformio_override.ini.sample b/platformio_override.ini.sample index 70ef96600..c8fb311fe 100644 --- a/platformio_override.ini.sample +++ b/platformio_override.ini.sample @@ -22,7 +22,6 @@ build_flags = ${common.build_flags_esp8266} ; -D WLED_DISABLE_OTA ; -D WLED_DISABLE_ALEXA ; -D WLED_DISABLE_BLYNK -; -D WLED_DISABLE_CRONIXIE ; -D WLED_DISABLE_HUESYNC ; -D WLED_DISABLE_INFRARED ; -D WLED_DISABLE_WEBSOCKETS diff --git a/readme.md b/readme.md index 07166b017..2e7843c20 100644 --- a/readme.md +++ b/readme.md @@ -27,7 +27,7 @@ A fast and feature-rich implementation of an ESP8266/ESP32 webserver to control - Presets can be used to automatically execute API calls - Nightlight function (gradually dims down) - Full OTA software updatability (HTTP + ArduinoOTA), password protectable -- Configurable analog clock + support for the Cronixie kit by Diamex +- Configurable analog clock (Cronixie, 7-segment and EleksTube IPS clock support via usermods) - Configurable Auto Brightness limit for safer operation - Filesystem-based config for easier backup of presets and settings diff --git a/usermods/Cronixie/readme.md b/usermods/Cronixie/readme.md new file mode 100644 index 000000000..1eeac8ed0 --- /dev/null +++ b/usermods/Cronixie/readme.md @@ -0,0 +1,8 @@ +# Cronixie clock usermod + +This usermod supports driving the Cronixie M and L clock kits by Diamex. + +## Installation + +Compile and upload after adding `-D USERMOD_CRONIXIE` to `build_flags` of your PlatformIO environment. +Make sure the Auto Brightness Limiter is enabled at 420mA (!) and configure 60 WS281x LEDs. \ No newline at end of file diff --git a/usermods/Cronixie/usermod_cronixie.h b/usermods/Cronixie/usermod_cronixie.h new file mode 100644 index 000000000..5e4255f43 --- /dev/null +++ b/usermods/Cronixie/usermod_cronixie.h @@ -0,0 +1,301 @@ +#pragma once + +#include "wled.h" + +class UsermodCronixie : public Usermod { + private: + unsigned long lastTime = 0; + char cronixieDisplay[7] = "HHMMSS"; + byte _digitOut[6] = {10,10,10,10,10,10}; + byte dP[6] = {255, 255, 255, 255, 255, 255}; + + // set your config variables to their boot default value (this can also be done in readFromConfig() or a constructor if you prefer) + bool backlight = true; + + public: + void initCronixie() + { + if (dP[0] == 255) // if dP[0] is 255, cronixie is not yet init'ed + { + setCronixie(); + strip.getSegment(0).grouping = 10; // 10 LEDs per digit + } + } + + void setup() { + + } + + void loop() { + if (!toki.isTick()) return; + initCronixie(); + _overlayCronixie(); + strip.trigger(); + } + + byte getSameCodeLength(char code, int index, char const cronixieDisplay[]) + { + byte counter = 0; + + for (int i = index+1; i < 6; i++) + { + if (cronixieDisplay[i] == code) + { + counter++; + } else { + return counter; + } + } + return counter; + } + + void setCronixie() + { + /* + * digit purpose index + * 0-9 | 0-9 (incl. random) + * 10 | blank + * 11 | blank, bg off + * 12 | test upw. + * 13 | test dnw. + * 14 | binary AM/PM + * 15 | BB upper +50 for no trailing 0 + * 16 | BBB + * 17 | BBBB + * 18 | BBBBB + * 19 | BBBBBB + * 20 | H + * 21 | HH + * 22 | HHH + * 23 | HHHH + * 24 | M + * 25 | MM + * 26 | MMM + * 27 | MMMM + * 28 | MMMMM + * 29 | MMMMMM + * 30 | S + * 31 | SS + * 32 | SSS + * 33 | SSSS + * 34 | SSSSS + * 35 | SSSSSS + * 36 | Y + * 37 | YY + * 38 | YYYY + * 39 | I + * 40 | II + * 41 | W + * 42 | WW + * 43 | D + * 44 | DD + * 45 | DDD + * 46 | V + * 47 | VV + * 48 | VVV + * 49 | VVVV + * 50 | VVVVV + * 51 | VVVVVV + * 52 | v + * 53 | vv + * 54 | vvv + * 55 | vvvv + * 56 | vvvvv + * 57 | vvvvvv + */ + + //H HourLower | HH - Hour 24. | AH - Hour 12. | HHH Hour of Month | HHHH Hour of Year + //M MinuteUpper | MM Minute of Hour | MMM Minute of 12h | MMMM Minute of Day | MMMMM Minute of Month | MMMMMM Minute of Year + //S SecondUpper | SS Second of Minute | SSS Second of 10 Minute | SSSS Second of Hour | SSSSS Second of Day | SSSSSS Second of Week + //B AM/PM | BB 0-6/6-12/12-18/18-24 | BBB 0-3... | BBBB 0-1.5... | BBBBB 0-1 | BBBBBB 0-0.5 + + //Y YearLower | YY - Year LU | YYYY - Std. + //I MonthLower | II - Month of Year + //W Week of Month | WW Week of Year + //D Day of Week | DD Day Of Month | DDD Day Of Year + + DEBUG_PRINT("cset "); + DEBUG_PRINTLN(cronixieDisplay); + + for (int i = 0; i < 6; i++) + { + dP[i] = 10; + switch (cronixieDisplay[i]) + { + case '_': dP[i] = 10; break; + case '-': dP[i] = 11; break; + case 'r': dP[i] = random(1,7); break; //random btw. 1-6 + case 'R': dP[i] = random(0,10); break; //random btw. 0-9 + //case 't': break; //Test upw. + //case 'T': break; //Test dnw. + case 'b': dP[i] = 14 + getSameCodeLength('b',i,cronixieDisplay); i = i+dP[i]-14; break; + case 'B': dP[i] = 14 + getSameCodeLength('B',i,cronixieDisplay); i = i+dP[i]-14; break; + case 'h': dP[i] = 70 + getSameCodeLength('h',i,cronixieDisplay); i = i+dP[i]-70; break; + case 'H': dP[i] = 20 + getSameCodeLength('H',i,cronixieDisplay); i = i+dP[i]-20; break; + case 'A': dP[i] = 108; i++; break; + case 'a': dP[i] = 58; i++; break; + case 'm': dP[i] = 74 + getSameCodeLength('m',i,cronixieDisplay); i = i+dP[i]-74; break; + case 'M': dP[i] = 24 + getSameCodeLength('M',i,cronixieDisplay); i = i+dP[i]-24; break; + case 's': dP[i] = 80 + getSameCodeLength('s',i,cronixieDisplay); i = i+dP[i]-80; break; //refresh more often bc. of secs + case 'S': dP[i] = 30 + getSameCodeLength('S',i,cronixieDisplay); i = i+dP[i]-30; break; + case 'Y': dP[i] = 36 + getSameCodeLength('Y',i,cronixieDisplay); i = i+dP[i]-36; break; + case 'y': dP[i] = 86 + getSameCodeLength('y',i,cronixieDisplay); i = i+dP[i]-86; break; + case 'I': dP[i] = 39 + getSameCodeLength('I',i,cronixieDisplay); i = i+dP[i]-39; break; //Month. Don't ask me why month and minute both start with M. + case 'i': dP[i] = 89 + getSameCodeLength('i',i,cronixieDisplay); i = i+dP[i]-89; break; + //case 'W': break; + //case 'w': break; + case 'D': dP[i] = 43 + getSameCodeLength('D',i,cronixieDisplay); i = i+dP[i]-43; break; + case 'd': dP[i] = 93 + getSameCodeLength('d',i,cronixieDisplay); i = i+dP[i]-93; break; + case '0': dP[i] = 0; break; + case '1': dP[i] = 1; break; + case '2': dP[i] = 2; break; + case '3': dP[i] = 3; break; + case '4': dP[i] = 4; break; + case '5': dP[i] = 5; break; + case '6': dP[i] = 6; break; + case '7': dP[i] = 7; break; + case '8': dP[i] = 8; break; + case '9': dP[i] = 9; break; + //case 'V': break; //user var0 + //case 'v': break; //user var1 + } + } + DEBUG_PRINT("result "); + for (int i = 0; i < 5; i++) + { + DEBUG_PRINT((int)dP[i]); + DEBUG_PRINT(" "); + } + DEBUG_PRINTLN((int)dP[5]); + + _overlayCronixie(); // refresh + } + + void _overlayCronixie() + { + byte h = hour(localTime); + byte h0 = h; + byte m = minute(localTime); + byte s = second(localTime); + byte d = day(localTime); + byte mi = month(localTime); + int y = year(localTime); + //this has to be changed in time for 22nd century + y -= 2000; if (y<0) y += 30; //makes countdown work + + if (useAMPM && !countdownMode) + { + if (h>12) h-=12; + else if (h==0) h+=12; + } + for (int i = 0; i < 6; i++) + { + if (dP[i] < 12) _digitOut[i] = dP[i]; + else { + if (dP[i] < 65) + { + switch(dP[i]) + { + case 21: _digitOut[i] = h/10; _digitOut[i+1] = h- _digitOut[i]*10; i++; break; //HH + case 25: _digitOut[i] = m/10; _digitOut[i+1] = m- _digitOut[i]*10; i++; break; //MM + case 31: _digitOut[i] = s/10; _digitOut[i+1] = s- _digitOut[i]*10; i++; break; //SS + + case 20: _digitOut[i] = h- (h/10)*10; break; //H + case 24: _digitOut[i] = m/10; break; //M + case 30: _digitOut[i] = s/10; break; //S + + case 43: _digitOut[i] = weekday(localTime); _digitOut[i]--; if (_digitOut[i]<1) _digitOut[i]= 7; break; //D + case 44: _digitOut[i] = d/10; _digitOut[i+1] = d- _digitOut[i]*10; i++; break; //DD + case 40: _digitOut[i] = mi/10; _digitOut[i+1] = mi- _digitOut[i]*10; i++; break; //II + case 37: _digitOut[i] = y/10; _digitOut[i+1] = y- _digitOut[i]*10; i++; break; //YY + case 39: _digitOut[i] = 2; _digitOut[i+1] = 0; _digitOut[i+2] = y/10; _digitOut[i+3] = y- _digitOut[i+2]*10; i+=3; break; //YYYY + + //case 16: _digitOut[i+2] = ((h0/3)&1)?1:0; i++; //BBB (BBBB NI) + //case 15: _digitOut[i+1] = (h0>17 || (h0>5 && h0<12))?1:0; i++; //BB + case 14: _digitOut[i] = (h0>11)?1:0; break; //B + } + } else + { + switch(dP[i]) + { + case 71: _digitOut[i] = h/10; _digitOut[i+1] = h- _digitOut[i]*10; if(_digitOut[i] == 0) _digitOut[i]=10; i++; break; //hh + case 75: _digitOut[i] = m/10; _digitOut[i+1] = m- _digitOut[i]*10; if(_digitOut[i] == 0) _digitOut[i]=10; i++; break; //mm + case 81: _digitOut[i] = s/10; _digitOut[i+1] = s- _digitOut[i]*10; if(_digitOut[i] == 0) _digitOut[i]=10; i++; break; //ss + //case 66: _digitOut[i+2] = ((h0/3)&1)?1:10; i++; //bbb (bbbb NI) + //case 65: _digitOut[i+1] = (h0>17 || (h0>5 && h0<12))?1:10; i++; //bb + case 64: _digitOut[i] = (h0>11)?1:10; break; //b + + case 93: _digitOut[i] = weekday(localTime); _digitOut[i]--; if (_digitOut[i]<1) _digitOut[i]= 7; break; //d + case 94: _digitOut[i] = d/10; _digitOut[i+1] = d- _digitOut[i]*10; if(_digitOut[i] == 0) _digitOut[i]=10; i++; break; //dd + case 90: _digitOut[i] = mi/10; _digitOut[i+1] = mi- _digitOut[i]*10; if(_digitOut[i] == 0) _digitOut[i]=10; i++; break; //ii + case 87: _digitOut[i] = y/10; _digitOut[i+1] = y- _digitOut[i]*10; i++; break; //yy + case 89: _digitOut[i] = 2; _digitOut[i+1] = 0; _digitOut[i+2] = y/10; _digitOut[i+3] = y- _digitOut[i+2]*10; i+=3; break; //yyyy + } + } + } + } + } + + void handleOverlayDraw() + { + byte offsets[] = {5, 0, 6, 1, 7, 2, 8, 3, 9, 4}; + + for (uint16_t i = 0; i < 6; i++) + { + byte o = 10*i; + byte excl = 10; + if(_digitOut[i] < 10) excl = offsets[_digitOut[i]]; + excl += o; + + if (backlight && _digitOut[i] <11) + { + uint32_t col = strip.gamma32(strip.getSegment(0).colors[1]); + for (uint16_t j=o; j< o+10; j++) { + if (j != excl) strip.setPixelColor(j, col); + } + } else + { + for (uint16_t j=o; j< o+10; j++) { + if (j != excl) strip.setPixelColor(j, 0); + } + } + } + } + + void addToJsonState(JsonObject& root) + { + root["nx"] = cronixieDisplay; + } + + void readFromJsonState(JsonObject& root) + { + if (root["nx"].is()) { + strncpy(cronixieDisplay, root["nx"], 6); + } + } + + void addToConfig(JsonObject& root) + { + JsonObject top = root.createNestedObject(F("Cronixie")); + top["backlight"] = backlight; + } + + bool readFromConfig(JsonObject& root) + { + // default settings values could be set here (or below using the 3-argument getJsonValue()) instead of in the class definition or constructor + // setting them inside readFromConfig() is slightly more robust, handling the rare but plausible use case of single value being missing after boot (e.g. if the cfg.json was manually edited and a value was removed) + + JsonObject top = root[F("Cronixie")]; + + bool configComplete = !top.isNull(); + + configComplete &= getJsonValue(top["backlight"], backlight); + + return configComplete; + } + + uint16_t getId() + { + return USERMOD_ID_CRONIXIE; + } +}; \ No newline at end of file diff --git a/usermods/EleksTube_IPS/TFTs.h b/usermods/EleksTube_IPS/TFTs.h index 675129dce..4c6bd9cb8 100644 --- a/usermods/EleksTube_IPS/TFTs.h +++ b/usermods/EleksTube_IPS/TFTs.h @@ -12,6 +12,7 @@ class TFTs : public TFT_eSPI { private: uint8_t digits[NUM_DIGITS]; + // These read 16- and 32-bit types from the SD card file. // BMP data is stored little-endian, Arduino is little-endian too. // May need to reverse subscript order if porting elsewhere. @@ -33,7 +34,16 @@ private: } uint16_t output_buffer[TFT_HEIGHT][TFT_WIDTH]; - + int16_t w = 135, h = 240, x = 0, y = 0, bufferedDigit = 255; + uint16_t digitR, digitG, digitB, dimming = 255; + uint32_t digitColor = 0; + + void drawBuffer() { + bool oldSwapBytes = getSwapBytes(); + setSwapBytes(true); + pushImage(x, y, w, h, (uint16_t *)output_buffer); + setSwapBytes(oldSwapBytes); + } // These BMP functions are stolen directly from the TFT_SPIFFS_BMP example in the TFT_eSPI library. // Unfortunately, they aren't part of the library itself, so I had to copy them. @@ -41,44 +51,69 @@ private: //// BEGIN STOLEN CODE - // Draw directly from file stored in RGB565 format + // Draw directly from file stored in RGB565 format. Fastest bool drawBin(const char *filename) { fs::File bmpFS; - // Open requested file on SD card bmpFS = WLED_FS.open(filename, "r"); - if (!bmpFS) - { - Serial.print(F("File not found: ")); - Serial.println(filename); - return(false); - } - size_t sz = bmpFS.size(); - if (sz <= 64800) - { - bool oldSwapBytes = getSwapBytes(); - setSwapBytes(true); - - int16_t h = sz / (135 * 2); - - //draw img that is shorter than 240pix into the center - int16_t y = (height() - h) /2; - - bmpFS.read((uint8_t *) output_buffer,sz); - - if (!realtimeMode || realtimeOverride) strip.service(); - - pushImage(0, y, 135, h, (uint16_t *)output_buffer); - - setSwapBytes(oldSwapBytes); + if (sz > 64800) { + bmpFS.close(); + return false; } + uint16_t r, g, b, dimming = 255; + int16_t row, col; + + //draw img that is shorter than 240pix into the center + w = 135; + h = sz / (w * 2); + x = 0; + y = (height() - h) /2; + + uint8_t lineBuffer[w * 2]; + + if (!realtimeMode || realtimeOverride) strip.service(); + + // 0,0 coordinates are top left + for (row = 0; row < h; row++) { + + bmpFS.read(lineBuffer, sizeof(lineBuffer)); + uint8_t PixM, PixL; + + // Colors are already in 16-bit R5, G6, B5 format + for (col = 0; col < w; col++) + { + if (dimming == 255 && !digitColor) { // not needed, copy directly + output_buffer[row][col] = (lineBuffer[col*2+1] << 8) | (lineBuffer[col*2]); + } else { + // 16 BPP pixel format: R5, G6, B5 ; bin: RRRR RGGG GGGB BBBB + PixM = lineBuffer[col*2+1]; + PixL = lineBuffer[col*2]; + // align to 8-bit value (MSB left aligned) + r = (PixM) & 0xF8; + g = ((PixM << 5) | (PixL >> 3)) & 0xFC; + b = (PixL << 3) & 0xF8; + r *= dimming; g *= dimming; b *= dimming; + r = r >> 8; g = g >> 8; b = b >> 8; + if (digitColor) { // grayscale pixel coloring + uint8_t l = (r > g) ? ((r > b) ? r:b) : ((g > b) ? g:b); + r = g = b = l; + r *= digitR; g *= digitG; b *= digitB; + r = r >> 8; g = g >> 8; b = b >> 8; + } + output_buffer[row][col] = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3); + } + } + } + + drawBuffer(); + bmpFS.close(); - return(true); + return true; } bool drawBmp(const char *filename) { @@ -87,53 +122,52 @@ private: // Open requested file on SD card bmpFS = WLED_FS.open(filename, "r"); - if (!bmpFS) - { - Serial.print(F("File not found: ")); - Serial.println(filename); - return(false); - } - - uint32_t seekOffset; - int16_t w, h, row; - uint8_t r, g, b; + uint32_t seekOffset, headerSize, paletteSize = 0; + int16_t row; + uint16_t r, g, b, dimming = 255, bitDepth; uint16_t magic = read16(bmpFS); - if (magic == 0xFFFF) { + if (magic != ('B' | ('M' << 8))) { // File not found or not a BMP Serial.println(F("BMP not found!")); bmpFS.close(); - return(false); - } - - if (magic != 0x4D42) { - Serial.print(F("File not a BMP. Magic: ")); - Serial.println(magic); - bmpFS.close(); - return(false); + return false; } - read32(bmpFS); - read32(bmpFS); - seekOffset = read32(bmpFS); - read32(bmpFS); - w = read32(bmpFS); - h = read32(bmpFS); + read32(bmpFS); // filesize in bytes + read32(bmpFS); // reserved + seekOffset = read32(bmpFS); // start of bitmap + headerSize = read32(bmpFS); // header size + w = read32(bmpFS); // width + h = read32(bmpFS); // height + read16(bmpFS); // color planes (must be 1) + bitDepth = read16(bmpFS); - if ((read16(bmpFS) != 1) || (read16(bmpFS) != 24) || (read32(bmpFS) != 0)) { + if (read32(bmpFS) != 0 || (bitDepth != 24 && bitDepth != 1 && bitDepth != 4 && bitDepth != 8)) { Serial.println(F("BMP format not recognized.")); bmpFS.close(); - return(false); + return false; } - //draw img that is shorter than 240pix into the center - int16_t y = (height() - h) /2; + uint32_t palette[256]; + if (bitDepth <= 8) // 1,4,8 bit bitmap: read color palette + { + read32(bmpFS); read32(bmpFS); read32(bmpFS); // size, w resolution, h resolution + paletteSize = read32(bmpFS); + if (paletteSize == 0) paletteSize = bitDepth * bitDepth; //if 0, size is 2^bitDepth + bmpFS.seek(14 + headerSize); // start of color palette + for (uint16_t i = 0; i < paletteSize; i++) { + palette[i] = read32(bmpFS); + } + } + + // draw img that is shorter than 240pix into the center + x = (width() - w) /2; + y = (height() - h) /2; - bool oldSwapBytes = getSwapBytes(); - setSwapBytes(true); bmpFS.seek(seekOffset); - uint16_t padding = (4 - ((w * 3) & 3)) & 3; - uint8_t lineBuffer[w * 3 + padding]; + uint32_t lineSize = ((bitDepth * w +31) >> 5) * 4; + uint8_t lineBuffer[lineSize]; uint8_t serviceStrip = (!realtimeMode || realtimeOverride) ? 7 : 0; // row is decremented as the BMP image is drawn bottom up @@ -142,23 +176,121 @@ private: bmpFS.read(lineBuffer, sizeof(lineBuffer)); uint8_t* bptr = lineBuffer; - // Convert 24 to 16 bit colours while copying to output buffer. + // Convert 24 to 16 bit colors while copying to output buffer. for (uint16_t col = 0; col < w; col++) { - b = *bptr++; - g = *bptr++; - r = *bptr++; - output_buffer[row][col] = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3); + if (bitDepth == 24) { + b = *bptr++; + g = *bptr++; + r = *bptr++; + } else { + uint32_t c = 0; + if (bitDepth == 8) { + c = palette[*bptr++]; + } + else if (bitDepth == 4) { + c = palette[(*bptr >> ((col & 0x01)?0:4)) & 0x0F]; + if (col & 0x01) bptr++; + } + else { // bitDepth == 1 + c = palette[(*bptr >> (7 - (col & 0x07))) & 0x01]; + if ((col & 0x07) == 0x07) bptr++; + } + b = c; g = c >> 8; r = c >> 16; + } + if (dimming != 255) { // only dimm when needed + r *= dimming; g *= dimming; b *= dimming; + r = r >> 8; g = g >> 8; b = b >> 8; + } + if (digitColor) { // grayscale pixel coloring + uint8_t l = (r > g) ? ((r > b) ? r:b) : ((g > b) ? g:b); + r = g = b = l; + + r *= digitR; g *= digitG; b *= digitB; + r = r >> 8; g = g >> 8; b = b >> 8; + } + output_buffer[row][col] = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | ((b & 0xFF) >> 3); } } - pushImage(0, y, w, h, (uint16_t *)output_buffer); - setSwapBytes(oldSwapBytes); + drawBuffer(); bmpFS.close(); - return(true); + return true; } + bool drawClk(const char *filename) { + fs::File bmpFS; + + // Open requested file on SD card + bmpFS = WLED_FS.open(filename, "r"); + + if (!bmpFS) + { + Serial.print("File not found: "); + Serial.println(filename); + return false; + } + + uint16_t r, g, b, dimming = 255, magic; + int16_t row, col; + + magic = read16(bmpFS); + if (magic != 0x4B43) { // look for "CK" header + Serial.print(F("File not a CLK. Magic: ")); + Serial.println(magic); + bmpFS.close(); + return false; + } + + w = read16(bmpFS); + h = read16(bmpFS); + x = (width() - w) / 2; + y = (height() - h) / 2; + + uint8_t lineBuffer[w * 2]; + + if (!realtimeMode || realtimeOverride) strip.service(); + + // 0,0 coordinates are top left + for (row = 0; row < h; row++) { + + bmpFS.read(lineBuffer, sizeof(lineBuffer)); + uint8_t PixM, PixL; + + // Colors are already in 16-bit R5, G6, B5 format + for (col = 0; col < w; col++) + { + if (dimming == 255 && !digitColor) { // not needed, copy directly + output_buffer[row][col+x] = (lineBuffer[col*2+1] << 8) | (lineBuffer[col*2]); + } else { + // 16 BPP pixel format: R5, G6, B5 ; bin: RRRR RGGG GGGB BBBB + PixM = lineBuffer[col*2+1]; + PixL = lineBuffer[col*2]; + // align to 8-bit value (MSB left aligned) + r = (PixM) & 0xF8; + g = ((PixM << 5) | (PixL >> 3)) & 0xFC; + b = (PixL << 3) & 0xF8; + r *= dimming; g *= dimming; b *= dimming; + r = r >> 8; g = g >> 8; b = b >> 8; + if (digitColor) { // grayscale pixel coloring + uint8_t l = (r > g) ? ((r > b) ? r:b) : ((g > b) ? g:b); + r = g = b = l; + r *= digitR; g *= digitG; b *= digitB; + r = r >> 8; g = g >> 8; b = b >> 8; + } + output_buffer[row][col+x] = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3); + } + } + } + + drawBuffer(); + + bmpFS.close(); + return true; + } + + public: TFTs() : TFT_eSPI(), chip_select() { for (uint8_t digit=0; digit < NUM_DIGITS; digit++) digits[digit] = 0; } @@ -167,6 +299,9 @@ public: enum show_t { no, yes, force }; // A digit of 0xFF means blank the screen. const static uint8_t blanked = 255; + + uint8_t tubeSegment = 1; + uint8_t digitOffset = 0; void begin() { pinMode(TFT_ENABLE_PIN, OUTPUT); @@ -182,34 +317,60 @@ public: void showDigit(uint8_t digit) { chip_select.setDigit(digit); + uint8_t digitToDraw = digits[digit]; + if (digitToDraw < 10) digitToDraw += digitOffset; - if (digits[digit] == blanked) { - fillScreen(TFT_BLACK); + if (digitToDraw == blanked) { + fillScreen(TFT_BLACK); return; } - else { - // Filenames are no bigger than "255.bmp\0" - char file_name[10]; - sprintf(file_name, "/%d.bmp", digits[digit]); - if (WLED_FS.exists(file_name)) { - drawBmp(file_name); - } else { - sprintf(file_name, "/%d.bin", digits[digit]); - drawBin(file_name); - } + + // if last digit was the same, skip loading from FS to buffer + if (!digitColor && digitToDraw == bufferedDigit) drawBuffer(); + digitR = R(digitColor); digitG = G(digitColor); digitB = B(digitColor); + + // Filenames are no bigger than "254.bmp\0" + char file_name[10]; + // Fastest, raw RGB565 + sprintf(file_name, "/%d.bin", digitToDraw); + if (WLED_FS.exists(file_name)) { + if (drawBin(file_name)) bufferedDigit = digitToDraw; + return; } - } + // Fast, raw RGB565, see https://github.com/aly-fly/EleksTubeHAX on how to create this clk format + sprintf(file_name, "/%d.clk", digitToDraw); + if (WLED_FS.exists(file_name)) { + if (drawClk(file_name)) bufferedDigit = digitToDraw; + return; + } + // Slow, regular RGB888 or 1,4,8 bit palette BMP + sprintf(file_name, "/%d.bmp", digitToDraw); + if (drawBmp(file_name)) bufferedDigit = digitToDraw; + return; + } void setDigit(uint8_t digit, uint8_t value, show_t show=yes) { uint8_t old_value = digits[digit]; digits[digit] = value; - + + // Color in grayscale bitmaps if Segment 1 exists + // TODO If secondary and tertiary are black, color all in primary, + // else color first three from Seg 1 color slots and last three from Seg 2 color slots + WS2812FX::Segment& seg1 = strip.getSegment(tubeSegment); + if (seg1.isActive()) { + digitColor = strip.getPixelColor(seg1.start + digit); + dimming = seg1.opacity; + } else { + digitColor = 0; + dimming = 255; + } + if (show != no && (old_value != value || show == force)) { showDigit(digit); } } - uint8_t getDigit(uint8_t digit) { return digits[digit]; } + uint8_t getDigit(uint8_t digit) {return digits[digit];} - void showAllDigits() { for (uint8_t digit=0; digit < NUM_DIGITS; digit++) showDigit(digit); } + void showAllDigits() {for (uint8_t digit=0; digit < NUM_DIGITS; digit++) showDigit(digit);} // Making chip_select public so we don't have to proxy all methods, and the caller can just use it directly. ChipSelect chip_select; diff --git a/usermods/EleksTube_IPS/readme.md b/usermods/EleksTube_IPS/readme.md index a08d69d2e..87827ac4a 100644 --- a/usermods/EleksTube_IPS/readme.md +++ b/usermods/EleksTube_IPS/readme.md @@ -5,16 +5,17 @@ It enables running all WLED effects on the background SK6812 lighting, while dis Code is largely based on https://github.com/SmittyHalibut/EleksTubeHAX by Mark Smith! Supported: -- Display with custom bitmaps or raw RGB565 images (.bin) from filesystem +- Display with custom bitmaps (.bmp) or raw RGB565 images (.bin) from filesystem - Background lighting -- Power button +- All 4 hardware buttons - RTC (with RTC usermod) - Standard WLED time features (NTP, DST, timezones) Not supported: -- 3 navigation buttons, on-device setup +- On-device setup with buttons (WiFi setup only) -Your images must be exactly 135 pixels wide and 1-240 pixels high. +Your images must be 1-135 pixels wide and 1-240 pixels high. +For BMP, 1, 4, 8, and 24 bits per pixel formats are supported. ## Installation @@ -25,7 +26,20 @@ Use LED pin 12, relay pin 27 and button pin 34. ## Use of RGB565 images -Binary 16-bit per pixel RGB565 format `.bin` images are now supported. This has the benefit of only using 2/3rds of the file size a `.bmp` has. +Binary 16-bit per pixel RGB565 format `.bin` and `.clk` images are now supported. This has the benefit of only using 2/3rds of the file size a 24 BPP `.bmp` has. The drawback is that this format cannot be handled by common image programs and that an extra conversion step is needed. -You can use https://lvgl.io/tools/imageconverter to convert your .bmp to a .bin file (settings `True color` and `Binary RGB565`) -Thank you to @RedNax67 for adding .bin support. \ No newline at end of file +You can use https://lvgl.io/tools/imageconverter to convert your .bmp to a .bin file (settings `True color` and `Binary RGB565`). +Thank you to @RedNax67 for adding .bin and .clk support. +For most clockface designs, using 4 or 8 BPP BMP formats will save even more file size: + +| Bits per pixel | File size in kB (for 135x240 img) | % of 24 BPP BMP | Max unique colors +| --- | --- | --- | --- | +24 | 98 | 100% | 16M (66K) +16 (.clk) | 64.8 | 66% | 66K +8 | 33.7 | 34% | 256 +4 | 16.4 | 17% | 16 +1 | 4.9 | 5% | 2 + +Comparison 1 vs. 4 vs. 8 vs. 24 BPP. With this clockface on the actual clock, 4 bit looks good, and 8 bit is almost indistinguishable from 24 bit. + +![comparison](https://user-images.githubusercontent.com/21045690/156899667-5b55ed9f-6e03-4066-b2aa-1260e9570369.png) \ No newline at end of file diff --git a/usermods/EleksTube_IPS/usermod_elekstube_ips.h b/usermods/EleksTube_IPS/usermod_elekstube_ips.h index f2ce8eb08..06c6ecc89 100644 --- a/usermods/EleksTube_IPS/usermod_elekstube_ips.h +++ b/usermods/EleksTube_IPS/usermod_elekstube_ips.h @@ -6,6 +6,13 @@ class ElekstubeIPSUsermod : public Usermod { private: + // strings to reduce flash memory usage (used more than twice) + static const char _name[]; + static const char _tubeSeg[]; + static const char _digitOffset[]; + + char cronixieDisplay[7] = "HHMMSS"; + TFTs tfts; void updateClockDisplay(TFTs::show_t show=TFTs::yes) { bool set[6] = {false}; @@ -21,6 +28,8 @@ class ElekstubeIPSUsermod : public Usermod { set[i] = false; //display HHMMSS time } } + + uint8_t hr = hour(localTime); uint8_t hrTens = hr/10; uint8_t mi = minute(localTime); @@ -37,6 +46,10 @@ class ElekstubeIPSUsermod : public Usermod { unsigned long lastTime = 0; public: + uint8_t lastBri; + uint32_t lastCols[6]; + TFTs::show_t fshow=TFTs::yes; + void setup() { tfts.begin(); tfts.fillScreen(TFT_BLACK); @@ -47,14 +60,99 @@ class ElekstubeIPSUsermod : public Usermod { } void loop() { - if (toki.isTick()) { - updateLocalTime(); - updateClockDisplay(); + if (!toki.isTick()) return; + updateLocalTime(); + + WS2812FX::Segment& seg1 = strip.getSegment(tfts.tubeSegment); + if (seg1.isActive()) { + bool update = false; + if (seg1.opacity != lastBri) update = true; + lastBri = seg1.opacity; + for (uint8_t i = 0; i < 6; i++) { + uint32_t c = strip.getPixelColor(seg1.start + i); + if (c != lastCols[i]) update = true; + lastCols[i] = c; + } + if (update) fshow=TFTs::force; + } else if (lastCols[0] != 0) { // Segment 1 deleted + fshow=TFTs::force; + lastCols[0] = 0; } + + updateClockDisplay(fshow); + fshow=TFTs::yes; + } + + /** + * addToConfig() (called from set.cpp) stores persistent properties to cfg.json + */ + void addToConfig(JsonObject &root) { + // we add JSON object: {"EleksTubeIPS": {"tubeSegment": 1, "digitOffset": 0}} + JsonObject top = root.createNestedObject(FPSTR(_name)); // usermodname + top[FPSTR(_tubeSeg)] = tfts.tubeSegment; + top[FPSTR(_digitOffset)] = tfts.digitOffset; + DEBUG_PRINTLN(F("EleksTube config saved.")); + } + + /** + * readFromConfig() is called before setup() to populate properties from values stored in cfg.json + * + * The function should return true if configuration was successfully loaded or false if there was no configuration. + */ + bool readFromConfig(JsonObject &root) { + // we look for JSON object: {"EleksTubeIPS": {"tubeSegment": 1, "digitOffset": 0}} + DEBUG_PRINT(FPSTR(_name)); + + JsonObject top = root[FPSTR(_name)]; + if (top.isNull()) { + DEBUG_PRINTLN(F(": No config found. (Using defaults.)")); + return false; + } + + tfts.tubeSegment = top[FPSTR(_tubeSeg)] | tfts.tubeSegment; + uint8_t digitOffsetPrev = tfts.digitOffset; + tfts.digitOffset = top[FPSTR(_digitOffset)] | tfts.digitOffset; + if (tfts.digitOffset > 240) tfts.digitOffset = 240; + if (tfts.digitOffset != digitOffsetPrev) fshow=TFTs::force; + + // use "return !top["newestParameter"].isNull();" when updating Usermod with new features + return !top[FPSTR(_digitOffset)].isNull(); + } + + /* + * addToJsonState() can be used to add custom entries to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void addToJsonState(JsonObject& root) + { + root["nx"] = cronixieDisplay; + root[FPSTR(_digitOffset)] = tfts.digitOffset; + } + + + /* + * readFromJsonState() can be used to receive data clients send to the /json/state part of the JSON API (state object). + * Values in the state object may be modified by connected clients + */ + void readFromJsonState(JsonObject& root) + { + if (root["nx"].is()) { + strncpy(cronixieDisplay, root["nx"], 6); + } + + uint8_t digitOffsetPrev = tfts.digitOffset; + tfts.digitOffset = root[FPSTR(_digitOffset)] | tfts.digitOffset; + if (tfts.digitOffset > 240) tfts.digitOffset = 240; + if (tfts.digitOffset != digitOffsetPrev) fshow=TFTs::force; } uint16_t getId() { return USERMOD_ID_ELEKSTUBE_IPS; } -}; \ No newline at end of file +}; + +// strings to reduce flash memory usage (used more than twice) +const char ElekstubeIPSUsermod::_name[] PROGMEM = "EleksTubeIPS"; +const char ElekstubeIPSUsermod::_tubeSeg[] PROGMEM = "tubeSegment"; +const char ElekstubeIPSUsermod::_digitOffset[] PROGMEM = "digitOffset"; diff --git a/wled00/FX.cpp b/wled00/FX.cpp index d4fd5444c..54f3669e5 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -2784,16 +2784,9 @@ uint16_t WS2812FX::mode_popcorn(void) { if (numPopcorn == 0) numPopcorn = 1; for(uint8_t i = 0; i < numPopcorn; i++) { - bool isActive = popcorn[i].pos >= 0.0f; - - if (isActive) { // if kernel is active, update its position + if (popcorn[i].pos >= 0.0f) { // if kernel is active, update its position popcorn[i].pos += popcorn[i].vel; popcorn[i].vel += gravity; - uint32_t col = color_wheel(popcorn[i].colIndex); - if (!SEGMENT.palette && popcorn[i].colIndex < NUM_COLORS) col = SEGCOLOR(popcorn[i].colIndex); - - uint16_t ledIndex = popcorn[i].pos; - if (ledIndex < SEGLEN) setPixelColor(ledIndex, col); } else { // if kernel is inactive, randomly pop it if (random8() < 2) { // POP!!! popcorn[i].pos = 0.01f; @@ -2812,6 +2805,13 @@ uint16_t WS2812FX::mode_popcorn(void) { } } } + if (popcorn[i].pos >= 0.0f) { // draw now active popcorn (either active before or just popped) + uint32_t col = color_wheel(popcorn[i].colIndex); + if (!SEGMENT.palette && popcorn[i].colIndex < NUM_COLORS) col = SEGCOLOR(popcorn[i].colIndex); + + uint16_t ledIndex = popcorn[i].pos; + if (ledIndex < SEGLEN) setPixelColor(ledIndex, col); + } } return FRAMETIME; diff --git a/wled00/FX.h b/wled00/FX.h index dbcef15ce..3984b9cf7 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -247,7 +247,7 @@ class WS2812FX { // segment parameters public: - typedef struct Segment { // 30 (32 in memory) bytes + typedef struct Segment { // 31 (32 in memory) bytes uint16_t start; uint16_t stop; //segment invalid if stop == 0 uint16_t offset; @@ -260,6 +260,7 @@ class WS2812FX { uint8_t opacity; uint32_t colors[NUM_COLORS]; uint8_t cct; //0==1900K, 255==10091K + uint8_t _capabilities; char *name; bool setColor(uint8_t slot, uint32_t c, uint8_t segn) { //returns true if changed if (slot >= NUM_COLORS || segn >= MAX_NUM_SEGMENTS) return false; @@ -335,7 +336,8 @@ class WS2812FX { return vLength; } uint8_t differs(Segment& b); - uint8_t getLightCapabilities(); + inline uint8_t getLightCapabilities() {return _capabilities;} + void refreshLightCapabilities(); } segment; // segment runtime parameters @@ -887,14 +889,15 @@ class WS2812FX { uint32_t _colors_t[3]; uint8_t _bri_t; + bool _no_rgb = false; uint8_t _segment_index = 0; uint8_t _segment_index_palette_last = 99; uint8_t _mainSegment; segment _segments[MAX_NUM_SEGMENTS] = { // SRAM footprint: 24 bytes per element - // start, stop, offset, speed, intensity, palette, mode, options, grouping, spacing, opacity (unused), color[] - {0, 7, 0, DEFAULT_SPEED, 128, 0, DEFAULT_MODE, NO_OPTIONS, 1, 0, 255, {DEFAULT_COLOR}} + // start, stop, offset, speed, intensity, palette, mode, options, grouping, spacing, opacity (unused), color[], capabilities + {0, 7, 0, DEFAULT_SPEED, 128, 0, DEFAULT_MODE, NO_OPTIONS, 1, 0, 255, {DEFAULT_COLOR}, 0} }; segment_runtime _segment_runtimes[MAX_NUM_SEGMENTS]; // SRAM footprint: 28 bytes per element friend class Segment_runtime; diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 48120c8c5..67e92c559 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -152,7 +152,14 @@ void WS2812FX::service() { _colors_t[slot] = transitions[t].currentColor(SEGMENT.colors[slot]); } if (!cctFromRgb || correctWB) busses.setSegmentCCT(_cct_t, correctWB); - for (uint8_t c = 0; c < 3; c++) _colors_t[c] = gamma32(_colors_t[c]); + _no_rgb = !(SEGMENT.getLightCapabilities() & 0x01); + for (uint8_t c = 0; c < NUM_COLORS; c++) { + // if segment is not RGB capable, treat RGB channels of main segment colors as if 0 + // this prevents Dual mode with white value 0 from setting White channel from inaccessible RGB values + // If not RGB capable, also treat palette as if default (0), as palettes set white channel to 0 + if (_no_rgb) _colors_t[c] = _colors_t[c] & 0xFF000000; + _colors_t[c] = gamma32(_colors_t[c]); + } handle_palette(); delay = (this->*_mode[SEGMENT.mode])(); //effect function if (SEGMENT.mode != FX_MODE_HALLOWEEN_EYES) SEGENV.call++; @@ -561,8 +568,10 @@ uint8_t WS2812FX::Segment::differs(Segment& b) { return d; } -uint8_t WS2812FX::Segment::getLightCapabilities() { - if (!isActive()) return 0; +void WS2812FX::Segment::refreshLightCapabilities() { + if (!isActive()) { + _capabilities = 0; return; + } uint8_t capabilities = 0; uint8_t awm = Bus::getAutoWhiteMode(); bool whiteSlider = (awm == RGBW_MODE_DUAL || awm == RGBW_MODE_MANUAL_ONLY); @@ -589,7 +598,7 @@ uint8_t WS2812FX::Segment::getLightCapabilities() { } if (correctWB && type != TYPE_ANALOG_1CH) capabilities |= 0x04; //white balance correction (uses CCT slider) } - return capabilities; + _capabilities = capabilities; } //used for JSON API info.leds.rgbw. Little practical use, deprecate with info.leds.rgbw. @@ -628,7 +637,8 @@ void WS2812FX::setSegment(uint8_t n, uint16_t i1, uint16_t i2, uint8_t grouping, Segment& seg = _segments[n]; //return if neither bounds nor grouping have changed - if (seg.start == i1 && seg.stop == i2 + bool boundsUnchanged = (seg.start == i1 && seg.stop == i2); + if (boundsUnchanged && (!grouping || (seg.grouping == grouping && seg.spacing == spacing)) && (offset == UINT16_MAX || offset == seg.offset)) return; @@ -653,6 +663,7 @@ void WS2812FX::setSegment(uint8_t n, uint16_t i1, uint16_t i2, uint8_t grouping, } if (offset < UINT16_MAX) seg.offset = offset; _segment_runtimes[n].markForReset(); + if (!boundsUnchanged) seg.refreshLightCapabilities(); } void WS2812FX::restartRuntime() { @@ -748,6 +759,8 @@ void WS2812FX::fixInvalidSegments() { { if (_segments[i].start >= _length) setSegment(i, 0, 0); if (_segments[i].stop > _length) setSegment(i, _segments[i].start, _length); + // this is always called as the last step after finalizeInit(), update covered bus types + getSegment(i).refreshLightCapabilities(); } } @@ -1123,22 +1136,17 @@ void WS2812FX::handle_palette(void) */ uint32_t IRAM_ATTR WS2812FX::color_from_palette(uint16_t i, bool mapping, bool wrap, uint8_t mcol, uint8_t pbri) { - if (SEGMENT.palette == 0 && mcol < 3) { + if ((SEGMENT.palette == 0 && mcol < 3) || _no_rgb) { uint32_t color = SEGCOLOR(mcol); - if (pbri != 255) { - CRGB crgb_color = col_to_crgb(color); - crgb_color.nscale8_video(pbri); - return crgb_to_col(crgb_color); - } else { - return color; - } + if (pbri == 255) return color; + return RGBW32(scale8_video(R(color),pbri), scale8_video(G(color),pbri), scale8_video(B(color),pbri), scale8_video(W(color),pbri)); } uint8_t paletteIndex = i; if (mapping && SEGLEN > 1) paletteIndex = (i*255)/(SEGLEN -1); if (!wrap) paletteIndex = scale8(paletteIndex, 240); //cut off blend at palette "end" CRGB fastled_col; - fastled_col = ColorFromPalette( currentPalette, paletteIndex, pbri, (paletteBlend == 3)? NOBLEND:LINEARBLEND); + fastled_col = ColorFromPalette(currentPalette, paletteIndex, pbri, (paletteBlend == 3)? NOBLEND:LINEARBLEND); return crgb_to_col(fastled_col); } diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index c2dbe91f1..6b1acc4e3 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -84,6 +84,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(strip.ablMilliampsMax, hw_led[F("maxpwr")]); CJSON(strip.milliampsPerLed, hw_led[F("ledma")]); Bus::setAutoWhiteMode(hw_led[F("rgbwm")] | Bus::getAutoWhiteMode()); + strip.fixInvalidSegments(); // refreshes segment light capabilities (in case auto white mode changed) CJSON(correctWB, hw_led["cct"]); CJSON(cctFromRgb, hw_led[F("cr")]); CJSON(strip.cctBlending, hw_led[F("cb")]); @@ -94,7 +95,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { if (fromFS || !ins.isNull()) { uint8_t s = 0; // bus iterator - busses.removeAll(); + if (fromFS) busses.removeAll(); // can't safely manipulate busses directly in network callback uint32_t mem = 0; for (JsonObject elm : ins) { if (s >= WLED_MAX_BUSSES) break; @@ -116,11 +117,17 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { uint8_t ledType = elm["type"] | TYPE_WS2812_RGB; bool reversed = elm["rev"]; bool refresh = elm["ref"] | false; - ledType |= refresh << 7; // hack bit 7 to indicate strip requires off refresh + ledType |= refresh << 7; // hack bit 7 to indicate strip requires off refresh s++; - BusConfig bc = BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst); - mem += BusManager::memUsage(bc); - if (mem <= MAX_LED_MEMORY && busses.getNumBusses() <= WLED_MAX_BUSSES) busses.add(bc); // finalization will be done in WLED::beginStrip() + if (fromFS) { + BusConfig bc = BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst); + mem += BusManager::memUsage(bc); + if (mem <= MAX_LED_MEMORY && busses.getNumBusses() <= WLED_MAX_BUSSES) busses.add(bc); // finalization will be done in WLED::beginStrip() + } else { + if (busConfigs[s] != nullptr) delete busConfigs[s]; + busConfigs[s] = new BusConfig(ledType, pins, start, length, colorOrder, reversed, skipFirst); + doInitBusses = true; + } } // finalization done in beginStrip() } @@ -359,10 +366,8 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { CJSON(latitude, if_ntp[F("lt")]); JsonObject ol = doc[F("ol")]; - prev = overlayDefault; - CJSON(overlayDefault ,ol[F("clock")]); // 0 + CJSON(overlayCurrent ,ol[F("clock")]); // 0 CJSON(countdownMode, ol[F("cntdwn")]); - if (prev != overlayDefault) overlayCurrent = overlayDefault; CJSON(overlayMin, ol["min"]); CJSON(overlayMax, ol[F("max")]); @@ -761,7 +766,7 @@ void serializeConfig() { if_ntp[F("lt")] = latitude; JsonObject ol = doc.createNestedObject("ol"); - ol[F("clock")] = overlayDefault; + ol[F("clock")] = overlayCurrent; ol[F("cntdwn")] = countdownMode; ol["min"] = overlayMin; diff --git a/wled00/const.h b/wled00/const.h index 0698f7337..692038bc2 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -71,6 +71,7 @@ #define USERMOD_RGB_ROTARY_ENCODER 22 //Usermod "rgb-rotary-encoder.h" #define USERMOD_ID_QUINLED_AN_PENTA 23 //Usermod "quinled-an-penta.h" #define USERMOD_ID_SSDR 24 //Usermod "usermod_v2_seven_segment_display_reloaded.h" +#define USERMOD_ID_CRONIXIE 25 //Usermod "usermod_cronixie.h" //Access point behavior #define AP_BEHAVIOR_BOOT_NO_CONN 0 //Open AP when no connection after boot diff --git a/wled00/data/index.css b/wled00/data/index.css index 2a0412d2b..87a42d89d 100644 --- a/wled00/data/index.css +++ b/wled00/data/index.css @@ -430,7 +430,7 @@ button { } #ndlt { - margin: 12px 0; + margin: 12px 0; } #roverstar { @@ -692,7 +692,10 @@ input[type=range]::-moz-range-thumb { } #qcs-w { margin-top: 10px; + display: none; } + +/* Quick color select buttons */ .qcs { margin: 2px; border-radius: 14px; @@ -701,12 +704,16 @@ input[type=range]::-moz-range-thumb { height: 28px; line-height: 28px; } + +/* Quick color select Black button (has white border) */ .qcsb { width: 26px; height: 26px; line-height: 26px; border: 1px solid var(--c-f); } + +/* Hex color input wrapper div */ #hexw { margin-top: 5px; display: none; @@ -728,7 +735,7 @@ select/*, .sel-p, .sel-pl, .sel-ple*/ { transform:translate3d(0,0,0); } #tt { - text-align: center; + text-align: center; } .cl { background-color: #000; @@ -829,6 +836,12 @@ input[type=number]::-webkit-outer-spin-button { .segpwr { padding: 4px 0 0 8px; } +.pname { + top: 1px; +} +.plname { + top:0; +} .pid { position: absolute; @@ -887,8 +900,6 @@ input[type=number]::-webkit-outer-spin-button { position: absolute; bottom: 8px; right: 8px; - color: var(--c-f); - cursor: pointer; } .check, .radio { @@ -942,7 +953,7 @@ input[type=number]::-webkit-outer-spin-button { .radio:hover input ~ .radiomark, .check:hover input ~ .checkmark { - background-color: var(--c-4); + background-color: var(--c-5); } .checkmark:after, .radiomark:after { diff --git a/wled00/data/index.htm b/wled00/data/index.htm index c8b6b4186..0d82baec1 100644 --- a/wled00/data/index.htm +++ b/wled00/data/index.htm @@ -187,7 +187,6 @@ - @@ -326,7 +325,6 @@
For best performance, it is recommended to turn off the streaming source when not in use. -
diff --git a/wled00/data/index.js b/wled00/data/index.js index 8a93aa825..d0ad07ddb 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -1,9 +1,9 @@ //page js var loc = false, locip; var noNewSegs = false; -var isOn = false, nlA = false, isLv = false, isInfo = false, isNodes = false, syncSend = false, syncTglRecv = true, isRgbw = false, cct = false; +var isOn = false, nlA = false, isLv = false, isInfo = false, isNodes = false, syncSend = false, syncTglRecv = true; +var hasWhite = false, hasRGB = false, hasCCT = false; var whites = [0,0,0]; -var colors = [[0,0,0],[0,0,0],[0,0,0]]; var expanded = [false]; var powered = [true]; var nlDur = 60, nlTar = 0; @@ -44,7 +44,10 @@ function gEBCN(c) {return d.getElementsByClassName(c);} function isEmpty(o) {return Object.keys(o).length === 0;} function isObj(i) {return (i && typeof i === 'object' && !Array.isArray(i));} -// returns RGB color from a given slot s 0-2 from color array a +// returns true if dataset R, G & B values are 0 +function isRgbBlack(a) {return (parseInt(a.r) == 0 && parseInt(a.g) == 0 && parseInt(a.b) == 0);} + +// returns RGB color from a given dataset function rgbStr(a) {return "rgb(" + a.r + "," + a.g + "," + a.b + ")";} // brightness approximation for selecting white as text color if background bri < 127, and black if higher @@ -53,8 +56,18 @@ function rgbBri(a) {return 0.2126*parseInt(a.r) + 0.7152*parseInt(a.g) + 0.0722* // sets background of color slot selectors function setCSL(cs) { - cs.style.backgroundColor = rgbStr(cs.dataset); - cs.style.color = rgbBri(cs.dataset) > 127 ? "#000":"#fff"; // if text has no CSS "shadow" + let w = whites[parseInt(cs.id.substr(3))]; + if (hasRGB && !isRgbBlack(cs.dataset)) { + cs.style.backgroundColor = rgbStr(cs.dataset); + cs.style.color = rgbBri(cs.dataset) > 127 ? "#000":"#fff"; // if text has no CSS "shadow" + if (hasWhite && w > 0) { + cs.style.background = `linear-gradient(180deg, ${rgbStr(cs.dataset)} 30%, rgb(${w},${w},${w}))`; + } + } else { + if (!hasWhite) w = 0; + cd.style.background = `rgb(${w},${w},${w})`; + cd.style.color = w > 127 ? "#000":"#fff"; + } } function applyCfg() @@ -62,13 +75,17 @@ function applyCfg() cTheme(cfg.theme.base === "light"); var bg = cfg.theme.color.bg; if (bg) sCol('--c-1', bg); + if (lastinfo.leds) updateUI(); // update component visibility +/* var ccfg = cfg.comp.colors; gId('hexw').style.display = ccfg.hex ? "block":"none"; - gId('picker').style.display = ccfg.picker ? "block":"none"; - gId('vwrap').style.display = ccfg.picker ? "block":"none"; - //gId('kwrap').style.display = ccfg.picker ? "block":"none"; - gId('rgbwrap').style.display = ccfg.rgb ? "block":"none"; - gId('qcs-w').style.display = ccfg.quick ? "block":"none"; + gId('picker').style.display = (hasRGB && ccfg.picker) ? "block":"none"; + gId('vwrap').style.display = (hasRGB && ccfg.picker) ? "block":"none"; + gId('kwrap').style.display = (hasRGB && !hasCCT && ccfg.picker) ? "block":"none"; + gId('rgbwrap').style.display = (hasRGB && ccfg.rgb) ? "block":"none"; + gId('qcs-w').style.display = (hasRGB && ccfg.quick) ? "block":"none"; + gId('palw').style.display = hasRGB ? "block":"none"; +*/ var l = cfg.comp.labels; var e = d.querySelectorAll('.tab-label'); for (let i of e) i.style.display = l ? "block":"none"; @@ -583,12 +600,10 @@ function parseInfo() { if (li.live) name = "(Live) " + name; if (loc) name = "(L) " + name; d.title = name; - isRgbw = li.leds.wv; ledCount = li.leds.count; syncTglRecv = li.str; maxSeg = li.leds.maxseg; pmt = li.fs.pmt; - cct = li.leds.cct; } function populateInfo(i) @@ -1013,9 +1028,16 @@ function updateUI() updateTrail(gId('sliderC2')); updateTrail(gId('sliderC3')); - gId('wwrap').style.display = (isRgbw) ? "block":"none"; - gId("wbal").style.display = (cct) ? "block":"none"; - gId('kwrap').style.display = (cct) ? "none":"block"; + gId('wwrap').style.display = (hasWhite) ? "block":"none"; + gId('wbal').style.display = (hasCCT) ? "block":"none"; + var ccfg = cfg.comp.colors; + gId('hexw').style.display = ccfg.hex ? "block":"none"; + gId('picker').style.display = (hasRGB && ccfg.picker) ? "block":"none"; + gId('vwrap').style.display = (hasRGB && ccfg.picker) ? "block":"none"; + gId('kwrap').style.display = (hasRGB && !hasCCT && ccfg.picker) ? "block":"none"; + gId('rgbwrap').style.display = (hasRGB && ccfg.rgb) ? "block":"none"; + gId('qcs-w').style.display = (hasRGB && ccfg.quick) ? "block":"none"; + gId('palw').style.display = hasRGB ? "block":"none"; updatePA(); updatePSliders(); @@ -1129,13 +1151,33 @@ function readState(s,command=false) tr = s.transition; gId('tt').value = tr/10; - var selc=0; var ind=0; populateSegments(s); + var selc=0; + var sellvl=0; // 0: selc is invalid, 1: selc is mainseg, 2: selc is first selected + hasRGB = hasWhite = hasCCT = false; for (let i = 0; i < (s.seg||[]).length; i++) { - if(s.seg[i].sel) {selc = ind; break;} ind++; + if (sellvl == 0 && s.seg[i].id == s.mainseg) { + selc = i; + sellvl = 1; + } + if (s.seg[i].sel) { + if (sellvl < 2) selc = i; // get first selected segment + sellvl = 2; + var lc = lastinfo.leds.seglc[s.seg[i].id]; + hasRGB |= lc & 0x01; + hasWhite |= lc & 0x02; + hasCCT |= lc & 0x04; + } + //if(s.seg[i].sel) {selc = ind; break;} ind++; } var i=s.seg[selc]; + if (sellvl == 1) { + var lc = lastinfo.leds.seglc[i.id]; + hasRGB = lc & 0x01; + hasWhite = lc & 0x02; + hasCCT = lc & 0x04; + } if (!i) { showToast('No Segments!', true); updateUI(); @@ -1151,7 +1193,7 @@ function readState(s,command=false) if (isRgbw) { let w = cd[e].dataset.w = i.col[e][3]; whites[e] = parseInt(w); } } selectSlot(csel); - if (i.cct && i.cct>=0) gId("sliderA").value = i.cct; + if (i.cct != null && i.cct>=0) gId("sliderA").value = i.cct; gId('sliderSpeed').value = i.sx; gId('sliderIntensity').value = i.ix; @@ -1998,8 +2040,12 @@ function selectSlot(b) for (let i of cd) i.classList.remove('xxs-w'); cd[b].classList.add('xxs-w'); setPicker(cd[b].style.backgroundColor); + //force slider update on initial load (picker "color:change" not fired if black) + if (cpick.color.value == 0) updatePSliders(); gId('sliderW').value = whites[b]; - updatePSliders(); + updateTrail(gId('sliderW')); + redrawPalPrev(); + //updatePSliders(); } //set the color from a hex string. Used by quick color selectors diff --git a/wled00/data/settings_leds.htm b/wled00/data/settings_leds.htm index c9e88541b..63d0f8484 100644 --- a/wled00/data/settings_leds.htm +++ b/wled00/data/settings_leds.htm @@ -157,7 +157,7 @@ } gId("rf"+n).onclick = (t == 31) ? (function(){return false}) : (function(){}); // prevent change for TM1814 isRGBW |= (t == 30 || t == 31 || (t > 40 && t < 46 && t != 43)); // RGBW checkbox, TYPE_xxxx values from const.h - gId("co"+n).style.display = ((t>=80 && t<96) || t == 41 || t == 42) ? "none":"inline"; // hide color order for PWM W & WW/CW + gId("co"+n).style.display = ((t>=80 && t<96) || (t > 40 && t < 48)) ? "none":"inline"; // hide color order for PWM gId("dig"+n+"c").style.display = (t > 40 && t < 48) ? "none":"inline"; // hide count for analog gId("dig"+n+"r").style.display = (t>=80 && t<96) ? "none":"inline"; // hide reversed for virtual gId("dig"+n+"s").style.display = ((t>=80 && t<96) || (t > 40 && t < 48)) ? "none":"inline"; // hide skip 1st for virtual & analog @@ -180,7 +180,7 @@ var n = LCs[i].name.substring(2); // bus number // do we have a led count field if (nm=="LC") { - var c=parseInt(LCs[i].value,10); //get LED gount + var c=parseInt(LCs[i].value,10); //get LED count if (!customStarts || !startsDirty[n]) gId("ls"+n).value=sLC; //update start value gId("ls"+n).disabled = !customStarts; //enable/disable field editing if(c){ diff --git a/wled00/data/settings_time.htm b/wled00/data/settings_time.htm index 006efe7ab..9eddc3e5a 100644 --- a/wled00/data/settings_time.htm +++ b/wled00/data/settings_time.htm @@ -7,6 +7,7 @@ Time Settings