diff --git a/usermods/EleksTube_IPS/TFTs.h b/usermods/EleksTube_IPS/TFTs.h index c8e1ed3b..4c6bd9cb 100644 --- a/usermods/EleksTube_IPS/TFTs.h +++ b/usermods/EleksTube_IPS/TFTs.h @@ -35,6 +35,8 @@ 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(); @@ -75,10 +77,6 @@ private: if (!realtimeMode || realtimeOverride) strip.service(); - #ifdef ELEKSTUBE_DIMMING - dimming=bri; - #endif - // 0,0 coordinates are top left for (row = 0; row < h; row++) { @@ -88,7 +86,7 @@ private: // Colors are already in 16-bit R5, G6, B5 format for (col = 0; col < w; col++) { - if (dimming == 255) { // not needed, copy directly + 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 @@ -98,12 +96,14 @@ private: 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; + 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); } } @@ -176,9 +176,6 @@ private: bmpFS.read(lineBuffer, sizeof(lineBuffer)); uint8_t* bptr = lineBuffer; - #ifdef ELEKSTUBE_DIMMING - dimming=bri; - #endif // Convert 24 to 16 bit colors while copying to output buffer. for (uint16_t col = 0; col < w; col++) { @@ -202,12 +199,15 @@ private: b = c; g = c >> 8; r = c >> 16; } if (dimming != 255) { // only dimm when needed - b *= dimming; - g *= dimming; - r *= dimming; - b = b >> 8; - g = g >> 8; - r = r >> 8; + 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); } @@ -250,10 +250,6 @@ private: uint8_t lineBuffer[w * 2]; - #ifdef ELEKSTUBE_DIMMING - dimming=bri; - #endif - if (!realtimeMode || realtimeOverride) strip.service(); // 0,0 coordinates are top left @@ -265,7 +261,7 @@ private: // Colors are already in 16-bit R5, G6, B5 format for (col = 0; col < w; col++) { - if (dimming == 255) { // not needed, copy directly + 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 @@ -275,12 +271,14 @@ private: 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; + 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); } } @@ -301,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); @@ -317,13 +318,15 @@ public: void showDigit(uint8_t digit) { chip_select.setDigit(digit); uint8_t digitToDraw = digits[digit]; + if (digitToDraw < 10) digitToDraw += digitOffset; if (digitToDraw == blanked) { fillScreen(TFT_BLACK); return; } // if last digit was the same, skip loading from FS to buffer - if (digitToDraw == bufferedDigit) drawBuffer(); + 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]; @@ -347,15 +350,27 @@ public: void setDigit(uint8_t digit, uint8_t value, show_t show=yes) { uint8_t old_value = digits[digit]; - digits[digit] = value; + 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 a08d69d2..87827ac4 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 713c82f8..0fb5ac46 100644 --- a/usermods/EleksTube_IPS/usermod_elekstube_ips.h +++ b/usermods/EleksTube_IPS/usermod_elekstube_ips.h @@ -6,6 +6,11 @@ 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[]; + TFTs tfts; void updateClockDisplay(TFTs::show_t show=TFTs::yes) { bool set[6] = {false}; @@ -40,6 +45,7 @@ class ElekstubeIPSUsermod : public Usermod { public: uint8_t lastBri; + uint32_t lastCols[6]; TFTs::show_t fshow=TFTs::yes; void setup() { @@ -52,21 +58,94 @@ class ElekstubeIPSUsermod : public Usermod { } void loop() { - if (toki.isTick()) { - updateLocalTime(); - #ifdef ELEKSTUBE_DIMMING - if (bri != lastBri) { - fshow=TFTs::force; - lastBri = bri; - } - #endif - updateClockDisplay(fshow); - fshow=TFTs::yes; + 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[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) + { + 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/udp.cpp b/wled00/udp.cpp index 7f4bdcc0..3d7dabf3 100644 --- a/wled00/udp.cpp +++ b/wled00/udp.cpp @@ -652,19 +652,16 @@ void sendSysInfoUDP() uint8_t sequenceNumber = 0; // this needs to be shared across all outputs uint8_t realtimeBroadcast(uint8_t type, IPAddress client, uint16_t length, uint8_t *buffer, uint8_t bri, bool isRGBW) { - if (!interfacesInited) return 1; // network not initialised + if (!interfacesInited || !client[0] || !length) return 1; // network not initialised or dummy/unset IP address WiFiUDP ddpUdp; switch (type) { case 0: // DDP { - // calclate the number of UDP packets we need to send + // calculate the number of UDP packets we need to send uint16_t channelCount = length * 3; // 1 channel for every R,G,B value - uint16_t packetCount = channelCount / DDP_CHANNELS_PER_PACKET; - if (channelCount % DDP_CHANNELS_PER_PACKET) { - packetCount++; - } + uint16_t packetCount = ((channelCount-1) / DDP_CHANNELS_PER_PACKET) +1; // there are 3 channels per RGB pixel uint32_t channel = 0; // TODO: allow specifying the start channel